opmsec 0.1.0 → 0.1.3
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/README.md +256 -173
- 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 +3 -3
- 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 +229 -33
- 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 +41 -5
- 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/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 +7 -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 +46 -0
- package/packages/scanner/src/services/report-formatter.ts +122 -3
- package/packages/scanner/src/services/zk-verifier.ts +118 -0
- package/packages/web/.next/app-build-manifest.json +15 -0
- package/packages/web/.next/build-manifest.json +20 -0
- package/packages/web/.next/package.json +1 -0
- package/packages/web/.next/prerender-manifest.json +11 -0
- package/packages/web/.next/react-loadable-manifest.json +1 -0
- package/packages/web/.next/routes-manifest.json +1 -0
- package/packages/web/.next/server/app/page.js +272 -0
- package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -0
- package/packages/web/.next/server/app-paths-manifest.json +3 -0
- package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/packages/web/.next/server/middleware-build-manifest.js +22 -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-manifest.json +1 -0
- package/packages/web/.next/server/server-reference-manifest.js +1 -0
- package/packages/web/.next/server/server-reference-manifest.json +5 -0
- package/packages/web/.next/server/vendor-chunks/@swc.js +55 -0
- package/packages/web/.next/server/vendor-chunks/next.js +3010 -0
- package/packages/web/.next/server/webpack-runtime.js +209 -0
- package/packages/web/.next/static/chunks/app/layout.js +39 -0
- package/packages/web/.next/static/chunks/app/page.js +61 -0
- package/packages/web/.next/static/chunks/app-pages-internals.js +182 -0
- package/packages/web/.next/static/chunks/main-app.js +1882 -0
- package/packages/web/.next/static/chunks/polyfills.js +1 -0
- package/packages/web/.next/static/chunks/webpack.js +1393 -0
- package/packages/web/.next/static/css/app/layout.css +1237 -0
- package/packages/web/.next/static/development/_buildManifest.js +1 -0
- package/packages/web/.next/static/development/_ssgManifest.js +1 -0
- package/packages/web/.next/static/webpack/633457081244afec._.hot-update.json +1 -0
- package/packages/web/.next/static/webpack/6fee6306e0f98869.webpack.hot-update.json +1 -0
- package/packages/web/.next/static/webpack/73e341375c8d429e.webpack.hot-update.json +1 -0
- package/packages/web/.next/static/webpack/app/layout.6fee6306e0f98869.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/layout.73e341375c8d429e.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/page.6fee6306e0f98869.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/app/page.73e341375c8d429e.hot-update.js +22 -0
- package/packages/web/.next/static/webpack/webpack.6fee6306e0f98869.hot-update.js +12 -0
- package/packages/web/.next/static/webpack/webpack.73e341375c8d429e.hot-update.js +12 -0
- package/packages/web/.next/trace +5 -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 +358 -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
|
@@ -42,6 +42,10 @@ export interface AgentScanResult {
|
|
|
42
42
|
export interface AgentEntry {
|
|
43
43
|
agent_id: string;
|
|
44
44
|
model: string;
|
|
45
|
+
model_intelligence?: number;
|
|
46
|
+
model_coding?: number;
|
|
47
|
+
model_weight?: number;
|
|
48
|
+
score_tx_hash?: string;
|
|
45
49
|
result: AgentScanResult;
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -102,3 +106,40 @@ export interface ChainPatrolResult {
|
|
|
102
106
|
status: 'UNKNOWN' | 'ALLOWED' | 'BLOCKED';
|
|
103
107
|
source: string;
|
|
104
108
|
}
|
|
109
|
+
|
|
110
|
+
export interface CheckDepResult {
|
|
111
|
+
name: string;
|
|
112
|
+
version: string;
|
|
113
|
+
typosquat: { likelyTarget: string; confidence: string; reason: string } | null;
|
|
114
|
+
cveCount: number;
|
|
115
|
+
cveCritical: number;
|
|
116
|
+
cveHigh: number;
|
|
117
|
+
cveIds: string[];
|
|
118
|
+
fixVersion: string | null;
|
|
119
|
+
onChainScore: number | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface CheckAgentResult {
|
|
123
|
+
agentId: string;
|
|
124
|
+
model: string;
|
|
125
|
+
intelligence: number;
|
|
126
|
+
coding: number;
|
|
127
|
+
findings: Array<{
|
|
128
|
+
package: string;
|
|
129
|
+
issue: string;
|
|
130
|
+
severity: string;
|
|
131
|
+
explanation: string;
|
|
132
|
+
suggested_replacement: string | null;
|
|
133
|
+
suggested_version: string | null;
|
|
134
|
+
}>;
|
|
135
|
+
overall: string;
|
|
136
|
+
riskScore: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface CheckReport {
|
|
140
|
+
project: string;
|
|
141
|
+
timestamp: string;
|
|
142
|
+
totalDeps: number;
|
|
143
|
+
deps: CheckDepResult[];
|
|
144
|
+
agents: CheckAgentResult[];
|
|
145
|
+
}
|
|
@@ -17,10 +17,14 @@ export function truncateAddress(addr: string): string {
|
|
|
17
17
|
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function getEnvOrThrow(key: string): string {
|
|
20
|
+
export function getEnvOrThrow(key: string, ...fallbackKeys: string[]): string {
|
|
21
21
|
const val = process.env[key];
|
|
22
|
-
if (
|
|
23
|
-
|
|
22
|
+
if (val) return val;
|
|
23
|
+
for (const fk of fallbackKeys) {
|
|
24
|
+
const fv = process.env[fk];
|
|
25
|
+
if (fv) return fv;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Missing required env var: ${key}`);
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export function getEnvOrDefault(key: string, fallback: string): string {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SYSTEM_PROMPT, buildUserPrompt } from '@opm/core';
|
|
1
|
+
import { SYSTEM_PROMPT, buildUserPrompt, getModelRankingFor } from '@opm/core';
|
|
2
2
|
import type { AgentEntry, KnownCVE } from '@opm/core';
|
|
3
3
|
import {
|
|
4
4
|
fetchPackageData, buildLocalPackageData, extractMetadata,
|
|
@@ -59,10 +59,16 @@ export async function runAgent(
|
|
|
59
59
|
const userPrompt = buildUserPrompt(meta, history, sourceFiles, knownCVEs);
|
|
60
60
|
const result = await callLLM(config.model, SYSTEM_PROMPT, userPrompt);
|
|
61
61
|
|
|
62
|
+
log(`[${config.agentId}] Fetching model intelligence ranking...`);
|
|
63
|
+
const { intelligence, coding, weight } = await getModelRankingFor(config.model);
|
|
64
|
+
|
|
65
|
+
log(`[${config.agentId}] ${config.model} — intelligence: ${intelligence}, coding: ${coding}, weight: ${weight}`);
|
|
66
|
+
|
|
62
67
|
log(`[${config.agentId}] Submitting score (${result.risk_score}) to contract...`);
|
|
68
|
+
let scoreTxHash: string | undefined;
|
|
63
69
|
try {
|
|
64
|
-
await submitScoreOnChain(packageName, version, result.risk_score, result.reasoning);
|
|
65
|
-
log(`[${config.agentId}] Score submitted on-chain
|
|
70
|
+
scoreTxHash = await submitScoreOnChain(packageName, version, result.risk_score, result.reasoning);
|
|
71
|
+
log(`[${config.agentId}] Score submitted on-chain ✓`);
|
|
66
72
|
} catch (err: any) {
|
|
67
73
|
log(`[${config.agentId}] On-chain: ${err?.shortMessage || err?.message || 'failed'}`);
|
|
68
74
|
}
|
|
@@ -70,6 +76,10 @@ export async function runAgent(
|
|
|
70
76
|
return {
|
|
71
77
|
agent_id: config.agentId,
|
|
72
78
|
model: config.model,
|
|
79
|
+
model_intelligence: intelligence,
|
|
80
|
+
model_coding: coding,
|
|
81
|
+
model_weight: weight,
|
|
82
|
+
score_tx_hash: scoreTxHash,
|
|
73
83
|
result,
|
|
74
84
|
};
|
|
75
85
|
}
|
|
@@ -4,10 +4,13 @@ export { enqueueScan } from './queue/memory-queue';
|
|
|
4
4
|
export type { LocalScanContext } from './agents/base-agent';
|
|
5
5
|
export { runAgent } from './agents/base-agent';
|
|
6
6
|
export { getAgentConfigs } from './agents/agent-configs';
|
|
7
|
-
export { callLLM, getLLMProvider } from './services/openrouter';
|
|
7
|
+
export { callLLM, callLLMRaw, getLLMProvider } from './services/openrouter';
|
|
8
8
|
export { fetchPackageData, extractMetadata, buildVersionHistory, fetchSourceFiles, extractLocalSourceFiles, buildLocalPackageData } from './services/npm-registry';
|
|
9
9
|
export { submitScoreOnChain, setReportURIOnChain } from './services/contract-writer';
|
|
10
|
-
export { uploadReportToFileverse, fetchReportFromFileverse } from './services/fileverse';
|
|
10
|
+
export { uploadReportToFileverse, uploadCheckReportToFileverse, fetchReportFromFileverse } from './services/fileverse';
|
|
11
|
+
export { formatCheckReportAsMarkdown } from './services/report-formatter';
|
|
12
|
+
export { runBenchmarkSuite, type AgentCandidate, type BenchmarkRunResult } from './services/benchmark-runner';
|
|
13
|
+
export { generateProof, verifyProof, generateCommitment, proofToOnChainBytes, type ZKProof } from './services/zk-verifier';
|
|
11
14
|
|
|
12
15
|
if (import.meta.main) {
|
|
13
16
|
const [pkg, ver] = process.argv.slice(2);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ScanReport, AgentEntry } from '@opm/core';
|
|
2
|
-
import {
|
|
2
|
+
import { classifyRisk, getModelWeight, calculateWeightedScore } from '@opm/core';
|
|
3
3
|
import { runAgent, type LocalScanContext } from '../agents/base-agent';
|
|
4
4
|
import { getAgentConfigs } from '../agents/agent-configs';
|
|
5
5
|
import { setReportURIOnChain } from '../services/contract-writer';
|
|
@@ -57,8 +57,13 @@ async function executeScan(
|
|
|
57
57
|
|
|
58
58
|
if (agents.length === 0) throw new Error('All agents failed');
|
|
59
59
|
|
|
60
|
-
const
|
|
61
|
-
const
|
|
60
|
+
const weights = await Promise.all(agents.map(a => getModelWeight(a.model)));
|
|
61
|
+
const weightedScores = agents.map((a, i) => ({
|
|
62
|
+
score: a.result.risk_score,
|
|
63
|
+
weight: weights[i],
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
const aggScore = calculateWeightedScore(weightedScores);
|
|
62
67
|
|
|
63
68
|
const report: ScanReport = {
|
|
64
69
|
package: packageName,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateBenchmarkDataset,
|
|
3
|
+
buildBenchmarkPrompt,
|
|
4
|
+
evaluateBenchmark,
|
|
5
|
+
SYSTEM_PROMPT,
|
|
6
|
+
type BenchmarkCase,
|
|
7
|
+
type BenchmarkResult,
|
|
8
|
+
} from '@opm/core';
|
|
9
|
+
import { callLLM } from './openrouter';
|
|
10
|
+
import {
|
|
11
|
+
generateCommitment,
|
|
12
|
+
generateProof,
|
|
13
|
+
verifyProof,
|
|
14
|
+
type ZKProof,
|
|
15
|
+
} from './zk-verifier';
|
|
16
|
+
|
|
17
|
+
export interface AgentCandidate {
|
|
18
|
+
name: string;
|
|
19
|
+
model: string;
|
|
20
|
+
systemPrompt?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BenchmarkRunResult {
|
|
24
|
+
candidate: AgentCandidate;
|
|
25
|
+
results: BenchmarkResult[];
|
|
26
|
+
passed: number;
|
|
27
|
+
failed: number;
|
|
28
|
+
total: number;
|
|
29
|
+
accuracyPct: number;
|
|
30
|
+
zkProof: ZKProof;
|
|
31
|
+
verified: boolean;
|
|
32
|
+
failureReasons: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runBenchmarkSuite(
|
|
36
|
+
candidate: AgentCandidate,
|
|
37
|
+
onStatus?: (msg: string) => void,
|
|
38
|
+
): Promise<BenchmarkRunResult> {
|
|
39
|
+
const log = onStatus || console.log;
|
|
40
|
+
const benchmarks = generateBenchmarkDataset();
|
|
41
|
+
const systemPrompt = candidate.systemPrompt || SYSTEM_PROMPT;
|
|
42
|
+
|
|
43
|
+
log(`Generating benchmark commitment for ${benchmarks.length} test cases...`);
|
|
44
|
+
|
|
45
|
+
const expectedVerdicts = benchmarks.map((b) => {
|
|
46
|
+
const levelMap: Record<string, number> = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
|
|
47
|
+
return levelMap[b.expected.risk_level] ?? 0;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const commitment = generateCommitment(expectedVerdicts);
|
|
51
|
+
log(`Commitment generated: ${commitment.expectedHash.slice(0, 16)}...`);
|
|
52
|
+
|
|
53
|
+
const results: BenchmarkResult[] = [];
|
|
54
|
+
const actualVerdicts: number[] = [];
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < benchmarks.length; i++) {
|
|
57
|
+
const bench = benchmarks[i];
|
|
58
|
+
log(`[${i + 1}/${benchmarks.length}] ${bench.description}...`);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const userPrompt = buildBenchmarkPrompt(bench);
|
|
62
|
+
const agentResult = await callLLM(candidate.model, systemPrompt, userPrompt);
|
|
63
|
+
|
|
64
|
+
const evaluation = evaluateBenchmark(bench, agentResult.risk_level, agentResult.risk_score);
|
|
65
|
+
results.push(evaluation);
|
|
66
|
+
|
|
67
|
+
const levelMap: Record<string, number> = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
|
|
68
|
+
actualVerdicts.push(evaluation.verdict === 'PASS'
|
|
69
|
+
? (levelMap[bench.expected.risk_level] ?? 0)
|
|
70
|
+
: (levelMap[agentResult.risk_level] ?? 0));
|
|
71
|
+
|
|
72
|
+
const icon = evaluation.verdict === 'PASS' ? '✓' : '✗';
|
|
73
|
+
log(`[${i + 1}/${benchmarks.length}] ${icon} ${bench.category}: score=${agentResult.risk_score} level=${agentResult.risk_level} (expected ${bench.expected.risk_level})`);
|
|
74
|
+
} catch (err: any) {
|
|
75
|
+
log(`[${i + 1}/${benchmarks.length}] ✗ Error: ${err?.message || 'failed'}`);
|
|
76
|
+
results.push({
|
|
77
|
+
caseId: bench.id,
|
|
78
|
+
category: bench.category,
|
|
79
|
+
expectedLevel: bench.expected.risk_level,
|
|
80
|
+
actualLevel: 'ERROR',
|
|
81
|
+
expectedScoreRange: [bench.expected.min_risk_score, bench.expected.max_risk_score],
|
|
82
|
+
actualScore: -1,
|
|
83
|
+
verdict: 'FAIL',
|
|
84
|
+
reason: `Agent error: ${err?.message || 'unknown'}`,
|
|
85
|
+
});
|
|
86
|
+
actualVerdicts.push(-1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
log('Generating ZK proof of accuracy...');
|
|
91
|
+
const zkProof = generateProof(commitment, expectedVerdicts, actualVerdicts);
|
|
92
|
+
const verified = verifyProof(zkProof);
|
|
93
|
+
|
|
94
|
+
const passed = results.filter((r) => r.verdict === 'PASS').length;
|
|
95
|
+
const failed = results.filter((r) => r.verdict === 'FAIL').length;
|
|
96
|
+
const failureReasons = results
|
|
97
|
+
.filter((r) => r.verdict === 'FAIL')
|
|
98
|
+
.map((r) => `${r.caseId} (${r.category}): ${r.reason}`);
|
|
99
|
+
|
|
100
|
+
log(`ZK proof ${verified ? 'verified ✓' : 'INVALID ✗'}`);
|
|
101
|
+
log(`Accuracy: ${passed}/${results.length} (${Math.round((passed / results.length) * 100)}%)`);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
candidate,
|
|
105
|
+
results,
|
|
106
|
+
passed,
|
|
107
|
+
failed,
|
|
108
|
+
total: results.length,
|
|
109
|
+
accuracyPct: Math.round((passed / results.length) * 100),
|
|
110
|
+
zkProof,
|
|
111
|
+
verified,
|
|
112
|
+
failureReasons,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { ethers } from 'ethers';
|
|
2
|
-
import { OPM_REGISTRY_ABI, getEnvOrThrow, getEnvOrDefault, BASE_SEPOLIA_RPC } from '@opm/core';
|
|
2
|
+
import { OPM_REGISTRY_ABI, getEnvOrThrow, getEnvOrDefault, BASE_SEPOLIA_RPC, DEFAULT_CONTRACT_ADDRESS } from '@opm/core';
|
|
3
3
|
|
|
4
4
|
function getContract() {
|
|
5
5
|
const rpc = getEnvOrDefault('BASE_SEPOLIA_RPC_URL', BASE_SEPOLIA_RPC);
|
|
6
6
|
const provider = new ethers.JsonRpcProvider(rpc);
|
|
7
7
|
const wallet = new ethers.Wallet(getEnvOrThrow('AGENT_PRIVATE_KEY'), provider);
|
|
8
|
-
|
|
9
|
-
return new ethers.Contract(address, OPM_REGISTRY_ABI, wallet);
|
|
8
|
+
return new ethers.Contract(getEnvOrDefault('CONTRACT_ADDRESS', DEFAULT_CONTRACT_ADDRESS), OPM_REGISTRY_ABI, wallet);
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
export async function submitScoreOnChain(
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import type { ScanReport } from '@opm/core';
|
|
2
|
-
import { getEnvOrDefault } from '@opm/core';
|
|
3
|
-
import { formatReportAsMarkdown } from './report-formatter';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_API_URL = 'http://localhost:8001';
|
|
1
|
+
import type { ScanReport, CheckReport } from '@opm/core';
|
|
2
|
+
import { getEnvOrDefault, FILEVERSE_DEFAULT_URL } from '@opm/core';
|
|
3
|
+
import { formatReportAsMarkdown, formatCheckReportAsMarkdown } from './report-formatter';
|
|
6
4
|
const POLL_INTERVAL_MS = 3000;
|
|
7
5
|
const POLL_TIMEOUT_MS = 60_000;
|
|
8
6
|
|
|
9
7
|
function getApiConfig() {
|
|
10
|
-
const apiUrl = getEnvOrDefault('FILEVERSE_API_URL',
|
|
8
|
+
const apiUrl = getEnvOrDefault('FILEVERSE_API_URL', FILEVERSE_DEFAULT_URL);
|
|
11
9
|
const apiKey = process.env.FILEVERSE_API_KEY;
|
|
12
10
|
if (!apiKey) throw new Error('FILEVERSE_API_KEY is required (generate at ddocs.new → Settings → Developer Mode)');
|
|
13
11
|
return { apiUrl, apiKey };
|
|
@@ -56,11 +54,32 @@ async function pollForSync(apiUrl: string, apiKey: string, ddocId: string): Prom
|
|
|
56
54
|
return `https://ddocs.new/pending/${ddocId}`;
|
|
57
55
|
}
|
|
58
56
|
|
|
57
|
+
export async function uploadCheckReportToFileverse(report: CheckReport): Promise<string> {
|
|
58
|
+
const { apiUrl, apiKey } = getApiConfig();
|
|
59
|
+
const title = `OPM Check Report: ${report.project} (${report.totalDeps} deps)`;
|
|
60
|
+
const content = formatCheckReportAsMarkdown(report);
|
|
61
|
+
|
|
62
|
+
const res = await fetch(`${apiUrl}/api/ddocs?apiKey=${encodeURIComponent(apiKey)}`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify({ title, content }),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const body = await res.text();
|
|
70
|
+
throw new Error(`Fileverse create failed (${res.status}): ${body}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { data } = await res.json() as { data: { ddocId: string; syncStatus: string; link?: string } };
|
|
74
|
+
if (data.syncStatus === 'synced' && data.link) return data.link;
|
|
75
|
+
return pollForSync(apiUrl, apiKey, data.ddocId);
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
export async function fetchReportFromFileverse(reportURI: string): Promise<ScanReport | null> {
|
|
60
79
|
if (!reportURI || reportURI.startsWith('local://')) return null;
|
|
61
80
|
|
|
62
81
|
const apiKey = process.env.FILEVERSE_API_KEY;
|
|
63
|
-
const apiUrl = getEnvOrDefault('FILEVERSE_API_URL',
|
|
82
|
+
const apiUrl = getEnvOrDefault('FILEVERSE_API_URL', FILEVERSE_DEFAULT_URL);
|
|
64
83
|
|
|
65
84
|
const ddocId = extractDdocId(reportURI);
|
|
66
85
|
if (ddocId && apiKey) {
|
|
@@ -84,3 +84,49 @@ export async function callLLM(
|
|
|
84
84
|
|
|
85
85
|
return parsed;
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
export async function callLLMRaw<T = unknown>(
|
|
89
|
+
model: string,
|
|
90
|
+
systemPrompt: string,
|
|
91
|
+
userPrompt: string,
|
|
92
|
+
): Promise<T> {
|
|
93
|
+
const { apiUrl, apiKey, kind } = getProvider();
|
|
94
|
+
|
|
95
|
+
const headers: Record<string, string> = {
|
|
96
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (kind === 'openrouter') {
|
|
101
|
+
headers['HTTP-Referer'] = 'https://opm.dev';
|
|
102
|
+
headers['X-Title'] = 'OPM Security Scanner';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const res = await fetch(apiUrl, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers,
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
model,
|
|
110
|
+
messages: [
|
|
111
|
+
{ role: 'system', content: systemPrompt },
|
|
112
|
+
{ role: 'user', content: userPrompt },
|
|
113
|
+
],
|
|
114
|
+
response_format: { type: 'json_object' },
|
|
115
|
+
temperature: 0.1,
|
|
116
|
+
max_tokens: 4096,
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
const body = await res.text();
|
|
122
|
+
throw new Error(`${kind} ${res.status}: ${body}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const data = await res.json() as { choices: Array<{ message: { content: string } }> };
|
|
126
|
+
const raw = data.choices?.[0]?.message?.content;
|
|
127
|
+
if (!raw) throw new Error(`Empty response from ${kind}/${model}`);
|
|
128
|
+
|
|
129
|
+
const parsed = safeJsonParse<T>(raw);
|
|
130
|
+
if (!parsed) throw new Error(`Invalid JSON from ${model}: ${raw.slice(0, 200)}`);
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ScanReport, AgentEntry, Vulnerability, SupplyChainIndicators } from '@opm/core';
|
|
1
|
+
import type { ScanReport, AgentEntry, Vulnerability, SupplyChainIndicators, CheckReport } from '@opm/core';
|
|
2
2
|
|
|
3
3
|
function riskEmoji(score: number): string {
|
|
4
4
|
if (score >= 70) return '🔴';
|
|
@@ -49,10 +49,15 @@ function formatAgent(agent: AgentEntry, index: number): string {
|
|
|
49
49
|
const { result } = agent;
|
|
50
50
|
const emoji = riskEmoji(result.risk_score);
|
|
51
51
|
|
|
52
|
+
const modelLines = [`- **Model:** \`${agent.model}\``];
|
|
53
|
+
if (agent.model_intelligence || agent.model_coding) {
|
|
54
|
+
modelLines.push(`- **AI Intelligence Index:** ${agent.model_intelligence || '—'}/100 | **Coding Index:** ${agent.model_coding || '—'}/100 | **Weight:** ${agent.model_weight || '—'}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
return [
|
|
53
58
|
`### Agent ${index + 1}: \`${agent.agent_id}\``,
|
|
54
59
|
'',
|
|
55
|
-
|
|
60
|
+
...modelLines,
|
|
56
61
|
`- **Risk Score:** ${emoji} **${result.risk_score}/100** (${result.risk_level})`,
|
|
57
62
|
`- **Recommendation:** ${result.recommendation}`,
|
|
58
63
|
'',
|
|
@@ -101,10 +106,12 @@ export function formatReportAsMarkdown(report: ScanReport): string {
|
|
|
101
106
|
'',
|
|
102
107
|
'---',
|
|
103
108
|
'',
|
|
104
|
-
'## Aggregate Risk',
|
|
109
|
+
'## Aggregate Risk (Intelligence-Weighted)',
|
|
105
110
|
'',
|
|
106
111
|
`\`${riskBar(report.aggregate_risk_score)}\` **${report.aggregate_risk_score}/100** — ${report.consensus}`,
|
|
107
112
|
'',
|
|
113
|
+
`> Scores are weighted by each model's AI Intelligence and Coding indices from [Artificial Analysis](https://artificialanalysis.ai).`,
|
|
114
|
+
'',
|
|
108
115
|
report.aggregate_risk_score < 40
|
|
109
116
|
? '> ✅ This package appears safe based on multi-agent consensus.'
|
|
110
117
|
: report.aggregate_risk_score < 70
|
|
@@ -132,3 +139,115 @@ export function formatReportAsMarkdown(report: ScanReport): string {
|
|
|
132
139
|
|
|
133
140
|
return sections.join('\n');
|
|
134
141
|
}
|
|
142
|
+
|
|
143
|
+
export function formatCheckReportAsMarkdown(report: CheckReport): string {
|
|
144
|
+
const timestamp = new Date(report.timestamp).toLocaleString('en-US', {
|
|
145
|
+
dateStyle: 'long', timeStyle: 'short',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const typosquats = report.deps.filter((d) => d.typosquat);
|
|
149
|
+
const cveBlocked = report.deps.filter((d) => d.cveCritical > 0);
|
|
150
|
+
const cveWarned = report.deps.filter((d) => d.cveCount > 0 && d.cveCritical === 0);
|
|
151
|
+
const highRisk = report.deps.filter((d) => d.onChainScore !== null && d.onChainScore >= 70);
|
|
152
|
+
const safeCount = report.totalDeps - typosquats.length - cveBlocked.length - cveWarned.length - highRisk.length;
|
|
153
|
+
|
|
154
|
+
const sections: string[] = [
|
|
155
|
+
`# OPM Dependency Check Report`,
|
|
156
|
+
'',
|
|
157
|
+
`- **Project:** ${report.project}`,
|
|
158
|
+
`- **Scanned:** ${timestamp}`,
|
|
159
|
+
`- **Total dependencies:** ${report.totalDeps}`,
|
|
160
|
+
'',
|
|
161
|
+
'---',
|
|
162
|
+
'',
|
|
163
|
+
'## Summary',
|
|
164
|
+
'',
|
|
165
|
+
`| Category | Count |`,
|
|
166
|
+
`|---|---|`,
|
|
167
|
+
`| ${typosquats.length > 0 ? '🔴' : '🟢'} Typosquats | ${typosquats.length} |`,
|
|
168
|
+
`| ${cveBlocked.length > 0 ? '🔴' : '🟢'} Critical CVEs | ${cveBlocked.length} |`,
|
|
169
|
+
`| ${cveWarned.length > 0 ? '🟡' : '🟢'} CVE Warnings | ${cveWarned.length} |`,
|
|
170
|
+
`| ${highRisk.length > 0 ? '🔴' : '🟢'} High On-chain Risk | ${highRisk.length} |`,
|
|
171
|
+
`| 🟢 Safe | ${Math.max(0, safeCount)} |`,
|
|
172
|
+
'',
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
if (typosquats.length > 0) {
|
|
176
|
+
sections.push('---', '', '## Typosquat Risks', '');
|
|
177
|
+
for (const d of typosquats) {
|
|
178
|
+
sections.push(
|
|
179
|
+
`### \`${d.name}\`@${d.version}`,
|
|
180
|
+
'',
|
|
181
|
+
`- **Likely intended package:** \`${d.typosquat!.likelyTarget}\``,
|
|
182
|
+
`- **Confidence:** ${d.typosquat!.confidence}`,
|
|
183
|
+
`- **Reason:** ${d.typosquat!.reason}`,
|
|
184
|
+
'',
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (cveBlocked.length > 0) {
|
|
190
|
+
sections.push('---', '', '## Critical Vulnerabilities', '');
|
|
191
|
+
for (const d of cveBlocked) {
|
|
192
|
+
sections.push(
|
|
193
|
+
`### \`${d.name}\`@${d.version}`,
|
|
194
|
+
'',
|
|
195
|
+
`- **Critical:** ${d.cveCritical} | **High:** ${d.cveHigh}`,
|
|
196
|
+
`- **CVEs:** ${d.cveIds.join(', ')}`,
|
|
197
|
+
d.fixVersion ? `- **Fix:** upgrade to \`${d.fixVersion}\`` : '',
|
|
198
|
+
'',
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (cveWarned.length > 0) {
|
|
204
|
+
sections.push('---', '', '## CVE Warnings', '');
|
|
205
|
+
for (const d of cveWarned) {
|
|
206
|
+
sections.push(
|
|
207
|
+
`- \`${d.name}\`@${d.version} — ${d.cveCount} CVE(s)${d.fixVersion ? ` → \`${d.fixVersion}\`` : ''}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
sections.push('');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (highRisk.length > 0) {
|
|
214
|
+
sections.push('---', '', '## High On-chain Risk', '');
|
|
215
|
+
for (const d of highRisk) {
|
|
216
|
+
sections.push(`- \`${d.name}\`@${d.version} — risk score **${d.onChainScore}/100**`);
|
|
217
|
+
}
|
|
218
|
+
sections.push('');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (report.agents.length > 0) {
|
|
222
|
+
sections.push('---', '', '## AI Agent Analysis', '');
|
|
223
|
+
for (const a of report.agents) {
|
|
224
|
+
const flags = a.findings.filter((f) => f.issue !== 'safe' && f.severity !== 'NONE');
|
|
225
|
+
sections.push(
|
|
226
|
+
`### \`${a.agentId}\` — ${a.model}`,
|
|
227
|
+
'',
|
|
228
|
+
`- **AI Intelligence:** ${a.intelligence}/100 | **Coding:** ${a.coding}/100`,
|
|
229
|
+
`- **Risk Score:** ${a.riskScore}/100`,
|
|
230
|
+
'',
|
|
231
|
+
);
|
|
232
|
+
if (flags.length > 0) {
|
|
233
|
+
for (const f of flags) {
|
|
234
|
+
sections.push(
|
|
235
|
+
`- **[${f.severity}]** \`${f.package}\` — ${f.issue}: ${f.explanation}` +
|
|
236
|
+
(f.suggested_replacement ? ` → \`${f.suggested_replacement}\`` : ''),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
sections.push(`- ✅ No issues found`);
|
|
241
|
+
}
|
|
242
|
+
sections.push('', `> ${a.overall}`, '');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
sections.push(
|
|
247
|
+
'---',
|
|
248
|
+
'',
|
|
249
|
+
`*Report generated by [OPM](https://github.com/dhananjaypai08/opm) — On-chain Package Manager*`,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return sections.filter((l) => l !== undefined).join('\n');
|
|
253
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zero-knowledge accuracy verification via hash commitments.
|
|
5
|
+
*
|
|
6
|
+
* The scheme works as follows:
|
|
7
|
+
* 1. A trusted authority generates benchmark test cases with expected outputs
|
|
8
|
+
* 2. Expected outputs are hashed with a secret salt → commitment
|
|
9
|
+
* 3. The candidate agent runs against the benchmarks
|
|
10
|
+
* 4. Actual outputs are hashed with the same salt
|
|
11
|
+
* 5. A proof is generated: hash(commitment || result_hashes || accuracy_flag)
|
|
12
|
+
* 6. The verifier checks the proof without seeing individual test results
|
|
13
|
+
*
|
|
14
|
+
* This ensures:
|
|
15
|
+
* - Test cases remain private (can't be gamed)
|
|
16
|
+
* - Individual results aren't disclosed
|
|
17
|
+
* - Only a binary pass/fail is revealed
|
|
18
|
+
* - The proof is deterministic and verifiable
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface ZKCommitment {
|
|
22
|
+
salt: string;
|
|
23
|
+
expectedHash: string;
|
|
24
|
+
caseCount: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ZKProof {
|
|
28
|
+
commitment: ZKCommitment;
|
|
29
|
+
resultHash: string;
|
|
30
|
+
accuracyProof: string;
|
|
31
|
+
passed: boolean;
|
|
32
|
+
timestamp: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AccuracyWitness {
|
|
36
|
+
expectedVerdicts: number[];
|
|
37
|
+
actualVerdicts: number[];
|
|
38
|
+
salt: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function poseidonHash(...inputs: string[]): string {
|
|
42
|
+
const h = createHash('sha256');
|
|
43
|
+
for (const input of inputs) {
|
|
44
|
+
h.update(input);
|
|
45
|
+
}
|
|
46
|
+
return h.digest('hex');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function generateCommitment(expectedVerdicts: number[]): ZKCommitment {
|
|
50
|
+
const salt = randomBytes(32).toString('hex');
|
|
51
|
+
const verdictStr = expectedVerdicts.join(',');
|
|
52
|
+
const expectedHash = poseidonHash(salt, verdictStr);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
salt,
|
|
56
|
+
expectedHash,
|
|
57
|
+
caseCount: expectedVerdicts.length,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function generateProof(
|
|
62
|
+
commitment: ZKCommitment,
|
|
63
|
+
expectedVerdicts: number[],
|
|
64
|
+
actualVerdicts: number[],
|
|
65
|
+
): ZKProof {
|
|
66
|
+
if (expectedVerdicts.length !== actualVerdicts.length) {
|
|
67
|
+
throw new Error('Verdict arrays must have equal length');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const expectedStr = expectedVerdicts.join(',');
|
|
71
|
+
const commitmentCheck = poseidonHash(commitment.salt, expectedStr);
|
|
72
|
+
if (commitmentCheck !== commitment.expectedHash) {
|
|
73
|
+
throw new Error('Commitment verification failed — expected verdicts do not match');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const actualStr = actualVerdicts.join(',');
|
|
77
|
+
const resultHash = poseidonHash(commitment.salt, actualStr);
|
|
78
|
+
|
|
79
|
+
const allMatch = expectedVerdicts.every((e, i) => e === actualVerdicts[i]);
|
|
80
|
+
|
|
81
|
+
const accuracyProof = poseidonHash(
|
|
82
|
+
commitment.expectedHash,
|
|
83
|
+
resultHash,
|
|
84
|
+
allMatch ? '1' : '0',
|
|
85
|
+
commitment.salt,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
commitment,
|
|
90
|
+
resultHash,
|
|
91
|
+
accuracyProof,
|
|
92
|
+
passed: allMatch,
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function verifyProof(proof: ZKProof): boolean {
|
|
98
|
+
const recomputedProof = poseidonHash(
|
|
99
|
+
proof.commitment.expectedHash,
|
|
100
|
+
proof.resultHash,
|
|
101
|
+
proof.passed ? '1' : '0',
|
|
102
|
+
proof.commitment.salt,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return recomputedProof === proof.accuracyProof;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function proofToOnChainBytes(proof: ZKProof): string {
|
|
109
|
+
const payload = JSON.stringify({
|
|
110
|
+
commitment: proof.commitment.expectedHash,
|
|
111
|
+
resultHash: proof.resultHash,
|
|
112
|
+
accuracyProof: proof.accuracyProof,
|
|
113
|
+
passed: proof.passed,
|
|
114
|
+
timestamp: proof.timestamp,
|
|
115
|
+
caseCount: proof.commitment.caseCount,
|
|
116
|
+
});
|
|
117
|
+
return '0x' + Buffer.from(payload).toString('hex');
|
|
118
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pages": {
|
|
3
|
+
"/layout": [
|
|
4
|
+
"static/chunks/webpack.js",
|
|
5
|
+
"static/chunks/main-app.js",
|
|
6
|
+
"static/css/app/layout.css",
|
|
7
|
+
"static/chunks/app/layout.js"
|
|
8
|
+
],
|
|
9
|
+
"/page": [
|
|
10
|
+
"static/chunks/webpack.js",
|
|
11
|
+
"static/chunks/main-app.js",
|
|
12
|
+
"static/chunks/app/page.js"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"polyfillFiles": [
|
|
3
|
+
"static/chunks/polyfills.js"
|
|
4
|
+
],
|
|
5
|
+
"devFiles": [],
|
|
6
|
+
"ampDevFiles": [],
|
|
7
|
+
"lowPriorityFiles": [
|
|
8
|
+
"static/development/_buildManifest.js",
|
|
9
|
+
"static/development/_ssgManifest.js"
|
|
10
|
+
],
|
|
11
|
+
"rootMainFiles": [
|
|
12
|
+
"static/chunks/webpack.js",
|
|
13
|
+
"static/chunks/main-app.js"
|
|
14
|
+
],
|
|
15
|
+
"rootMainFilesTree": {},
|
|
16
|
+
"pages": {
|
|
17
|
+
"/_app": []
|
|
18
|
+
},
|
|
19
|
+
"ampFirstPages": []
|
|
20
|
+
}
|