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