opmsec 0.1.5 → 0.1.52
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 +1 -2
- package/package.json +3 -3
- package/packages/cli/src/commands/audit.tsx +72 -5
- package/packages/cli/src/commands/check.tsx +80 -12
- package/packages/cli/src/commands/push.tsx +40 -3
- package/packages/cli/src/index.tsx +16 -0
- package/packages/cli/src/services/dep-graph.ts +298 -0
- package/packages/cli/src/services/ens-records.ts +5 -0
- package/packages/cli/src/services/lockfile.ts +224 -0
- package/packages/cli/src/services/merkle.ts +52 -0
- package/packages/cli/src/services/osv.ts +50 -0
- package/packages/core/src/constants.ts +2 -1
- package/packages/core/src/model-rankings.ts +12 -4
- package/packages/core/src/types.ts +38 -0
- package/packages/core/src/utils.ts +18 -0
- package/packages/scanner/src/agents/base-agent.ts +22 -10
- package/packages/scanner/src/queue/memory-queue.ts +23 -9
- package/packages/scanner/src/services/openrouter.ts +8 -2
- package/packages/web/.next/BUILD_ID +1 -1
- package/packages/web/.next/app-build-manifest.json +19 -7
- package/packages/web/.next/build-manifest.json +19 -6
- package/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/packages/web/.next/server/app/_not-found.html +1 -1
- package/packages/web/.next/server/app/_not-found.rsc +1 -1
- package/packages/web/.next/server/app/index.html +2 -2
- package/packages/web/.next/server/app/index.rsc +2 -2
- package/packages/web/.next/server/app/page.js +8 -272
- 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/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 -1
- package/packages/web/.next/server/pages/500.html +1 -1
- 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/chunks/app/page-7d2a23ad744d529a.js +1 -0
- package/packages/web/.next/trace +2 -2
- package/packages/web/app/page.tsx +0 -2
- package/packages/web/.next/static/chunks/app/layout.js +0 -69
- package/packages/web/.next/static/chunks/app/page-7e086379698b9fb0.js +0 -1
- package/packages/web/.next/static/chunks/app/page.js +0 -357
- package/packages/web/.next/static/chunks/webpack.js +0 -1393
- 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/16f18baa938a434c.webpack.hot-update.json +0 -1
- package/packages/web/.next/static/webpack/5fe9fe8578f9c3d2.webpack.hot-update.json +0 -1
- package/packages/web/.next/static/webpack/653e365406c0d9ac.webpack.hot-update.json +0 -1
- package/packages/web/.next/static/webpack/6800169a899e3a8b.webpack.hot-update.json +0 -1
- package/packages/web/.next/static/webpack/73c7d02260cc80e4.webpack.hot-update.json +0 -1
- package/packages/web/.next/static/webpack/a2d85d19aa028de1.webpack.hot-update.json +0 -1
- package/packages/web/.next/static/webpack/app/layout.16f18baa938a434c.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/layout.5fe9fe8578f9c3d2.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/layout.653e365406c0d9ac.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/layout.6800169a899e3a8b.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/layout.73c7d02260cc80e4.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/layout.a2d85d19aa028de1.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/page.653e365406c0d9ac.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/page.6800169a899e3a8b.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/page.73c7d02260cc80e4.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/app/page.a2d85d19aa028de1.hot-update.js +0 -22
- package/packages/web/.next/static/webpack/webpack.16f18baa938a434c.hot-update.js +0 -12
- package/packages/web/.next/static/webpack/webpack.5fe9fe8578f9c3d2.hot-update.js +0 -12
- package/packages/web/.next/static/webpack/webpack.653e365406c0d9ac.hot-update.js +0 -12
- package/packages/web/.next/static/webpack/webpack.6800169a899e3a8b.hot-update.js +0 -12
- package/packages/web/.next/static/webpack/webpack.73c7d02260cc80e4.hot-update.js +0 -12
- package/packages/web/.next/static/webpack/webpack.a2d85d19aa028de1.hot-update.js +0 -12
- /package/packages/web/.next/static/{0esGzFBCzREfVwijEGDfL → dQsL29tmXanBGrmJ9Agh2}/_buildManifest.js +0 -0
- /package/packages/web/.next/static/{0esGzFBCzREfVwijEGDfL → dQsL29tmXanBGrmJ9Agh2}/_ssgManifest.js +0 -0
package/.env.example
CHANGED
|
@@ -21,5 +21,4 @@ FILEVERSE_API_KEY= # from ddocs.new → Settings → Developer
|
|
|
21
21
|
# ETH_SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
|
|
22
22
|
# FILEVERSE_API_URL=http://localhost:8001
|
|
23
23
|
# CHAINPATROL_API_KEY= # optional, for blocklist checks
|
|
24
|
-
# ARTIFICIAL_ANALYSIS_API_KEY= # optional, for model-weighted scoring
|
|
25
|
-
PINATA_JWT=your_pinata_jwt_here
|
|
24
|
+
# ARTIFICIAL_ANALYSIS_API_KEY= # optional, for model-weighted scoring
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opmsec",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.52",
|
|
4
4
|
"private": false,
|
|
5
5
|
"bin": {
|
|
6
6
|
"opm": "packages/cli/src/index.tsx"
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"bun-types": "latest"
|
|
33
33
|
},
|
|
34
34
|
"opm": {
|
|
35
|
-
"signature": "
|
|
35
|
+
"signature": "0xa1c1b29e1b878ba3cee4cd14d75e79e5a7e683bbd57ef7cc2917c2693f2310400aaae5dccfb655498cb26e2e6ced586fd3d987ca484f82b8721e480836e88fa61b",
|
|
36
36
|
"author": "0x2a3942EbDd8c5ea3E66D3fC4301F56d0F15d4bE2",
|
|
37
37
|
"ensName": "djpaiethg.eth",
|
|
38
|
-
"checksum": "
|
|
38
|
+
"checksum": "0x0ce8eaad637d74e4172c3b9de6af85ec600bb0726dd53bc3d1520615542daa64"
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { HIGH_RISK_THRESHOLD, MEDIUM_RISK_THRESHOLD, truncateAddress, classifyRisk } from '@opm/core';
|
|
4
|
+
import type { DepGraphResult, FlaggedPath } from '@opm/core';
|
|
4
5
|
import { Header } from '../components/Header';
|
|
5
6
|
import { RiskBadge } from '../components/RiskBadge';
|
|
6
7
|
import { StatusLine } from '../components/StatusLine';
|
|
@@ -8,6 +9,7 @@ import { getPackageInfo } from '../services/contract';
|
|
|
8
9
|
import { checkPackageWithChainPatrol } from '../services/chainpatrol';
|
|
9
10
|
import { queryOSV, getOSVSeverity } from '../services/osv';
|
|
10
11
|
import { resolveVersion } from '../services/version';
|
|
12
|
+
import { resolveAndScanDepGraph } from '../services/dep-graph';
|
|
11
13
|
import * as fs from 'fs';
|
|
12
14
|
import * as path from 'path';
|
|
13
15
|
|
|
@@ -22,8 +24,12 @@ interface DepResult {
|
|
|
22
24
|
cveHighCount: number;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
type Phase = 'graph' | 'direct' | 'done';
|
|
28
|
+
|
|
25
29
|
export function AuditCommand() {
|
|
26
|
-
const [
|
|
30
|
+
const [phase, setPhase] = useState<Phase>('graph');
|
|
31
|
+
const [graphStatus, setGraphStatus] = useState('Resolving...');
|
|
32
|
+
const [graph, setGraph] = useState<DepGraphResult | null>(null);
|
|
27
33
|
const [results, setResults] = useState<DepResult[]>([]);
|
|
28
34
|
const [error, setError] = useState<string | null>(null);
|
|
29
35
|
|
|
@@ -32,6 +38,16 @@ export function AuditCommand() {
|
|
|
32
38
|
}, []);
|
|
33
39
|
|
|
34
40
|
async function runAudit() {
|
|
41
|
+
let graphResult: DepGraphResult | null = null;
|
|
42
|
+
try {
|
|
43
|
+
graphResult = await resolveAndScanDepGraph(process.cwd(), (msg) => setGraphStatus(msg));
|
|
44
|
+
setGraph(graphResult);
|
|
45
|
+
} catch {
|
|
46
|
+
setGraphStatus('Lockfile not found — direct deps only');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setPhase('direct');
|
|
50
|
+
|
|
35
51
|
const pkgPath = path.resolve('package.json');
|
|
36
52
|
if (!fs.existsSync(pkgPath)) throw new Error('No package.json found');
|
|
37
53
|
|
|
@@ -40,7 +56,7 @@ export function AuditCommand() {
|
|
|
40
56
|
const entries = Object.entries(deps) as [string, string][];
|
|
41
57
|
|
|
42
58
|
if (entries.length === 0) {
|
|
43
|
-
|
|
59
|
+
setPhase('done');
|
|
44
60
|
return;
|
|
45
61
|
}
|
|
46
62
|
|
|
@@ -76,7 +92,7 @@ export function AuditCommand() {
|
|
|
76
92
|
setResults([...checked]);
|
|
77
93
|
}
|
|
78
94
|
|
|
79
|
-
|
|
95
|
+
setPhase('done');
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
const high = results.filter((r) => r.score !== null && r.score >= HIGH_RISK_THRESHOLD);
|
|
@@ -88,8 +104,17 @@ export function AuditCommand() {
|
|
|
88
104
|
return (
|
|
89
105
|
<Box flexDirection="column">
|
|
90
106
|
<Header subtitle="audit" />
|
|
91
|
-
|
|
107
|
+
|
|
108
|
+
<StatusLine label="Resolving dependency tree"
|
|
109
|
+
status={phase === 'graph' ? 'running' : 'done'}
|
|
110
|
+
detail={phase === 'graph' ? graphStatus : graph
|
|
111
|
+
? `${graph.totalDeps} deps (${graph.directDeps} direct, ${graph.transitiveDeps} transitive)`
|
|
112
|
+
: 'direct deps only'} />
|
|
113
|
+
|
|
114
|
+
<StatusLine label={`Checking ${results.length} direct dependencies`}
|
|
115
|
+
status={phase === 'done' ? 'done' : 'running'} />
|
|
92
116
|
<Text> </Text>
|
|
117
|
+
|
|
93
118
|
{results.map((r) => (
|
|
94
119
|
<Box key={r.name} marginLeft={2}>
|
|
95
120
|
<Box width={30}>
|
|
@@ -113,7 +138,38 @@ export function AuditCommand() {
|
|
|
113
138
|
)}
|
|
114
139
|
</Box>
|
|
115
140
|
))}
|
|
116
|
-
|
|
141
|
+
|
|
142
|
+
{/* Transitive flagged paths from dep graph */}
|
|
143
|
+
{phase === 'done' && graph && graph.flaggedPaths.length > 0 && (
|
|
144
|
+
<Box flexDirection="column" marginTop={1}>
|
|
145
|
+
<Text color="gray">────────────────────────────────────────</Text>
|
|
146
|
+
<Text color="white" bold> Transitive Risk Paths ({graph.flaggedPaths.length})</Text>
|
|
147
|
+
{graph.flaggedPaths.slice(0, 10).map((fp, i) => {
|
|
148
|
+
const sevColor = fp.severity === 'CRITICAL' || fp.severity === 'HIGH' ? 'red' : 'yellow';
|
|
149
|
+
const chain = fp.path.slice(1).join(' → ');
|
|
150
|
+
return (
|
|
151
|
+
<Box key={i} marginLeft={2} flexDirection="column">
|
|
152
|
+
<Box>
|
|
153
|
+
<Text color={sevColor}>{fp.severity === 'CRITICAL' || fp.severity === 'HIGH' ? '✖' : '⚠'} </Text>
|
|
154
|
+
<Text color={sevColor} bold>{fp.severity.padEnd(9)}</Text>
|
|
155
|
+
<Text color="white">{chain}</Text>
|
|
156
|
+
</Box>
|
|
157
|
+
<Box marginLeft={12}>
|
|
158
|
+
<Text color="gray">{fp.reason}</Text>
|
|
159
|
+
{fp.fix && <Text color="green"> → {fp.fix}</Text>}
|
|
160
|
+
</Box>
|
|
161
|
+
</Box>
|
|
162
|
+
);
|
|
163
|
+
})}
|
|
164
|
+
{graph.flaggedPaths.length > 10 && (
|
|
165
|
+
<Box marginLeft={2}>
|
|
166
|
+
<Text color="gray">... and {graph.flaggedPaths.length - 10} more</Text>
|
|
167
|
+
</Box>
|
|
168
|
+
)}
|
|
169
|
+
</Box>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{phase === 'done' && (
|
|
117
173
|
<Box flexDirection="column" marginTop={1}>
|
|
118
174
|
<Text color="gray">────────────────────────────────────────</Text>
|
|
119
175
|
<Box>
|
|
@@ -131,6 +187,17 @@ export function AuditCommand() {
|
|
|
131
187
|
</>
|
|
132
188
|
)}
|
|
133
189
|
</Box>
|
|
190
|
+
{graph && (
|
|
191
|
+
<Box marginTop={0}>
|
|
192
|
+
<Text color="gray">📦 {graph.totalDeps} total ({graph.directDeps} direct, {graph.transitiveDeps} transitive)</Text>
|
|
193
|
+
{graph.flaggedPaths.length > 0 && (
|
|
194
|
+
<Text color="yellow"> · {graph.flaggedPaths.length} transitive risk paths</Text>
|
|
195
|
+
)}
|
|
196
|
+
</Box>
|
|
197
|
+
)}
|
|
198
|
+
{graph && (
|
|
199
|
+
<Text color="gray">🔗 Merkle root: {graph.merkleRoot.slice(0, 18)}...</Text>
|
|
200
|
+
)}
|
|
134
201
|
{high.length > 0 && (
|
|
135
202
|
<Text color="red" bold>⚠ {high.length} package(s) above risk threshold!</Text>
|
|
136
203
|
)}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { CHECK_SYSTEM_PROMPT, buildCheckPrompt, classifyRisk, getModelRankingFor } from '@opm/core';
|
|
4
|
-
import type { DepEntry, CheckReport, CheckDepResult, CheckAgentResult } from '@opm/core';
|
|
4
|
+
import type { DepEntry, CheckReport, CheckDepResult, CheckAgentResult, DepGraphResult, FlaggedPath } from '@opm/core';
|
|
5
5
|
import { Header } from '../components/Header';
|
|
6
6
|
import { StatusLine } from '../components/StatusLine';
|
|
7
7
|
import { RiskBadge } from '../components/RiskBadge';
|
|
8
8
|
import { Hyperlink } from '../components/Hyperlink';
|
|
9
|
+
import { resolveAndScanDepGraph } from '../services/dep-graph';
|
|
9
10
|
import { queryOSV, getOSVSeverity, getFixedVersion } from '../services/osv';
|
|
10
11
|
import { getPackageInfo } from '../services/contract';
|
|
11
12
|
import { detectTyposquatBatch } from '../services/typosquat';
|
|
@@ -13,10 +14,12 @@ import { callLLMRaw, getAgentConfigs, uploadCheckReportToFileverse } from '@opm/
|
|
|
13
14
|
import * as fs from 'fs';
|
|
14
15
|
import * as path from 'path';
|
|
15
16
|
|
|
16
|
-
type Phase = 'scanning' | 'agents' | 'upload' | 'done';
|
|
17
|
+
type Phase = 'graph' | 'scanning' | 'agents' | 'upload' | 'done';
|
|
17
18
|
|
|
18
19
|
export function CheckCommand() {
|
|
19
|
-
const [phase, setPhase] = useState<Phase>('
|
|
20
|
+
const [phase, setPhase] = useState<Phase>('graph');
|
|
21
|
+
const [graphStatus, setGraphStatus] = useState('Resolving...');
|
|
22
|
+
const [graph, setGraph] = useState<DepGraphResult | null>(null);
|
|
20
23
|
const [report, setReport] = useState<CheckReport | null>(null);
|
|
21
24
|
const [reportLink, setReportLink] = useState<string | null>(null);
|
|
22
25
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
|
@@ -34,6 +37,16 @@ export function CheckCommand() {
|
|
|
34
37
|
const projectName = pkgJson.name || path.basename(process.cwd());
|
|
35
38
|
const deps = Object.entries(pkgJson.dependencies || {}) as [string, string][];
|
|
36
39
|
const devDeps = Object.entries(pkgJson.devDependencies || {}) as [string, string][];
|
|
40
|
+
|
|
41
|
+
let graphResult: DepGraphResult | null = null;
|
|
42
|
+
try {
|
|
43
|
+
graphResult = await resolveAndScanDepGraph(process.cwd(), (msg) => setGraphStatus(msg));
|
|
44
|
+
setGraph(graphResult);
|
|
45
|
+
} catch {
|
|
46
|
+
setGraphStatus('Lockfile not found — scanning direct deps only');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setPhase('scanning');
|
|
37
50
|
const allEntries = [
|
|
38
51
|
...deps.map(([n, v]) => ({ n, v: clean(v) })),
|
|
39
52
|
...devDeps.map(([n, v]) => ({ n, v: clean(v) })),
|
|
@@ -120,10 +133,8 @@ export function CheckCommand() {
|
|
|
120
133
|
};
|
|
121
134
|
setReport(checkReport);
|
|
122
135
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
setUploadError('FILEVERSE_API_KEY not set');
|
|
126
|
-
} else {
|
|
136
|
+
if (process.env.FILEVERSE_API_KEY) {
|
|
137
|
+
setPhase('upload');
|
|
127
138
|
try {
|
|
128
139
|
const uploadResult = await uploadCheckReportToFileverse(checkReport);
|
|
129
140
|
setReportLink(uploadResult.link);
|
|
@@ -154,22 +165,46 @@ export function CheckCommand() {
|
|
|
154
165
|
<Header subtitle="check" />
|
|
155
166
|
<Text> </Text>
|
|
156
167
|
|
|
157
|
-
<StatusLine label=
|
|
158
|
-
status={phase === '
|
|
159
|
-
detail={phase === '
|
|
168
|
+
<StatusLine label="Resolving dependency tree"
|
|
169
|
+
status={phase === 'graph' ? 'running' : 'done'}
|
|
170
|
+
detail={phase === 'graph' ? graphStatus : graph
|
|
171
|
+
? `${graph.totalDeps} deps (${graph.directDeps} direct, ${graph.transitiveDeps} transitive)`
|
|
172
|
+
: 'direct deps only'} />
|
|
173
|
+
|
|
174
|
+
{phase !== 'graph' && (
|
|
175
|
+
<StatusLine label={`Scanning ${report?.totalDeps || '...'} direct dependencies`}
|
|
176
|
+
status={phase === 'scanning' ? 'running' : 'done'}
|
|
177
|
+
detail={phase === 'scanning' ? 'typosquats + CVEs + on-chain' : `${report?.totalDeps} checked`} />
|
|
178
|
+
)}
|
|
160
179
|
|
|
161
|
-
{phase !== 'scanning' && (
|
|
180
|
+
{phase !== 'graph' && phase !== 'scanning' && (
|
|
162
181
|
<StatusLine label="AI agents analyzing dependency tree"
|
|
163
182
|
status={phase === 'agents' ? 'running' : 'done'}
|
|
164
183
|
detail={report?.agents.length ? `${report.agents.length} agents` : undefined} />
|
|
165
184
|
)}
|
|
166
185
|
|
|
167
|
-
{(phase === 'upload' || phase === 'done') && (
|
|
186
|
+
{process.env.FILEVERSE_API_KEY && (phase === 'upload' || phase === 'done') && (
|
|
168
187
|
<StatusLine label="Upload report to Fileverse"
|
|
169
188
|
status={phase === 'upload' ? 'running' : reportLink ? 'done' : 'skip'}
|
|
170
189
|
detail={uploadError || undefined} />
|
|
171
190
|
)}
|
|
172
191
|
|
|
192
|
+
{/* Dep graph flagged paths */}
|
|
193
|
+
{phase === 'done' && graph && graph.flaggedPaths.length > 0 && (
|
|
194
|
+
<Box flexDirection="column" marginTop={1}>
|
|
195
|
+
<Text color="gray">────────────────────────────────────────</Text>
|
|
196
|
+
<Text color="white" bold> Flagged Dependency Paths ({graph.flaggedPaths.length})</Text>
|
|
197
|
+
{graph.flaggedPaths.slice(0, 15).map((fp, i) => (
|
|
198
|
+
<FlaggedPathRow key={i} fp={fp} />
|
|
199
|
+
))}
|
|
200
|
+
{graph.flaggedPaths.length > 15 && (
|
|
201
|
+
<Box marginLeft={2} marginTop={1}>
|
|
202
|
+
<Text color="gray">... and {graph.flaggedPaths.length - 15} more</Text>
|
|
203
|
+
</Box>
|
|
204
|
+
)}
|
|
205
|
+
</Box>
|
|
206
|
+
)}
|
|
207
|
+
|
|
173
208
|
{phase === 'done' && report && (
|
|
174
209
|
<Box flexDirection="column" marginTop={1}>
|
|
175
210
|
{typosquats.length > 0 && (
|
|
@@ -289,12 +324,25 @@ export function CheckCommand() {
|
|
|
289
324
|
<Text color="gray">────────────────────────────────────────</Text>
|
|
290
325
|
<Text color="white" bold> Summary</Text>
|
|
291
326
|
<Box marginLeft={2} flexDirection="column">
|
|
327
|
+
{graph && (
|
|
328
|
+
<Text color="white">
|
|
329
|
+
📦 {graph.totalDeps} deps ({graph.directDeps} direct, {graph.transitiveDeps} transitive)
|
|
330
|
+
</Text>
|
|
331
|
+
)}
|
|
292
332
|
<Text>{typosquats.length > 0 ? '🔴' : '🟢'} Typosquats: {typosquats.length}</Text>
|
|
293
333
|
<Text>{criticalCves.length > 0 ? '🔴' : cveWarnings.length > 0 ? '🟡' : '🟢'} CVEs: {criticalCves.length + cveWarnings.length} packages ({criticalCves.length} critical)</Text>
|
|
294
334
|
<Text>{highRisk.length > 0 ? '🔴' : '🟢'} On-chain risk: {highRisk.length} high-risk</Text>
|
|
295
335
|
{report.agents.length > 0 && (
|
|
296
336
|
<Text>{uniqueAgentFlags.length > 0 ? '🟡' : '🟢'} AI agents: {uniqueAgentFlags.length} flagged</Text>
|
|
297
337
|
)}
|
|
338
|
+
{graph && graph.flaggedPaths.length > 0 && (
|
|
339
|
+
<Text>
|
|
340
|
+
{graph.stats.critical > 0 ? '🔴' : '🟡'} Transitive risks: {graph.flaggedPaths.length} flagged paths
|
|
341
|
+
</Text>
|
|
342
|
+
)}
|
|
343
|
+
{graph && (
|
|
344
|
+
<Text color="gray">🔗 Merkle root: {graph.merkleRoot.slice(0, 18)}...</Text>
|
|
345
|
+
)}
|
|
298
346
|
</Box>
|
|
299
347
|
{reportLink && (
|
|
300
348
|
<Box marginLeft={2} marginTop={1}>
|
|
@@ -318,6 +366,26 @@ export function CheckCommand() {
|
|
|
318
366
|
);
|
|
319
367
|
}
|
|
320
368
|
|
|
369
|
+
function FlaggedPathRow({ fp }: { fp: FlaggedPath }) {
|
|
370
|
+
const sevColor = fp.severity === 'CRITICAL' ? 'red' : fp.severity === 'HIGH' ? 'red' : 'yellow';
|
|
371
|
+
const sevIcon = fp.severity === 'CRITICAL' || fp.severity === 'HIGH' ? '✖' : '⚠';
|
|
372
|
+
const chain = fp.path.slice(1).join(' → ');
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
376
|
+
<Box>
|
|
377
|
+
<Text color={sevColor}>{sevIcon} </Text>
|
|
378
|
+
<Text color={sevColor} bold>{fp.severity.padEnd(9)}</Text>
|
|
379
|
+
<Text color="white">{chain}</Text>
|
|
380
|
+
</Box>
|
|
381
|
+
<Box marginLeft={12}>
|
|
382
|
+
<Text color="gray">{fp.reason}</Text>
|
|
383
|
+
{fp.fix && <Text color="green"> → fix: {fp.fix}</Text>}
|
|
384
|
+
</Box>
|
|
385
|
+
</Box>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
321
389
|
function clean(v: string): string { return String(v).replace(/^[\^~]/, ''); }
|
|
322
390
|
|
|
323
391
|
function compareSemver(a: string, b: string): number {
|
|
@@ -10,7 +10,8 @@ import { computeChecksum, signChecksumAsync } from '../services/signature';
|
|
|
10
10
|
import { resolveENSName } from '../services/ens';
|
|
11
11
|
import { registerPackageOnChain } from '../services/contract';
|
|
12
12
|
import { writeENSRecords, buildOPMRecords, readOPMRecords, createPackageSubname, setENSContenthash, parseFileverseLink, readFileverseContentHash } from '../services/ens-records';
|
|
13
|
-
import { enqueueScan } from '@opm/scanner';
|
|
13
|
+
import { enqueueScan, submitScoreOnChain, setReportURIOnChain } from '@opm/scanner';
|
|
14
|
+
import { resolveAndScanDepGraph } from '../services/dep-graph';
|
|
14
15
|
import * as fs from 'fs';
|
|
15
16
|
import * as path from 'path';
|
|
16
17
|
import { execSync } from 'child_process';
|
|
@@ -46,6 +47,7 @@ interface PushResult {
|
|
|
46
47
|
ensRecordsCount?: number;
|
|
47
48
|
ensSubname?: string;
|
|
48
49
|
ipfsContenthash?: string;
|
|
50
|
+
merkleRoot?: string;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
interface PushCommandProps {
|
|
@@ -105,10 +107,11 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
|
|
|
105
107
|
let finalReportURI: string | undefined;
|
|
106
108
|
let finalRiskScore: number | undefined;
|
|
107
109
|
let finalIpfsHash: string | undefined;
|
|
110
|
+
let scanAgents: AgentEntry[] = [];
|
|
108
111
|
try {
|
|
109
112
|
const scanResult = await enqueueScan(name, version, (msg) =>
|
|
110
113
|
setScanLogs((prev) => [...prev.slice(-8), msg]),
|
|
111
|
-
{ tarballPath: tarballFile, pkgJsonPath },
|
|
114
|
+
{ local: { tarballPath: tarballFile, pkgJsonPath }, skipOnChainScore: true },
|
|
112
115
|
);
|
|
113
116
|
|
|
114
117
|
const riskScore = scanResult.report.aggregate_risk_score;
|
|
@@ -116,6 +119,7 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
|
|
|
116
119
|
finalReportURI = scanResult.reportURI;
|
|
117
120
|
finalRiskScore = riskScore;
|
|
118
121
|
finalIpfsHash = scanResult.ipfsHash;
|
|
122
|
+
scanAgents = scanResult.report.agents || [];
|
|
119
123
|
|
|
120
124
|
setResult((r) => ({
|
|
121
125
|
...r,
|
|
@@ -216,11 +220,43 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
|
|
|
216
220
|
const sigBytes = new Uint8Array(Buffer.from(signature.slice(2), 'hex'));
|
|
217
221
|
const txHash = await registerPackageOnChain(name, version, checksum, sigBytes, ensName);
|
|
218
222
|
setResult((r) => ({ ...r, txHash }));
|
|
223
|
+
|
|
224
|
+
// Submit individual agent scores now that the version exists on-chain
|
|
225
|
+
for (const agent of scanAgents) {
|
|
226
|
+
try {
|
|
227
|
+
setScanLogs((prev) => [...prev.slice(-8), `[${agent.agent_id}] Submitting score (${agent.result.risk_score}) to contract...`]);
|
|
228
|
+
await submitScoreOnChain(name, version, agent.result.risk_score, agent.result.reasoning);
|
|
229
|
+
setScanLogs((prev) => [...prev.slice(-8), `[${agent.agent_id}] Score submitted on-chain ✓`]);
|
|
230
|
+
} catch (err: any) {
|
|
231
|
+
setScanLogs((prev) => [...prev.slice(-8), `[${agent.agent_id}] Score submission: ${err?.shortMessage || err?.message || 'failed'}`]);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Set report URI on-chain
|
|
236
|
+
if (finalReportURI) {
|
|
237
|
+
try {
|
|
238
|
+
await setReportURIOnChain(name, version, finalReportURI);
|
|
239
|
+
setScanLogs((prev) => [...prev.slice(-8), 'Report URI stored on-chain ✓']);
|
|
240
|
+
} catch (err: any) {
|
|
241
|
+
setScanLogs((prev) => [...prev.slice(-8), `Report URI: ${err?.shortMessage || err?.message || 'failed'}`]);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
219
244
|
} catch (err: any) {
|
|
220
245
|
setScanLogs((prev) => [...prev, `Registration: ${err?.shortMessage || err?.message || 'failed'}`]);
|
|
221
246
|
}
|
|
222
247
|
updateStep('register', 'done');
|
|
223
248
|
|
|
249
|
+
// ── Compute dependency graph Merkle root ──
|
|
250
|
+
let merkleRoot: string | undefined;
|
|
251
|
+
try {
|
|
252
|
+
const graphResult = await resolveAndScanDepGraph(process.cwd());
|
|
253
|
+
merkleRoot = graphResult.merkleRoot;
|
|
254
|
+
setResult((r) => ({ ...r, merkleRoot }));
|
|
255
|
+
setScanLogs((prev) => [...prev.slice(-8), `Dep tree Merkle root: ${merkleRoot!.slice(0, 18)}...`]);
|
|
256
|
+
} catch {
|
|
257
|
+
setScanLogs((prev) => [...prev.slice(-8), 'Dep tree: no lockfile — skipped Merkle root']);
|
|
258
|
+
}
|
|
259
|
+
|
|
224
260
|
// ── Write package metadata to ENS text records ──
|
|
225
261
|
if (ensName) {
|
|
226
262
|
updateStep('ensRecords', 'running');
|
|
@@ -236,6 +272,7 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
|
|
|
236
272
|
reportURI: finalReportURI,
|
|
237
273
|
riskScore: finalRiskScore,
|
|
238
274
|
existingPackages: existingRecords.packages,
|
|
275
|
+
merkleRoot,
|
|
239
276
|
});
|
|
240
277
|
|
|
241
278
|
const writeResult = await writeENSRecords(
|
|
@@ -420,7 +457,7 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
|
|
|
420
457
|
</Box>
|
|
421
458
|
<Box>
|
|
422
459
|
<Text color="gray">Records: </Text>
|
|
423
|
-
<Text color="white">url, opm.version, opm.checksum, opm.fileverse, opm.risk_score{result.ipfsContenthash ? ', contenthash' : ''}</Text>
|
|
460
|
+
<Text color="white">url, opm.version, opm.checksum, opm.fileverse, opm.risk_score{result.merkleRoot ? ', opm.deptree' : ''}{result.ipfsContenthash ? ', contenthash' : ''}</Text>
|
|
424
461
|
</Box>
|
|
425
462
|
{result.ensSubname && (
|
|
426
463
|
<Box>
|
|
@@ -16,6 +16,19 @@ const args = process.argv.slice(2);
|
|
|
16
16
|
const command = args[0];
|
|
17
17
|
const rest = args.slice(1);
|
|
18
18
|
|
|
19
|
+
if (command === '--version' || command === '-v' || command === '-V' || command === 'version') {
|
|
20
|
+
try {
|
|
21
|
+
const fs = await import('fs');
|
|
22
|
+
const path = await import('path');
|
|
23
|
+
const pkgPath = path.resolve(__dirname, '../../../package.json');
|
|
24
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
25
|
+
console.log(`opm v${pkg.version}`);
|
|
26
|
+
} catch {
|
|
27
|
+
console.log('opm (version unknown)');
|
|
28
|
+
}
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
function parsePackageArg(pkg?: string) {
|
|
20
33
|
if (!pkg) return {};
|
|
21
34
|
const atIdx = pkg.lastIndexOf('@');
|
|
@@ -138,6 +151,9 @@ function Help() {
|
|
|
138
151
|
<Text color="gray"> view name.eth → author profile + OPM ENS records</Text>
|
|
139
152
|
<Text color="gray"> pkg@name.eth → ENS-resolved safest version by author</Text>
|
|
140
153
|
<Text> </Text>
|
|
154
|
+
<Text color="cyan" bold>Other:</Text>
|
|
155
|
+
<Text> opm --version / -v Show OPM version</Text>
|
|
156
|
+
<Text> </Text>
|
|
141
157
|
<Text color="cyan" bold>Environment (install/audit/info/view need no config):</Text>
|
|
142
158
|
<Text> OPM_SIGNING_KEY Author signing key (for push only)</Text>
|
|
143
159
|
<Text> NPM_TOKEN npm automation token (for push only)</Text>
|