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
@@ -0,0 +1,362 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { HIGH_RISK_THRESHOLD, MEDIUM_RISK_THRESHOLD, classifyRisk, truncateAddress } from '@opm/core';
4
+ import type { OnChainPackageInfo } from '@opm/core';
5
+ import { Header } from '../components/Header';
6
+ import { StatusLine } from '../components/StatusLine';
7
+ import { RiskBadge } from '../components/RiskBadge';
8
+ import { getPackageInfo, getSafestVersion } from '../services/contract';
9
+ import { verifyChecksum } from '../services/signature';
10
+ import { resolveENSName } from '../services/ens';
11
+ import { checkPackageWithChainPatrol } from '../services/chainpatrol';
12
+ import { queryOSV, getOSVSeverity, getFixedVersion, type OSVVulnerability } from '../services/osv';
13
+ import { resolveVersion } from '../services/version';
14
+ import { execSync } from 'child_process';
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+
18
+ type StepStatus = 'pending' | 'running' | 'done' | 'error' | 'skip';
19
+
20
+ interface Steps {
21
+ resolve: StepStatus;
22
+ cve: StepStatus;
23
+ onchain: StepStatus;
24
+ signature: StepStatus;
25
+ chainpatrol: StepStatus;
26
+ report: StepStatus;
27
+ install: StepStatus;
28
+ }
29
+
30
+ interface SecurityResult {
31
+ name: string;
32
+ version: string;
33
+ resolvedVersion: string;
34
+ cves: OSVVulnerability[];
35
+ info?: OnChainPackageInfo;
36
+ signatureValid?: boolean;
37
+ ensName?: string;
38
+ chainPatrolStatus?: string;
39
+ blocked: boolean;
40
+ warning: boolean;
41
+ blockReason?: string;
42
+ safestVersion?: string;
43
+ }
44
+
45
+ interface InstallCommandProps {
46
+ packageName?: string;
47
+ version?: string;
48
+ }
49
+
50
+ function categorizeCVEs(cves: OSVVulnerability[]) {
51
+ let critical = 0, high = 0, medium = 0, low = 0;
52
+ for (const cve of cves) {
53
+ const sev = getOSVSeverity(cve);
54
+ if (sev === 'CRITICAL') critical++;
55
+ else if (sev === 'HIGH') high++;
56
+ else if (sev === 'MEDIUM') medium++;
57
+ else low++;
58
+ }
59
+ return { critical, high, medium, low };
60
+ }
61
+
62
+ function sevColor(sev: string): string {
63
+ if (sev === 'CRITICAL') return 'magenta';
64
+ if (sev === 'HIGH') return 'red';
65
+ if (sev === 'MEDIUM') return 'yellow';
66
+ return 'gray';
67
+ }
68
+
69
+ export function InstallCommand({ packageName, version }: InstallCommandProps) {
70
+ const [steps, setSteps] = useState<Steps>({
71
+ resolve: 'pending', cve: 'pending', onchain: 'pending',
72
+ signature: 'pending', chainpatrol: 'pending', report: 'pending',
73
+ install: 'pending',
74
+ });
75
+ const [result, setResult] = useState<SecurityResult | null>(null);
76
+ const [error, setError] = useState<string | null>(null);
77
+ const [done, setDone] = useState(false);
78
+
79
+ const update = (key: keyof Steps, status: StepStatus) =>
80
+ setSteps((s) => ({ ...s, [key]: status }));
81
+
82
+ useEffect(() => {
83
+ run().catch((err) => setError(String(err)));
84
+ }, []);
85
+
86
+ 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
+ const r: SecurityResult = {
95
+ name: pkg.name, version: pkg.version,
96
+ resolvedVersion: pkg.version, cves: [],
97
+ blocked: false, warning: false,
98
+ };
99
+
100
+ update('resolve', 'running');
101
+ r.resolvedVersion = await resolveVersion(pkg.name, pkg.version);
102
+ setResult({ ...r });
103
+ update('resolve', 'done');
104
+
105
+ update('cve', 'running');
106
+ r.cves = await queryOSV(pkg.name, r.resolvedVersion);
107
+ const cveCounts = categorizeCVEs(r.cves);
108
+ if (cveCounts.critical > 0) {
109
+ r.blocked = true;
110
+ r.blockReason = `${cveCounts.critical} CRITICAL CVE(s) found`;
111
+ } else if (cveCounts.high > 0) {
112
+ r.warning = true;
113
+ }
114
+ setResult({ ...r });
115
+ update('cve', 'done');
116
+
117
+ update('onchain', 'running');
118
+ try {
119
+ const info = await getPackageInfo(pkg.name, r.resolvedVersion);
120
+ r.info = info;
121
+
122
+ if (info.exists) {
123
+ if (info.aggregateScore >= HIGH_RISK_THRESHOLD) {
124
+ r.blocked = true;
125
+ r.blockReason = (r.blockReason ? r.blockReason + '; ' : '') + `risk score ${info.aggregateScore}/100`;
126
+ } else if (info.aggregateScore >= MEDIUM_RISK_THRESHOLD) {
127
+ r.warning = true;
128
+ r.safestVersion = await getSafestVersion(pkg.name).catch(() => undefined);
129
+ }
130
+ }
131
+ } catch { /* not in registry */ }
132
+ setResult({ ...r });
133
+ update('onchain', 'done');
134
+
135
+ if (r.info?.exists) {
136
+ update('signature', 'running');
137
+ r.signatureValid = r.info.signature !== '0x'
138
+ ? verifyChecksum(r.info.checksum, r.info.signature, r.info.author)
139
+ : false;
140
+ if (r.info.author) {
141
+ r.ensName = await resolveENSName(r.info.author).catch(() => null) || undefined;
142
+ }
143
+ setResult({ ...r });
144
+ update('signature', 'done');
145
+ } else {
146
+ update('signature', 'skip');
147
+ }
148
+
149
+ if (!r.info?.exists) {
150
+ update('chainpatrol', 'running');
151
+ const cp = await checkPackageWithChainPatrol(pkg.name).catch(() => null);
152
+ r.chainPatrolStatus = cp?.status;
153
+ if (cp?.status === 'BLOCKED') {
154
+ r.blocked = true;
155
+ r.blockReason = (r.blockReason ? r.blockReason + '; ' : '') + 'ChainPatrol BLOCKED';
156
+ }
157
+ setResult({ ...r });
158
+ update('chainpatrol', 'done');
159
+ } else {
160
+ update('chainpatrol', 'skip');
161
+ }
162
+
163
+ if (r.info?.reportURI && !r.info.reportURI.startsWith('local://')) {
164
+ update('report', 'done');
165
+ } else {
166
+ update('report', 'skip');
167
+ }
168
+
169
+ if (r.blocked) {
170
+ setError(`Blocked: ${r.blockReason || 'security risk detected'}`);
171
+ update('install', 'error');
172
+ return;
173
+ }
174
+
175
+ update('install', 'running');
176
+ try {
177
+ const installTarget = packageName
178
+ ? `${packageName}${version ? `@${version}` : ''}`
179
+ : '';
180
+ execSync(`npm install ${installTarget}`, { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
181
+ } catch { /* non-fatal */ }
182
+ update('install', 'done');
183
+ setDone(true);
184
+ }
185
+
186
+ const cveCounts = result ? categorizeCVEs(result.cves) : { critical: 0, high: 0, medium: 0, low: 0 };
187
+ const severeCount = cveCounts.critical + cveCounts.high;
188
+ const suggestedUpgrade = result?.cves.length
189
+ ? getBestUpgradeVersion(result.cves, result.resolvedVersion)
190
+ : null;
191
+
192
+ return (
193
+ <Box flexDirection="column">
194
+ <Header subtitle="install" />
195
+ {result && <Text color="white" bold> {result.name}@{result.resolvedVersion}</Text>}
196
+ <Text> </Text>
197
+
198
+ <StatusLine label="Resolve version" status={steps.resolve}
199
+ detail={steps.resolve === 'done' ? result?.resolvedVersion : undefined} />
200
+
201
+ <StatusLine label="Query CVE database (OSV)" status={steps.cve}
202
+ detail={steps.cve === 'done'
203
+ ? (result?.cves.length
204
+ ? `${result.cves.length} known (${severeCount > 0 ? `${severeCount} high/critical` : 'none critical'})`
205
+ : 'clean')
206
+ : undefined} />
207
+ {steps.cve === 'done' && result && result.cves.length > 0 && (
208
+ <Box flexDirection="column" marginLeft={4}>
209
+ {result.cves.slice(0, 5).map((cve) => {
210
+ const sev = getOSVSeverity(cve);
211
+ const fix = getFixedVersion(cve, result.resolvedVersion);
212
+ return (
213
+ <Box key={cve.id} flexDirection="column">
214
+ <Box>
215
+ <Text color={sevColor(sev)} bold>{sev.padEnd(9)}</Text>
216
+ <Text color="white">{cve.id}</Text>
217
+ </Box>
218
+ <Box marginLeft={2}>
219
+ <Text color="gray">{cve.summary?.slice(0, 70)}</Text>
220
+ </Box>
221
+ {fix && (
222
+ <Box marginLeft={2}>
223
+ <Text color="green">upgrade to {fix}</Text>
224
+ </Box>
225
+ )}
226
+ </Box>
227
+ );
228
+ })}
229
+ {result.cves.length > 5 && (
230
+ <Text color="gray"> ...and {result.cves.length - 5} more</Text>
231
+ )}
232
+ </Box>
233
+ )}
234
+
235
+ <StatusLine label="On-chain registry lookup" status={steps.onchain}
236
+ detail={steps.onchain === 'done' && result?.info?.exists
237
+ ? `${result.info.aggregateScore}/100 (${classifyRisk(result.info.aggregateScore)})`
238
+ : steps.onchain === 'done' ? 'not registered' : undefined} />
239
+
240
+ <StatusLine label="Signature verification" status={steps.signature}
241
+ detail={steps.signature === 'skip' ? 'n/a' : undefined} />
242
+ {steps.signature === 'done' && result?.info && (
243
+ <Box flexDirection="column" marginLeft={4}>
244
+ <Box>
245
+ <Text color="gray">Checksum: </Text>
246
+ <Text color="cyan">{truncateAddress(result.info.checksum)}</Text>
247
+ </Box>
248
+ <Box>
249
+ <Text color="gray">Signature: </Text>
250
+ <Text color="cyan">{truncateAddress(result.info.signature)}</Text>
251
+ <Text color={result.signatureValid ? 'green' : 'red'}> {result.signatureValid ? '✓ verified' : '✗ invalid'}</Text>
252
+ </Box>
253
+ <Box>
254
+ <Text color="gray">Author: </Text>
255
+ <Text color="cyan">{truncateAddress(result.info.author)}</Text>
256
+ {result.ensName && <Text color="green"> → {result.ensName}</Text>}
257
+ </Box>
258
+ </Box>
259
+ )}
260
+
261
+ <StatusLine label="ChainPatrol check" status={steps.chainpatrol}
262
+ detail={steps.chainpatrol === 'done' ? (result?.chainPatrolStatus || 'UNKNOWN') : steps.chainpatrol === 'skip' ? 'n/a' : undefined} />
263
+
264
+ <StatusLine label="Fileverse report" status={steps.report}
265
+ detail={steps.report === 'done' ? 'linked' : steps.report === 'skip' ? 'n/a' : undefined} />
266
+
267
+ <StatusLine label="Install via npm" status={steps.install} />
268
+
269
+ {(steps.install === 'done' || steps.install === 'error') && result && (
270
+ <Box flexDirection="column" marginTop={1}>
271
+ <Text color="gray">────────────────────────────────────────</Text>
272
+ <Text color="white" bold> Security Summary</Text>
273
+ {result.info?.exists && (
274
+ <Box marginLeft={2}>
275
+ <Text color="gray">Risk: </Text>
276
+ <RiskBadge level={classifyRisk(result.info.aggregateScore)} score={result.info.aggregateScore} />
277
+ </Box>
278
+ )}
279
+ <Box marginLeft={2}>
280
+ <Text color="gray">CVEs: </Text>
281
+ {result.cves.length > 0 ? (
282
+ <Text color={severeCount > 0 ? 'red' : 'yellow'}>
283
+ {result.cves.length} known ({cveCounts.critical > 0 ? `${cveCounts.critical} critical, ` : ''}{cveCounts.high} high, {cveCounts.medium} medium, {cveCounts.low} low)
284
+ </Text>
285
+ ) : (
286
+ <Text color="green">none found</Text>
287
+ )}
288
+ </Box>
289
+ {result.info?.exists && (
290
+ <Box marginLeft={2}>
291
+ <Text color="gray">Signature: </Text>
292
+ <Text color={result.signatureValid ? 'green' : 'red'}>
293
+ {result.signatureValid ? 'verified' : 'unverified'}
294
+ </Text>
295
+ </Box>
296
+ )}
297
+ {result.ensName && (
298
+ <Box marginLeft={2}>
299
+ <Text color="gray">Author: </Text>
300
+ <Text color="green">{result.ensName}</Text>
301
+ </Box>
302
+ )}
303
+ {result.warning && !result.blocked && (
304
+ <Box marginLeft={2}>
305
+ <Text color="yellow">⚠ Vulnerabilities detected — review before using in production</Text>
306
+ </Box>
307
+ )}
308
+ {suggestedUpgrade && suggestedUpgrade !== result.resolvedVersion && (
309
+ <Box marginLeft={2}>
310
+ <Text color="yellow">⚠ Upgrade to </Text>
311
+ <Text color="green" bold>{suggestedUpgrade}</Text>
312
+ <Text color="yellow"> to fix known CVEs</Text>
313
+ </Box>
314
+ )}
315
+ {result.warning && result.safestVersion && (
316
+ <Box marginLeft={2}>
317
+ <Text color="yellow">⚠ Consider using safest on-chain version: {result.safestVersion}</Text>
318
+ </Box>
319
+ )}
320
+ </Box>
321
+ )}
322
+
323
+ {error && <Text color="red">{error}</Text>}
324
+ {done && <Text color="green" bold>Done.</Text>}
325
+ </Box>
326
+ );
327
+ }
328
+
329
+ function getBestUpgradeVersion(cves: OSVVulnerability[], currentVersion: string): string | null {
330
+ let highest: string | null = null;
331
+ for (const cve of cves) {
332
+ const fix = getFixedVersion(cve, currentVersion);
333
+ if (fix && (!highest || compareSemver(fix, highest) > 0)) {
334
+ highest = fix;
335
+ }
336
+ }
337
+ return highest;
338
+ }
339
+
340
+ function compareSemver(a: string, b: string): number {
341
+ const pa = a.replace(/^v/, '').split('.').map(Number);
342
+ const pb = b.replace(/^v/, '').split('.').map(Number);
343
+ for (let i = 0; i < 3; i++) {
344
+ const diff = (pa[i] || 0) - (pb[i] || 0);
345
+ if (diff !== 0) return diff;
346
+ }
347
+ return 0;
348
+ }
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
+ }
@@ -0,0 +1,36 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { Header } from '../components/Header';
4
+ import { execSync } from 'child_process';
5
+
6
+ interface PassthroughProps {
7
+ command: string;
8
+ args: string[];
9
+ }
10
+
11
+ export function PassthroughCommand({ command, args }: PassthroughProps) {
12
+ const [output, setOutput] = useState('');
13
+ const [exitCode, setExitCode] = useState<number | null>(null);
14
+
15
+ useEffect(() => {
16
+ const npmArgs = [command, ...args].join(' ');
17
+ try {
18
+ const result = execSync(`npm ${npmArgs}`, { encoding: 'utf-8', stdio: 'pipe' });
19
+ setOutput(result);
20
+ setExitCode(0);
21
+ } catch (err: any) {
22
+ setOutput(err.stdout || err.stderr || err.message);
23
+ setExitCode(err.status ?? 1);
24
+ }
25
+ }, []);
26
+
27
+ return (
28
+ <Box flexDirection="column">
29
+ <Header subtitle={command} />
30
+ {output && <Text>{output}</Text>}
31
+ {exitCode !== null && exitCode !== 0 && (
32
+ <Text color="red">Exited with code {exitCode}</Text>
33
+ )}
34
+ </Box>
35
+ );
36
+ }