opmsec 0.1.3 → 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.
- package/.husky/pre-commit +1 -0
- package/bun.lock +4 -4
- package/docs/mint.json +2 -2
- package/package.json +7 -6
- package/packages/cli/src/commands/install.tsx +282 -24
- package/packages/cli/src/index.tsx +3 -1
- package/packages/cli/src/services/version.ts +156 -5
- package/packages/core/src/utils.ts +135 -0
- package/packages/scanner/src/services/openrouter.ts +18 -7
- package/packages/web/.next/BUILD_ID +1 -0
- package/packages/web/.next/app-build-manifest.json +18 -7
- package/packages/web/.next/app-path-routes-manifest.json +4 -0
- package/packages/web/.next/build-manifest.json +19 -6
- package/packages/web/.next/diagnostics/build-diagnostics.json +6 -0
- package/packages/web/.next/diagnostics/framework.json +1 -0
- package/packages/web/.next/export-marker.json +6 -0
- package/packages/web/.next/images-manifest.json +58 -0
- package/packages/web/.next/next-minimal-server.js.nft.json +1 -0
- package/packages/web/.next/next-server.js.nft.json +1 -0
- package/packages/web/.next/prerender-manifest.json +54 -4
- package/packages/web/.next/required-server-files.json +320 -0
- package/packages/web/.next/routes-manifest.json +53 -1
- package/packages/web/.next/server/app/_not-found/page.js +2 -0
- package/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/packages/web/.next/server/app/_not-found.html +1 -0
- package/packages/web/.next/server/app/_not-found.meta +8 -0
- package/packages/web/.next/server/app/_not-found.rsc +16 -0
- package/packages/web/.next/server/app/index.html +1 -0
- package/packages/web/.next/server/app/index.meta +7 -0
- package/packages/web/.next/server/app/index.rsc +20 -0
- package/packages/web/.next/server/app/page.js +2 -272
- package/packages/web/.next/server/app/page.js.nft.json +1 -0
- package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/packages/web/.next/server/app-paths-manifest.json +1 -0
- package/packages/web/.next/server/chunks/611.js +6 -0
- package/packages/web/.next/server/chunks/778.js +30 -0
- package/packages/web/.next/server/functions-config-manifest.json +4 -0
- package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -1
- package/packages/web/.next/server/middleware-build-manifest.js +1 -22
- package/packages/web/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/packages/web/.next/server/next-font-manifest.js +1 -1
- package/packages/web/.next/server/pages/404.html +1 -0
- package/packages/web/.next/server/pages/500.html +1 -0
- package/packages/web/.next/server/pages/_app.js +1 -0
- package/packages/web/.next/server/pages/_app.js.nft.json +1 -0
- package/packages/web/.next/server/pages/_document.js +1 -0
- package/packages/web/.next/server/pages/_document.js.nft.json +1 -0
- package/packages/web/.next/server/pages/_error.js +19 -0
- package/packages/web/.next/server/pages/_error.js.nft.json +1 -0
- package/packages/web/.next/server/pages-manifest.json +6 -1
- package/packages/web/.next/server/server-reference-manifest.js +1 -1
- package/packages/web/.next/server/server-reference-manifest.json +1 -5
- package/packages/web/.next/server/webpack-runtime.js +1 -209
- package/packages/web/.next/static/2XIFCTTKVZwN_RsNE-Rrr/_buildManifest.js +1 -0
- package/packages/web/.next/static/2XIFCTTKVZwN_RsNE-Rrr/_ssgManifest.js +1 -0
- package/packages/web/.next/static/chunks/255-0dc49b7a6e8e5c05.js +1 -0
- package/packages/web/.next/static/chunks/4bd1b696-382748cc942d8a14.js +1 -0
- package/packages/web/.next/static/chunks/app/_not-found/page-0da542be7eb33a64.js +1 -0
- package/packages/web/.next/static/chunks/app/layout-28a489fb4398663f.js +1 -0
- package/packages/web/.next/static/chunks/app/page-e58ccdb78625bce6.js +1 -0
- package/packages/web/.next/static/chunks/framework-ac73abd125e371fe.js +1 -0
- package/packages/web/.next/static/chunks/main-app-dd261207182e5a23.js +1 -0
- package/packages/web/.next/static/chunks/main-ee293fa6aa18bdd1.js +1 -0
- package/packages/web/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
- package/packages/web/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
- package/packages/web/.next/static/chunks/webpack-e1ae44446e7f7355.js +1 -0
- package/packages/web/.next/static/css/21d69157e271f2ab.css +3 -0
- package/packages/web/.next/trace +2 -5
- package/packages/web/app/page.tsx +5 -2
- package/packages/web/.next/server/vendor-chunks/@swc.js +0 -55
- package/packages/web/.next/server/vendor-chunks/next.js +0 -3010
- package/packages/web/.next/static/chunks/app/layout.js +0 -39
- package/packages/web/.next/static/chunks/app/page.js +0 -61
- package/packages/web/.next/static/chunks/app-pages-internals.js +0 -182
- package/packages/web/.next/static/chunks/main-app.js +0 -1882
- package/packages/web/.next/static/chunks/webpack.js +0 -1393
- package/packages/web/.next/static/css/app/layout.css +0 -1237
- package/packages/web/.next/static/development/_buildManifest.js +0 -1
- package/packages/web/.next/static/development/_ssgManifest.js +0 -1
- package/packages/web/.next/static/webpack/633457081244afec._.hot-update.json +0 -1
- package/packages/web/.next/static/webpack/6fee6306e0f98869.webpack.hot-update.json +0 -1
- package/packages/web/.next/static/webpack/73e341375c8d429e.webpack.hot-update.json +0 -1
- package/packages/web/.next/static/webpack/app/layout.6fee6306e0f98869.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/layout.73e341375c8d429e.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/page.6fee6306e0f98869.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/page.73e341375c8d429e.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/webpack.6fee6306e0f98869.hot-update.js +0 -12
- package/packages/web/.next/static/webpack/webpack.73e341375c8d429e.hot-update.js +0 -12
- /package/packages/web/.next/static/chunks/{polyfills.js → polyfills-42372ed130431b0a.js} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cd packages/web && bun run build
|
package/bun.lock
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
"react": "^18.3.1",
|
|
13
13
|
"react-devtools-core": "^5.3.2",
|
|
14
14
|
"terminal-image": "^4.2.0",
|
|
15
|
-
"viem": "^2.47.
|
|
15
|
+
"viem": "^2.47.4",
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"bun-types": "
|
|
18
|
+
"bun-types": "^1.3.10",
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
21
|
"packages/cli": {
|
|
@@ -317,7 +317,7 @@
|
|
|
317
317
|
|
|
318
318
|
"opm": ["opm@workspace:packages/cli"],
|
|
319
319
|
|
|
320
|
-
"ox": ["ox@0.14.
|
|
320
|
+
"ox": ["ox@0.14.5", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-HgmHmBveYO40H/R3K6TMrwYtHsx/u6TAB+GpZlgJCoW0Sq5Ttpjih0IZZiwGQw7T6vdW4IAyobYrE2mdAvyF8Q=="],
|
|
321
321
|
|
|
322
322
|
"pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="],
|
|
323
323
|
|
|
@@ -427,7 +427,7 @@
|
|
|
427
427
|
|
|
428
428
|
"utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="],
|
|
429
429
|
|
|
430
|
-
"viem": ["viem@2.47.
|
|
430
|
+
"viem": ["viem@2.47.4", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.14.5", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-h0Wp/SYmJO/HB4B/em1OZ3W1LaKrmr7jzaN7talSlZpo0LCn0V6rZ5g923j6sf4VUSrqp/gUuWuHFc7UcoIp8A=="],
|
|
431
431
|
|
|
432
432
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
|
433
433
|
|
package/docs/mint.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opmsec",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"bin": {
|
|
6
6
|
"opm": "packages/cli/src/index.tsx"
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"build:contracts": "cd packages/contracts && npx hardhat compile",
|
|
15
15
|
"deploy:contracts": "cd packages/contracts && npx hardhat run scripts/deploy.ts --network baseSepolia",
|
|
16
16
|
"test:contracts": "cd packages/contracts && npx hardhat test",
|
|
17
|
-
"scan": "bun run packages/scanner/src/index.ts"
|
|
17
|
+
"scan": "bun run packages/scanner/src/index.ts",
|
|
18
|
+
"prepare": "husky || true"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
21
|
"@ensdomains/ensjs": "^4.2.2",
|
|
@@ -25,15 +26,15 @@
|
|
|
25
26
|
"react": "^18.3.1",
|
|
26
27
|
"react-devtools-core": "^5.3.2",
|
|
27
28
|
"terminal-image": "^4.2.0",
|
|
28
|
-
"viem": "^2.47.
|
|
29
|
+
"viem": "^2.47.4"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
|
-
"bun-types": "
|
|
32
|
+
"bun-types": "^1.3.10"
|
|
32
33
|
},
|
|
33
34
|
"opm": {
|
|
34
|
-
"signature": "
|
|
35
|
+
"signature": "0xf469749982344b8aee24fadd624cad2b5262dd48964ceb0de8d79d2d256343a04aef4e99682b3e48c8468191bb1ff2076615b49006e9c83cd0de2bd696aa92e91b",
|
|
35
36
|
"author": "0x2a3942EbDd8c5ea3E66D3fC4301F56d0F15d4bE2",
|
|
36
37
|
"ensName": "djpaiethg.eth",
|
|
37
|
-
"checksum": "
|
|
38
|
+
"checksum": "0x3c9a3dfc1e62fd2b36611988cf642d34e6bc7de352c903d4dc6968253cfe1b61"
|
|
38
39
|
}
|
|
39
40
|
}
|
|
@@ -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 {
|
|
@@ -76,12 +82,16 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
|
|
|
76
82
|
// ─── Single package install with full security pipeline ───────────────────────
|
|
77
83
|
|
|
78
84
|
function SingleInstall({ packageName, version }: { packageName: string; version?: string }) {
|
|
85
|
+
const isEns = version ? isENSVersion(version) : false;
|
|
86
|
+
|
|
79
87
|
const [steps, setSteps] = useState<Steps>({
|
|
80
|
-
resolve: 'pending',
|
|
88
|
+
resolve: 'pending', ens: isEns ? 'pending' : 'skip',
|
|
89
|
+
cve: 'pending', onchain: 'pending',
|
|
81
90
|
signature: 'pending', chainpatrol: 'pending', report: 'pending',
|
|
82
91
|
install: 'pending',
|
|
83
92
|
});
|
|
84
93
|
const [result, setResult] = useState<SecurityResult | null>(null);
|
|
94
|
+
const [ensDetail, setEnsDetail] = useState<string | undefined>(undefined);
|
|
85
95
|
const [error, setError] = useState<string | null>(null);
|
|
86
96
|
const [done, setDone] = useState(false);
|
|
87
97
|
|
|
@@ -99,11 +109,35 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
|
|
|
99
109
|
blocked: false, warning: false,
|
|
100
110
|
};
|
|
101
111
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
}
|
|
106
139
|
|
|
140
|
+
// ── CVE check ──
|
|
107
141
|
update('cve', 'running');
|
|
108
142
|
r.cves = await queryOSV(packageName, r.resolvedVersion);
|
|
109
143
|
const cveCounts = categorizeCVEs(r.cves);
|
|
@@ -116,6 +150,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
|
|
|
116
150
|
setResult({ ...r });
|
|
117
151
|
update('cve', 'done');
|
|
118
152
|
|
|
153
|
+
// ── On-chain registry lookup ──
|
|
119
154
|
update('onchain', 'running');
|
|
120
155
|
try {
|
|
121
156
|
const info = await getPackageInfo(packageName, r.resolvedVersion);
|
|
@@ -133,12 +168,35 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
|
|
|
133
168
|
setResult({ ...r });
|
|
134
169
|
update('onchain', 'done');
|
|
135
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 ──
|
|
136
194
|
if (r.info?.exists) {
|
|
137
195
|
update('signature', 'running');
|
|
138
196
|
r.signatureValid = r.info.signature !== '0x'
|
|
139
197
|
? verifyChecksum(r.info.checksum, r.info.signature, r.info.author)
|
|
140
198
|
: false;
|
|
141
|
-
if (r.info.author) {
|
|
199
|
+
if (r.info.author && !r.ensName) {
|
|
142
200
|
r.ensName = await resolveENSName(r.info.author).catch(() => null) || undefined;
|
|
143
201
|
}
|
|
144
202
|
setResult({ ...r });
|
|
@@ -147,6 +205,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
|
|
|
147
205
|
update('signature', 'skip');
|
|
148
206
|
}
|
|
149
207
|
|
|
208
|
+
// ── ChainPatrol check ──
|
|
150
209
|
if (!r.info?.exists) {
|
|
151
210
|
update('chainpatrol', 'running');
|
|
152
211
|
const cp = await checkPackageWithChainPatrol(packageName).catch(() => null);
|
|
@@ -161,12 +220,14 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
|
|
|
161
220
|
update('chainpatrol', 'skip');
|
|
162
221
|
}
|
|
163
222
|
|
|
223
|
+
// ── Fileverse report ──
|
|
164
224
|
if (r.info?.reportURI && !r.info.reportURI.startsWith('local://')) {
|
|
165
225
|
update('report', 'done');
|
|
166
226
|
} else {
|
|
167
227
|
update('report', 'skip');
|
|
168
228
|
}
|
|
169
229
|
|
|
230
|
+
// ── Block or install ──
|
|
170
231
|
if (r.blocked) {
|
|
171
232
|
setError(`Blocked: ${r.blockReason || 'security risk detected'}`);
|
|
172
233
|
update('install', 'error');
|
|
@@ -175,7 +236,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
|
|
|
175
236
|
|
|
176
237
|
update('install', 'running');
|
|
177
238
|
try {
|
|
178
|
-
const target = `${packageName}
|
|
239
|
+
const target = `${packageName}@${r.resolvedVersion}`;
|
|
179
240
|
execSync(`npm install ${target}`, { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
|
|
180
241
|
} catch { /* non-fatal */ }
|
|
181
242
|
update('install', 'done');
|
|
@@ -191,12 +252,43 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
|
|
|
191
252
|
return (
|
|
192
253
|
<Box flexDirection="column">
|
|
193
254
|
<Header subtitle="install" />
|
|
194
|
-
{result &&
|
|
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
|
+
)}
|
|
195
266
|
<Text> </Text>
|
|
196
267
|
|
|
197
268
|
<StatusLine label="Resolve version" status={steps.resolve}
|
|
198
269
|
detail={steps.resolve === 'done' ? result?.resolvedVersion : undefined} />
|
|
199
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
|
+
|
|
200
292
|
<StatusLine label="Query CVE database (OSV)" status={steps.cve}
|
|
201
293
|
detail={steps.cve === 'done'
|
|
202
294
|
? (result?.cves.length
|
|
@@ -231,6 +323,22 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
|
|
|
231
323
|
</Box>
|
|
232
324
|
)}
|
|
233
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
|
+
|
|
234
342
|
<StatusLine label="On-chain registry lookup" status={steps.onchain}
|
|
235
343
|
detail={steps.onchain === 'done' && result?.info?.exists
|
|
236
344
|
? `${result.info.aggregateScore}/100 (${classifyRisk(result.info.aggregateScore)})`
|
|
@@ -269,14 +377,30 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
|
|
|
269
377
|
<Box flexDirection="column" marginTop={1}>
|
|
270
378
|
<Text color="gray">────────────────────────────────────────</Text>
|
|
271
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
|
+
)}
|
|
272
396
|
{result.info?.exists && (
|
|
273
397
|
<Box marginLeft={2}>
|
|
274
|
-
<Text color="gray">Risk:
|
|
398
|
+
<Text color="gray">Risk: </Text>
|
|
275
399
|
<RiskBadge level={classifyRisk(result.info.aggregateScore)} score={result.info.aggregateScore} />
|
|
276
400
|
</Box>
|
|
277
401
|
)}
|
|
278
402
|
<Box marginLeft={2}>
|
|
279
|
-
<Text color="gray">CVEs:
|
|
403
|
+
<Text color="gray">CVEs: </Text>
|
|
280
404
|
{result.cves.length > 0 ? (
|
|
281
405
|
<Text color={severeCount > 0 ? 'red' : 'yellow'}>
|
|
282
406
|
{result.cves.length} known ({cveCounts.critical > 0 ? `${cveCounts.critical} critical, ` : ''}{cveCounts.high} high, {cveCounts.medium} medium, {cveCounts.low} low)
|
|
@@ -287,19 +411,19 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
|
|
|
287
411
|
</Box>
|
|
288
412
|
{result.info?.exists && (
|
|
289
413
|
<Box marginLeft={2}>
|
|
290
|
-
<Text color="gray">Signature
|
|
414
|
+
<Text color="gray">Signature:</Text>
|
|
291
415
|
<Text color={result.signatureValid ? 'green' : 'red'}>
|
|
292
|
-
{result.signatureValid ? 'verified' : 'unverified'}
|
|
416
|
+
{' '}{result.signatureValid ? 'verified' : 'unverified'}
|
|
293
417
|
</Text>
|
|
294
418
|
</Box>
|
|
295
419
|
)}
|
|
296
420
|
{result.ensName && (
|
|
297
421
|
<Box marginLeft={2}>
|
|
298
|
-
<Text color="gray">Author:
|
|
422
|
+
<Text color="gray">Author: </Text>
|
|
299
423
|
<Text color="green">{result.ensName}</Text>
|
|
300
424
|
</Box>
|
|
301
425
|
)}
|
|
302
|
-
{result.warning && !result.blocked && (
|
|
426
|
+
{result.warning && !result.blocked && !result.autoBumped && (
|
|
303
427
|
<Box marginLeft={2}>
|
|
304
428
|
<Text color="yellow">⚠ Vulnerabilities detected — review before using in production</Text>
|
|
305
429
|
</Box>
|
|
@@ -311,7 +435,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
|
|
|
311
435
|
<Text color="yellow"> to fix known CVEs</Text>
|
|
312
436
|
</Box>
|
|
313
437
|
)}
|
|
314
|
-
{result.warning && result.safestVersion && (
|
|
438
|
+
{result.warning && result.safestVersion && !result.autoBumped && (
|
|
315
439
|
<Box marginLeft={2}>
|
|
316
440
|
<Text color="yellow">⚠ Consider using safest on-chain version: {result.safestVersion}</Text>
|
|
317
441
|
</Box>
|
|
@@ -338,6 +462,11 @@ interface BulkDepResult {
|
|
|
338
462
|
blocked: boolean;
|
|
339
463
|
blockReason?: string;
|
|
340
464
|
suggestedUpgrade?: string;
|
|
465
|
+
ensResolved?: boolean;
|
|
466
|
+
ensName?: string;
|
|
467
|
+
autoBumped?: boolean;
|
|
468
|
+
originalVersion?: string;
|
|
469
|
+
autoBumpReason?: string;
|
|
341
470
|
}
|
|
342
471
|
|
|
343
472
|
function BulkInstall() {
|
|
@@ -346,6 +475,9 @@ function BulkInstall() {
|
|
|
346
475
|
const [error, setError] = useState<string | null>(null);
|
|
347
476
|
const [installStatus, setInstallStatus] = useState<StepStatus>('pending');
|
|
348
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);
|
|
349
481
|
|
|
350
482
|
useEffect(() => {
|
|
351
483
|
runBulk().catch((err) => setError(String(err)));
|
|
@@ -368,15 +500,79 @@ function BulkInstall() {
|
|
|
368
500
|
return;
|
|
369
501
|
}
|
|
370
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 ──
|
|
371
551
|
const checked: BulkDepResult[] = [];
|
|
372
552
|
|
|
373
553
|
for (const [name, verRange] of entries) {
|
|
374
|
-
const
|
|
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
|
+
|
|
375
570
|
const entry: BulkDepResult = {
|
|
376
571
|
name, version: rawVersion,
|
|
377
572
|
cves: [], cvesCritical: 0, cvesHigh: 0,
|
|
378
573
|
onChain: false, score: null,
|
|
379
574
|
blocked: false,
|
|
575
|
+
ensResolved, ensName,
|
|
380
576
|
};
|
|
381
577
|
|
|
382
578
|
const [osvResult, infoResult] = await Promise.allSettled([
|
|
@@ -406,6 +602,25 @@ function BulkInstall() {
|
|
|
406
602
|
}
|
|
407
603
|
}
|
|
408
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
|
+
|
|
409
624
|
checked.push(entry);
|
|
410
625
|
setDeps([...checked]);
|
|
411
626
|
}
|
|
@@ -419,16 +634,24 @@ function BulkInstall() {
|
|
|
419
634
|
return;
|
|
420
635
|
}
|
|
421
636
|
|
|
637
|
+
// Build npm install command with correct versions
|
|
638
|
+
const bumpedDeps = checked.filter((d) => d.autoBumped || d.ensResolved);
|
|
422
639
|
setInstallStatus('running');
|
|
423
640
|
try {
|
|
424
|
-
|
|
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
|
+
}
|
|
425
647
|
} catch { /* non-fatal */ }
|
|
426
648
|
setInstallStatus('done');
|
|
427
649
|
}
|
|
428
650
|
|
|
429
651
|
const blockedDeps = deps.filter((d) => d.blocked);
|
|
430
|
-
const
|
|
431
|
-
const
|
|
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));
|
|
432
655
|
const totalCves = deps.reduce((s, d) => s + d.cves.length, 0);
|
|
433
656
|
|
|
434
657
|
return (
|
|
@@ -436,13 +659,48 @@ function BulkInstall() {
|
|
|
436
659
|
<Header subtitle="install" />
|
|
437
660
|
<Text> </Text>
|
|
438
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
|
+
|
|
439
667
|
<StatusLine label={`Scanning ${total} dependencies`} status={scanning ? 'running' : 'done'}
|
|
440
668
|
detail={!scanning ? `${deps.length} checked` : `${deps.length}/${total}`} />
|
|
441
669
|
|
|
442
670
|
{deps.length > 0 && (
|
|
443
671
|
<Box flexDirection="column" marginTop={1}>
|
|
444
|
-
{
|
|
672
|
+
{bumpedDeps.length > 0 && (
|
|
445
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}>
|
|
446
704
|
<Text color="red" bold> BLOCKED ({blockedDeps.length})</Text>
|
|
447
705
|
{blockedDeps.map((d) => (
|
|
448
706
|
<Box key={d.name} flexDirection="column" marginLeft={2}>
|
|
@@ -476,7 +734,7 @@ function BulkInstall() {
|
|
|
476
734
|
)}
|
|
477
735
|
|
|
478
736
|
{warnDeps.length > 0 && (
|
|
479
|
-
<Box flexDirection="column" marginTop={blockedDeps.length > 0 ? 1 : 0}>
|
|
737
|
+
<Box flexDirection="column" marginTop={(blockedDeps.length + bumpedDeps.length) > 0 ? 1 : 0}>
|
|
480
738
|
<Text color="yellow" bold> WARNING ({warnDeps.length})</Text>
|
|
481
739
|
{warnDeps.map((d) => (
|
|
482
740
|
<Box key={d.name} marginLeft={2}>
|
|
@@ -491,7 +749,7 @@ function BulkInstall() {
|
|
|
491
749
|
)}
|
|
492
750
|
|
|
493
751
|
{safeDeps.length > 0 && (
|
|
494
|
-
<Box flexDirection="column" marginTop={(blockedDeps.length + warnDeps.length) > 0 ? 1 : 0}>
|
|
752
|
+
<Box flexDirection="column" marginTop={(blockedDeps.length + warnDeps.length + bumpedDeps.length) > 0 ? 1 : 0}>
|
|
495
753
|
<Text color="green" bold> SAFE ({safeDeps.length})</Text>
|
|
496
754
|
{safeDeps.map((d) => (
|
|
497
755
|
<Box key={d.name} marginLeft={2}>
|
|
@@ -518,7 +776,7 @@ function BulkInstall() {
|
|
|
518
776
|
|
|
519
777
|
<Box marginTop={1}>
|
|
520
778
|
<Text color={blockedDeps.length > 0 ? 'red' : totalCves > 0 ? 'yellow' : 'green'} bold>
|
|
521
|
-
{deps.length} packages scanned: {blockedDeps.length} blocked, {warnDeps.length} warnings, {totalCves} CVEs
|
|
779
|
+
{deps.length} packages scanned: {blockedDeps.length} blocked, {bumpedDeps.length} resolved, {warnDeps.length} warnings, {totalCves} CVEs
|
|
522
780
|
</Text>
|
|
523
781
|
</Box>
|
|
524
782
|
|
|
@@ -106,7 +106,8 @@ function Help() {
|
|
|
106
106
|
<Box flexDirection="column" marginLeft={2}>
|
|
107
107
|
<Text color="cyan" bold>Security commands:</Text>
|
|
108
108
|
<Text> opm push [--token t] [--otp c] Sign, scan, publish, register</Text>
|
|
109
|
-
<Text> opm install [pkg]
|
|
109
|
+
<Text> opm install [pkg[@ver]] Install with on-chain security verification</Text>
|
|
110
|
+
<Text> opm install pkg@ens.eth Install safest version by ENS author</Text>
|
|
110
111
|
<Text> opm check Scan all deps: typosquats, CVEs, AI analysis</Text>
|
|
111
112
|
<Text> opm fix Auto-fix typosquats and vulnerable versions</Text>
|
|
112
113
|
<Text> opm audit Scan all deps against on-chain security data</Text>
|
|
@@ -135,6 +136,7 @@ function Help() {
|
|
|
135
136
|
<Text> </Text>
|
|
136
137
|
<Text color="gray">Aliases: i/add → install, rm → uninstall, ls → list</Text>
|
|
137
138
|
<Text color="gray"> view name.eth → author profile, view pkg → info</Text>
|
|
139
|
+
<Text color="gray"> pkg@name.eth → ENS-resolved safest version by author</Text>
|
|
138
140
|
<Text> </Text>
|
|
139
141
|
<Text color="cyan" bold>Environment (install/audit/info/view need no config):</Text>
|
|
140
142
|
<Text> OPM_SIGNING_KEY Author signing key (for push only)</Text>
|