solidity-argus 0.3.3 → 0.3.5
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/package.json +1 -1
- package/src/agents/argus-prompt.ts +67 -8
- package/src/agents/scribe-prompt.ts +13 -5
- package/src/cli/commands/init.ts +1 -1
- package/src/cli/index.ts +0 -0
- package/src/config/schema.ts +7 -2
- package/src/create-hooks.ts +116 -27
- package/src/features/audit-enforcer/audit-enforcer.ts +31 -2
- package/src/features/migration/index.ts +14 -0
- package/src/features/migration/migration-adapter.ts +151 -0
- package/src/features/migration/parity-telemetry.ts +133 -0
- package/src/features/persistent-state/audit-state-manager.ts +28 -6
- package/src/features/persistent-state/event-sink.ts +175 -0
- package/src/features/persistent-state/findings-materializer.ts +51 -0
- package/src/features/persistent-state/index.ts +2 -0
- package/src/features/persistent-state/run-finalizer.ts +192 -0
- package/src/features/persistent-state/run-journal.ts +15 -4
- package/src/hooks/agent-tracker.ts +15 -0
- package/src/hooks/event-hook.ts +93 -1
- package/src/hooks/system-prompt-hook.ts +20 -0
- package/src/hooks/tool-tracking-hook.ts +263 -33
- package/src/shared/audit-artifact-resolver.ts +75 -0
- package/src/shared/drop-diagnostics.ts +108 -0
- package/src/shared/file-utils.ts +7 -2
- package/src/shared/index.ts +14 -0
- package/src/shared/path-root-resolver.ts +34 -0
- package/src/shared/report-path-resolver.ts +70 -0
- package/src/solodit-lifecycle.ts +86 -7
- package/src/state/adapters.ts +262 -0
- package/src/state/index.ts +15 -0
- package/src/state/projectors.ts +437 -0
- package/src/state/schemas.ts +453 -0
- package/src/state/types.ts +6 -0
- package/src/tools/report-generator-tool.ts +647 -36
- package/src/tools/report-preflight.ts +79 -0
- package/src/tools/solodit-search-tool.ts +15 -24
- package/src/utils/solodit-health.ts +18 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from "./adapters"
|
|
2
|
+
export { createAuditState } from "./audit-state"
|
|
3
|
+
export { createFindingStore } from "./finding-store"
|
|
4
|
+
export {
|
|
5
|
+
ProjectorError,
|
|
6
|
+
projectAuditState,
|
|
7
|
+
projectFindings,
|
|
8
|
+
projectReportInput,
|
|
9
|
+
projectToolExecutions,
|
|
10
|
+
SEVERITY_RANK,
|
|
11
|
+
stableHash,
|
|
12
|
+
validateEventSequence,
|
|
13
|
+
} from "./projectors"
|
|
14
|
+
export * from "./schemas"
|
|
15
|
+
export * from "./types"
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import {
|
|
3
|
+
type AuditEvent,
|
|
4
|
+
type CanonicalFinding,
|
|
5
|
+
type CanonicalToolExecution,
|
|
6
|
+
type ReportInput,
|
|
7
|
+
SCHEMA_VERSION,
|
|
8
|
+
validateCanonicalFinding,
|
|
9
|
+
} from "./schemas"
|
|
10
|
+
import type {
|
|
11
|
+
AuditPhase,
|
|
12
|
+
AuditState,
|
|
13
|
+
Finding,
|
|
14
|
+
FuzzCounterexample,
|
|
15
|
+
SoloditResult,
|
|
16
|
+
ToolExecution,
|
|
17
|
+
} from "./types"
|
|
18
|
+
|
|
19
|
+
type ProjectorErrorCode = "OUT_OF_ORDER" | "DUPLICATE_SEQ" | "INVALID_EVENT"
|
|
20
|
+
|
|
21
|
+
type CoverageReport = NonNullable<AuditState["coverageReport"]>
|
|
22
|
+
type GasHotspot = NonNullable<AuditState["gasHotspots"]>[number]
|
|
23
|
+
type ProxyContract = NonNullable<AuditState["proxyContracts"]>[number]
|
|
24
|
+
|
|
25
|
+
export class ProjectorError extends Error {
|
|
26
|
+
readonly code: ProjectorErrorCode
|
|
27
|
+
|
|
28
|
+
constructor(code: ProjectorErrorCode, message: string) {
|
|
29
|
+
super(message)
|
|
30
|
+
this.name = "ProjectorError"
|
|
31
|
+
this.code = code
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const SEVERITY_RANK: Record<CanonicalFinding["severity"], number> = {
|
|
36
|
+
Critical: 0,
|
|
37
|
+
High: 1,
|
|
38
|
+
Medium: 2,
|
|
39
|
+
Low: 3,
|
|
40
|
+
Informational: 4,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
44
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractScope(payload: unknown): string[] {
|
|
48
|
+
if (!isRecord(payload) || !Array.isArray(payload.scope)) return []
|
|
49
|
+
return payload.scope.filter((entry): entry is string => typeof entry === "string")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractPhase(payload: unknown): AuditPhase | undefined {
|
|
53
|
+
if (typeof payload === "string") {
|
|
54
|
+
return payload as AuditPhase
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!isRecord(payload)) return undefined
|
|
58
|
+
|
|
59
|
+
if (typeof payload.phase === "string") {
|
|
60
|
+
return payload.phase as AuditPhase
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof payload.currentPhase === "string") {
|
|
64
|
+
return payload.currentPhase as AuditPhase
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sortedUnique(values: string[]): string[] {
|
|
71
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getPayloadRecord(event: AuditEvent): Record<string, unknown> {
|
|
75
|
+
if (!isRecord(event.payload)) {
|
|
76
|
+
throw new ProjectorError("INVALID_EVENT", `event seq ${event.seq} payload must be an object`)
|
|
77
|
+
}
|
|
78
|
+
return event.payload
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveToolName(event: AuditEvent, payload: Record<string, unknown>): string {
|
|
82
|
+
if (typeof payload.tool === "string" && payload.tool.length > 0) {
|
|
83
|
+
return payload.tool
|
|
84
|
+
}
|
|
85
|
+
return event.source
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveFindingsCount(payload: Record<string, unknown>): number {
|
|
89
|
+
return typeof payload.findingsCount === "number" ? payload.findingsCount : 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveToolSuccess(payload: Record<string, unknown>): boolean {
|
|
93
|
+
return payload.success !== false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function asStringArray(value: unknown): string[] | undefined {
|
|
97
|
+
if (!Array.isArray(value)) return undefined
|
|
98
|
+
return value.filter((item): item is string => typeof item === "string")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function asString(value: unknown): string | undefined {
|
|
102
|
+
return typeof value === "string" ? value : undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function asSoloditResults(value: unknown): SoloditResult[] | undefined {
|
|
106
|
+
if (!Array.isArray(value)) return undefined
|
|
107
|
+
|
|
108
|
+
const results: SoloditResult[] = []
|
|
109
|
+
for (const raw of value) {
|
|
110
|
+
if (!isRecord(raw)) continue
|
|
111
|
+
if (typeof raw.query !== "string" || typeof raw.timestamp !== "number") continue
|
|
112
|
+
if (typeof raw.resultCount !== "number" || !Array.isArray(raw.topResults)) continue
|
|
113
|
+
const topResults = raw.topResults
|
|
114
|
+
.filter((entry): entry is Record<string, unknown> => isRecord(entry))
|
|
115
|
+
.map((entry) => ({
|
|
116
|
+
title: typeof entry.title === "string" ? entry.title : "",
|
|
117
|
+
severity: typeof entry.severity === "string" ? entry.severity : "",
|
|
118
|
+
url: typeof entry.url === "string" ? entry.url : "",
|
|
119
|
+
protocol: typeof entry.protocol === "string" ? entry.protocol : "",
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
results.push({
|
|
123
|
+
query: raw.query,
|
|
124
|
+
timestamp: raw.timestamp,
|
|
125
|
+
resultCount: raw.resultCount,
|
|
126
|
+
topResults,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return results
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function asFuzzCounterexamples(value: unknown): FuzzCounterexample[] | undefined {
|
|
134
|
+
if (!Array.isArray(value)) return undefined
|
|
135
|
+
|
|
136
|
+
const results: FuzzCounterexample[] = []
|
|
137
|
+
for (const raw of value) {
|
|
138
|
+
if (!isRecord(raw)) continue
|
|
139
|
+
if (typeof raw.testName !== "string" || typeof raw.runs !== "number") continue
|
|
140
|
+
if (typeof raw.timestamp !== "number") continue
|
|
141
|
+
|
|
142
|
+
const inputs = asStringArray(raw.inputs) ?? []
|
|
143
|
+
results.push({
|
|
144
|
+
testName: raw.testName,
|
|
145
|
+
inputs,
|
|
146
|
+
runs: raw.runs,
|
|
147
|
+
seed: typeof raw.seed === "number" ? raw.seed : undefined,
|
|
148
|
+
revertReason: typeof raw.revertReason === "string" ? raw.revertReason : undefined,
|
|
149
|
+
timestamp: raw.timestamp,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return results
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function asCoverageReport(value: unknown): CoverageReport | undefined {
|
|
157
|
+
if (!isRecord(value) || !Array.isArray(value.files)) return undefined
|
|
158
|
+
|
|
159
|
+
const files = value.files
|
|
160
|
+
.filter((entry): entry is Record<string, unknown> => isRecord(entry))
|
|
161
|
+
.map((entry) => ({
|
|
162
|
+
path: typeof entry.path === "string" ? entry.path : "",
|
|
163
|
+
linesPct: typeof entry.linesPct === "number" ? entry.linesPct : 0,
|
|
164
|
+
statementsPct: typeof entry.statementsPct === "number" ? entry.statementsPct : 0,
|
|
165
|
+
branchesPct: typeof entry.branchesPct === "number" ? entry.branchesPct : 0,
|
|
166
|
+
functionsPct: typeof entry.functionsPct === "number" ? entry.functionsPct : 0,
|
|
167
|
+
}))
|
|
168
|
+
|
|
169
|
+
return { files }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function asGasHotspots(value: unknown): GasHotspot[] | undefined {
|
|
173
|
+
if (!Array.isArray(value)) return undefined
|
|
174
|
+
|
|
175
|
+
return value
|
|
176
|
+
.filter((entry): entry is Record<string, unknown> => isRecord(entry))
|
|
177
|
+
.map((entry) => ({
|
|
178
|
+
contract: typeof entry.contract === "string" ? entry.contract : "",
|
|
179
|
+
function: typeof entry.function === "string" ? entry.function : "",
|
|
180
|
+
avgGas: typeof entry.avgGas === "number" ? entry.avgGas : 0,
|
|
181
|
+
}))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function asProxyContracts(value: unknown): ProxyContract[] | undefined {
|
|
185
|
+
if (!Array.isArray(value)) return undefined
|
|
186
|
+
|
|
187
|
+
return value
|
|
188
|
+
.filter((entry): entry is Record<string, unknown> => isRecord(entry))
|
|
189
|
+
.map((entry) => ({
|
|
190
|
+
file: typeof entry.file === "string" ? entry.file : "",
|
|
191
|
+
proxyType: typeof entry.proxyType === "string" ? entry.proxyType : "",
|
|
192
|
+
indicators: asStringArray(entry.indicators) ?? [],
|
|
193
|
+
}))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function extractLatestFromPayload<T>(
|
|
197
|
+
events: AuditEvent[],
|
|
198
|
+
key: string,
|
|
199
|
+
parser: (value: unknown) => T | undefined,
|
|
200
|
+
): T | undefined {
|
|
201
|
+
let latest: T | undefined
|
|
202
|
+
|
|
203
|
+
for (const event of events) {
|
|
204
|
+
if (!isRecord(event.payload)) continue
|
|
205
|
+
if (!(key in event.payload)) continue
|
|
206
|
+
|
|
207
|
+
const parsed = parser(event.payload[key])
|
|
208
|
+
if (parsed !== undefined) {
|
|
209
|
+
latest = parsed
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return latest
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function validateEventSequence(events: AuditEvent[]): void {
|
|
217
|
+
if (events.length === 0) return
|
|
218
|
+
|
|
219
|
+
const seen = new Set<number>()
|
|
220
|
+
|
|
221
|
+
for (const event of events) {
|
|
222
|
+
if (!Number.isInteger(event.seq) || event.seq <= 0) {
|
|
223
|
+
throw new ProjectorError("INVALID_EVENT", `invalid seq ${event.seq}`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (seen.has(event.seq)) {
|
|
227
|
+
throw new ProjectorError("DUPLICATE_SEQ", `duplicate seq ${event.seq}`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
seen.add(event.seq)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (let i = 0; i < events.length; i++) {
|
|
234
|
+
const expectedSeq = i + 1
|
|
235
|
+
const actualSeq = events[i]?.seq
|
|
236
|
+
if (actualSeq !== expectedSeq) {
|
|
237
|
+
throw new ProjectorError(
|
|
238
|
+
"OUT_OF_ORDER",
|
|
239
|
+
`expected seq ${expectedSeq} at index ${i}, got ${String(actualSeq)}`,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function projectFindings(events: AuditEvent[]): CanonicalFinding[] {
|
|
246
|
+
validateEventSequence(events)
|
|
247
|
+
|
|
248
|
+
const byId = new Map<string, CanonicalFinding>()
|
|
249
|
+
|
|
250
|
+
for (const event of events) {
|
|
251
|
+
if (event.type !== "finding.added") continue
|
|
252
|
+
|
|
253
|
+
const validation = validateCanonicalFinding(event.payload)
|
|
254
|
+
if (!validation.success) {
|
|
255
|
+
throw new ProjectorError(
|
|
256
|
+
"INVALID_EVENT",
|
|
257
|
+
`invalid finding payload at seq ${event.seq}: ${validation.errors.map((e) => e.field).join(",")}`,
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
byId.set(validation.data.id, validation.data)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return Array.from(byId.values()).sort((left, right) => {
|
|
265
|
+
const bySeverity = SEVERITY_RANK[left.severity] - SEVERITY_RANK[right.severity]
|
|
266
|
+
if (bySeverity !== 0) return bySeverity
|
|
267
|
+
|
|
268
|
+
const byFile = left.file.localeCompare(right.file)
|
|
269
|
+
if (byFile !== 0) return byFile
|
|
270
|
+
|
|
271
|
+
const byLine = left.lines[0] - right.lines[0]
|
|
272
|
+
if (byLine !== 0) return byLine
|
|
273
|
+
|
|
274
|
+
return left.id.localeCompare(right.id)
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function projectToolExecutions(events: AuditEvent[]): CanonicalToolExecution[] {
|
|
279
|
+
validateEventSequence(events)
|
|
280
|
+
|
|
281
|
+
const executionsByCallId = new Map<string, CanonicalToolExecution>()
|
|
282
|
+
|
|
283
|
+
for (const event of events) {
|
|
284
|
+
if (event.type !== "tool.started" && event.type !== "tool.completed") continue
|
|
285
|
+
if (typeof event.tool_call_id !== "string" || event.tool_call_id.length === 0) {
|
|
286
|
+
throw new ProjectorError(
|
|
287
|
+
"INVALID_EVENT",
|
|
288
|
+
`${event.type} at seq ${event.seq} missing tool_call_id`,
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const payload = getPayloadRecord(event)
|
|
293
|
+
const existing = executionsByCallId.get(event.tool_call_id)
|
|
294
|
+
|
|
295
|
+
if (event.type === "tool.started") {
|
|
296
|
+
executionsByCallId.set(event.tool_call_id, {
|
|
297
|
+
run_id: event.run_id,
|
|
298
|
+
schema_version: event.schema_version,
|
|
299
|
+
tool: resolveToolName(event, payload),
|
|
300
|
+
startTime: event.timestamp,
|
|
301
|
+
endTime: existing?.endTime,
|
|
302
|
+
success: existing?.success ?? false,
|
|
303
|
+
findingsCount: existing?.findingsCount ?? 0,
|
|
304
|
+
})
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const base: CanonicalToolExecution = existing ?? {
|
|
309
|
+
run_id: event.run_id,
|
|
310
|
+
schema_version: event.schema_version,
|
|
311
|
+
tool: resolveToolName(event, payload),
|
|
312
|
+
startTime: event.timestamp,
|
|
313
|
+
success: false,
|
|
314
|
+
findingsCount: 0,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
executionsByCallId.set(event.tool_call_id, {
|
|
318
|
+
...base,
|
|
319
|
+
tool: resolveToolName(event, payload),
|
|
320
|
+
endTime: event.timestamp,
|
|
321
|
+
success: resolveToolSuccess(payload),
|
|
322
|
+
findingsCount: resolveFindingsCount(payload),
|
|
323
|
+
run_id: event.run_id,
|
|
324
|
+
schema_version: event.schema_version,
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return Array.from(executionsByCallId.values()).sort((left, right) => {
|
|
329
|
+
if (left.startTime !== right.startTime) {
|
|
330
|
+
return left.startTime - right.startTime
|
|
331
|
+
}
|
|
332
|
+
return left.tool.localeCompare(right.tool)
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function projectAuditState(events: AuditEvent[], projectDir: string): AuditState {
|
|
337
|
+
validateEventSequence(events)
|
|
338
|
+
|
|
339
|
+
const sessionCreated = events.find((event) => event.type === "session.created")
|
|
340
|
+
const scope = sessionCreated ? extractScope(sessionCreated.payload) : []
|
|
341
|
+
|
|
342
|
+
const findings = projectFindings(events)
|
|
343
|
+
const toolsExecuted = projectToolExecutions(events)
|
|
344
|
+
|
|
345
|
+
const contractsReviewed = sortedUnique(
|
|
346
|
+
findings.map((finding) => finding.file).filter((file) => file.length > 0),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
let currentPhase: AuditPhase = "reconnaissance"
|
|
350
|
+
for (const event of events) {
|
|
351
|
+
if (event.type !== "phase.changed") continue
|
|
352
|
+
const phase = extractPhase(event.payload)
|
|
353
|
+
if (phase) {
|
|
354
|
+
currentPhase = phase
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
sessionId: sessionCreated?.session_id ?? events[0]?.session_id ?? "",
|
|
360
|
+
projectDir,
|
|
361
|
+
contractsReviewed,
|
|
362
|
+
findings: findings as Finding[],
|
|
363
|
+
toolsExecuted: toolsExecuted as ToolExecution[],
|
|
364
|
+
currentPhase,
|
|
365
|
+
scope,
|
|
366
|
+
startTime: events[0]?.timestamp ?? 0,
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function projectReportInput(
|
|
371
|
+
events: AuditEvent[],
|
|
372
|
+
runId: string,
|
|
373
|
+
projectDir: string,
|
|
374
|
+
): ReportInput {
|
|
375
|
+
validateEventSequence(events)
|
|
376
|
+
|
|
377
|
+
const sessionCreated = events.find((event) => event.type === "session.created")
|
|
378
|
+
const latestFinalized = [...events].reverse().find((event) => event.type === "run.finalized")
|
|
379
|
+
|
|
380
|
+
const findings = projectFindings(events)
|
|
381
|
+
const toolsExecuted = projectToolExecutions(events)
|
|
382
|
+
|
|
383
|
+
const scope = sessionCreated ? extractScope(sessionCreated.payload) : []
|
|
384
|
+
const soloditResults = extractLatestFromPayload(events, "soloditResults", asSoloditResults)
|
|
385
|
+
const fuzzCounterexamples = extractLatestFromPayload(
|
|
386
|
+
events,
|
|
387
|
+
"fuzzCounterexamples",
|
|
388
|
+
asFuzzCounterexamples,
|
|
389
|
+
)
|
|
390
|
+
const coverageReport = extractLatestFromPayload(events, "coverageReport", asCoverageReport)
|
|
391
|
+
const gasHotspots = extractLatestFromPayload(events, "gasHotspots", asGasHotspots)
|
|
392
|
+
const proxyContracts = extractLatestFromPayload(events, "proxyContracts", asProxyContracts)
|
|
393
|
+
const patternVersion = extractLatestFromPayload(events, "patternVersion", asString)
|
|
394
|
+
const skillsLoaded = extractLatestFromPayload(events, "skillsLoaded", asStringArray)
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
run_id: runId,
|
|
398
|
+
seq: events.at(-1)?.seq ?? 0,
|
|
399
|
+
session_id: sessionCreated?.session_id ?? events[0]?.session_id ?? "",
|
|
400
|
+
tool_call_id: latestFinalized?.tool_call_id ?? "",
|
|
401
|
+
source: latestFinalized?.source ?? events[0]?.source ?? "projector",
|
|
402
|
+
schema_version: latestFinalized?.schema_version ?? events[0]?.schema_version ?? SCHEMA_VERSION,
|
|
403
|
+
projectDir,
|
|
404
|
+
findings,
|
|
405
|
+
toolsExecuted,
|
|
406
|
+
scope,
|
|
407
|
+
soloditResults,
|
|
408
|
+
fuzzCounterexamples,
|
|
409
|
+
coverageReport,
|
|
410
|
+
gasHotspots,
|
|
411
|
+
proxyContracts,
|
|
412
|
+
patternVersion,
|
|
413
|
+
skillsLoaded,
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function sortForStableStringify(value: unknown): unknown {
|
|
418
|
+
if (Array.isArray(value)) {
|
|
419
|
+
return value.map((item) => sortForStableStringify(item))
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (isRecord(value)) {
|
|
423
|
+
const out: Record<string, unknown> = {}
|
|
424
|
+
for (const key of Object.keys(value).sort((a, b) => a.localeCompare(b))) {
|
|
425
|
+
out[key] = sortForStableStringify(value[key])
|
|
426
|
+
}
|
|
427
|
+
return out
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return value
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function stableHash(obj: unknown): string {
|
|
434
|
+
const stable = sortForStableStringify(obj)
|
|
435
|
+
const json = JSON.stringify(stable)
|
|
436
|
+
return createHash("sha256").update(json).digest("hex")
|
|
437
|
+
}
|