solidity-argus 0.3.2 → 0.3.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/package.json +1 -1
- package/src/agents/argus-prompt.ts +21 -8
- package/src/agents/scribe-prompt.ts +9 -26
- package/src/cli/index.ts +0 -0
- package/src/config/schema.ts +5 -0
- package/src/create-hooks.ts +81 -20
- 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/event-sink.ts +171 -0
- package/src/features/persistent-state/index.ts +2 -0
- package/src/features/persistent-state/run-finalizer.ts +175 -0
- package/src/features/persistent-state/run-journal.ts +1 -1
- package/src/hooks/agent-tracker.ts +15 -0
- package/src/hooks/event-hook.ts +93 -1
- package/src/hooks/tool-tracking-hook.ts +263 -33
- package/src/shared/audit-artifact-resolver.ts +74 -0
- package/src/shared/drop-diagnostics.ts +108 -0
- package/src/shared/index.ts +14 -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 +356 -0
- package/src/tools/report-generator-tool.ts +692 -20
- package/src/tools/solodit-search-tool.ts +11 -24
- package/src/utils/solodit-health.ts +18 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuditPhase,
|
|
3
|
+
Finding,
|
|
4
|
+
FindingSeverity,
|
|
5
|
+
FuzzCounterexample,
|
|
6
|
+
SoloditResult,
|
|
7
|
+
ToolExecution,
|
|
8
|
+
} from "./types"
|
|
9
|
+
|
|
10
|
+
export const SCHEMA_VERSION = "1.0.0"
|
|
11
|
+
|
|
12
|
+
export type AuditEventType =
|
|
13
|
+
| "session.created"
|
|
14
|
+
| "session.idle"
|
|
15
|
+
| "session.deleted"
|
|
16
|
+
| "tool.started"
|
|
17
|
+
| "tool.completed"
|
|
18
|
+
| "finding.added"
|
|
19
|
+
| "phase.changed"
|
|
20
|
+
| "run.finalized"
|
|
21
|
+
|
|
22
|
+
export interface ValidationError {
|
|
23
|
+
field: string
|
|
24
|
+
code: string
|
|
25
|
+
message: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ValidationResult<T> =
|
|
29
|
+
| { success: true; data: T }
|
|
30
|
+
| { success: false; errors: ValidationError[] }
|
|
31
|
+
|
|
32
|
+
export interface AuditEvent {
|
|
33
|
+
type: AuditEventType
|
|
34
|
+
run_id: string
|
|
35
|
+
seq: number
|
|
36
|
+
session_id: string
|
|
37
|
+
tool_call_id?: string
|
|
38
|
+
source: string
|
|
39
|
+
schema_version: string
|
|
40
|
+
timestamp: number
|
|
41
|
+
payload: unknown
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CanonicalFinding extends Finding {
|
|
45
|
+
run_id: string
|
|
46
|
+
seq: number
|
|
47
|
+
schema_version: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CanonicalToolExecution extends ToolExecution {
|
|
51
|
+
run_id: string
|
|
52
|
+
schema_version: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CoverageReport {
|
|
56
|
+
files: Array<{
|
|
57
|
+
path: string
|
|
58
|
+
linesPct: number
|
|
59
|
+
statementsPct: number
|
|
60
|
+
branchesPct: number
|
|
61
|
+
functionsPct: number
|
|
62
|
+
}>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface GasHotspot {
|
|
66
|
+
contract: string
|
|
67
|
+
function: string
|
|
68
|
+
avgGas: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ProxyContract {
|
|
72
|
+
file: string
|
|
73
|
+
proxyType: string
|
|
74
|
+
indicators: string[]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface AuditRunSnapshot {
|
|
78
|
+
run_id: string
|
|
79
|
+
seq: number
|
|
80
|
+
session_id: string
|
|
81
|
+
tool_call_id: string
|
|
82
|
+
source: string
|
|
83
|
+
schema_version: string
|
|
84
|
+
findings: CanonicalFinding[]
|
|
85
|
+
phase: AuditPhase
|
|
86
|
+
tools: CanonicalToolExecution[]
|
|
87
|
+
started_at: number
|
|
88
|
+
finalized_at?: number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ReportInput {
|
|
92
|
+
run_id: string
|
|
93
|
+
seq: number
|
|
94
|
+
session_id: string
|
|
95
|
+
tool_call_id: string
|
|
96
|
+
source: string
|
|
97
|
+
schema_version: string
|
|
98
|
+
projectDir: string
|
|
99
|
+
findings: CanonicalFinding[]
|
|
100
|
+
toolsExecuted: CanonicalToolExecution[]
|
|
101
|
+
scope: string[]
|
|
102
|
+
soloditResults?: SoloditResult[]
|
|
103
|
+
fuzzCounterexamples?: FuzzCounterexample[]
|
|
104
|
+
coverageReport?: CoverageReport
|
|
105
|
+
gasHotspots?: GasHotspot[]
|
|
106
|
+
proxyContracts?: ProxyContract[]
|
|
107
|
+
patternVersion?: string
|
|
108
|
+
skillsLoaded?: string[]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function pushRequiredRootStringError(
|
|
112
|
+
errors: ValidationError[],
|
|
113
|
+
obj: Record<string, unknown>,
|
|
114
|
+
field: keyof ReportInput,
|
|
115
|
+
): void {
|
|
116
|
+
if (typeof obj[field] !== "string" || (obj[field] as string).trim().length === 0) {
|
|
117
|
+
errors.push({
|
|
118
|
+
field: String(field),
|
|
119
|
+
code: "required",
|
|
120
|
+
message: `${String(field)} is required and must be a non-empty string`,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function pushRequiredRootNumberError(
|
|
126
|
+
errors: ValidationError[],
|
|
127
|
+
obj: Record<string, unknown>,
|
|
128
|
+
field: keyof ReportInput,
|
|
129
|
+
): void {
|
|
130
|
+
if (typeof obj[field] !== "number" || !Number.isInteger(obj[field] as number)) {
|
|
131
|
+
errors.push({
|
|
132
|
+
field: String(field),
|
|
133
|
+
code: "invalid",
|
|
134
|
+
message: `${String(field)} is required and must be an integer`,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const VALID_SEVERITIES: ReadonlySet<FindingSeverity> = new Set([
|
|
140
|
+
"Critical",
|
|
141
|
+
"High",
|
|
142
|
+
"Medium",
|
|
143
|
+
"Low",
|
|
144
|
+
"Informational",
|
|
145
|
+
])
|
|
146
|
+
const VALID_CONFIDENCES: ReadonlySet<CanonicalFinding["confidence"]> = new Set([
|
|
147
|
+
"High",
|
|
148
|
+
"Medium",
|
|
149
|
+
"Low",
|
|
150
|
+
])
|
|
151
|
+
const VALID_SOURCES: ReadonlySet<CanonicalFinding["source"]> = new Set([
|
|
152
|
+
"slither",
|
|
153
|
+
"manual",
|
|
154
|
+
"pattern",
|
|
155
|
+
"scvd",
|
|
156
|
+
"solodit",
|
|
157
|
+
"fuzz",
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
161
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pushRequiredStringError(
|
|
165
|
+
errors: ValidationError[],
|
|
166
|
+
obj: Record<string, unknown>,
|
|
167
|
+
field: keyof CanonicalFinding,
|
|
168
|
+
): void {
|
|
169
|
+
if (typeof obj[field] !== "string" || (obj[field] as string).trim().length === 0) {
|
|
170
|
+
errors.push({
|
|
171
|
+
field,
|
|
172
|
+
code: "required",
|
|
173
|
+
message: `${field} is required and must be a non-empty string`,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function validateCanonicalFinding(raw: unknown): ValidationResult<CanonicalFinding> {
|
|
179
|
+
if (!isRecord(raw)) {
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
errors: [
|
|
183
|
+
{
|
|
184
|
+
field: "$root",
|
|
185
|
+
code: "type",
|
|
186
|
+
message: "canonical finding must be an object",
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const errors: ValidationError[] = []
|
|
193
|
+
|
|
194
|
+
pushRequiredStringError(errors, raw, "id")
|
|
195
|
+
pushRequiredStringError(errors, raw, "check")
|
|
196
|
+
pushRequiredStringError(errors, raw, "description")
|
|
197
|
+
pushRequiredStringError(errors, raw, "file")
|
|
198
|
+
pushRequiredStringError(errors, raw, "run_id")
|
|
199
|
+
pushRequiredStringError(errors, raw, "schema_version")
|
|
200
|
+
|
|
201
|
+
if (typeof raw.seq !== "number" || !Number.isInteger(raw.seq) || raw.seq < 0) {
|
|
202
|
+
errors.push({
|
|
203
|
+
field: "seq",
|
|
204
|
+
code: "invalid",
|
|
205
|
+
message: "seq must be a non-negative integer",
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!Array.isArray(raw.lines) || raw.lines.length !== 2) {
|
|
210
|
+
errors.push({
|
|
211
|
+
field: "lines",
|
|
212
|
+
code: "invalid",
|
|
213
|
+
message: "lines must be a [start, end] tuple",
|
|
214
|
+
})
|
|
215
|
+
} else {
|
|
216
|
+
const [start, end] = raw.lines
|
|
217
|
+
if (typeof start !== "number" || typeof end !== "number") {
|
|
218
|
+
errors.push({
|
|
219
|
+
field: "lines",
|
|
220
|
+
code: "invalid",
|
|
221
|
+
message: "lines must contain numbers",
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (typeof raw.severity !== "string" || !VALID_SEVERITIES.has(raw.severity as FindingSeverity)) {
|
|
227
|
+
errors.push({
|
|
228
|
+
field: "severity",
|
|
229
|
+
code: "enum",
|
|
230
|
+
message: "severity must be one of: Critical, High, Medium, Low, Informational",
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (
|
|
235
|
+
typeof raw.confidence !== "string" ||
|
|
236
|
+
!VALID_CONFIDENCES.has(raw.confidence as CanonicalFinding["confidence"])
|
|
237
|
+
) {
|
|
238
|
+
errors.push({
|
|
239
|
+
field: "confidence",
|
|
240
|
+
code: "enum",
|
|
241
|
+
message: "confidence must be one of: High, Medium, Low",
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (
|
|
246
|
+
typeof raw.source !== "string" ||
|
|
247
|
+
!VALID_SOURCES.has(raw.source as CanonicalFinding["source"])
|
|
248
|
+
) {
|
|
249
|
+
errors.push({
|
|
250
|
+
field: "source",
|
|
251
|
+
code: "enum",
|
|
252
|
+
message: "source must be one of: slither, manual, pattern, scvd, solodit, fuzz",
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (errors.length > 0) {
|
|
257
|
+
return { success: false, errors }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { success: true, data: raw as unknown as CanonicalFinding }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function validateReportInput(raw: unknown): ValidationResult<ReportInput> {
|
|
264
|
+
if (!isRecord(raw)) {
|
|
265
|
+
return {
|
|
266
|
+
success: false,
|
|
267
|
+
errors: [
|
|
268
|
+
{
|
|
269
|
+
field: "$root",
|
|
270
|
+
code: "type",
|
|
271
|
+
message: "report input must be an object",
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const errors: ValidationError[] = []
|
|
278
|
+
|
|
279
|
+
pushRequiredRootStringError(errors, raw, "run_id")
|
|
280
|
+
pushRequiredRootNumberError(errors, raw, "seq")
|
|
281
|
+
pushRequiredRootStringError(errors, raw, "session_id")
|
|
282
|
+
pushRequiredRootStringError(errors, raw, "tool_call_id")
|
|
283
|
+
pushRequiredRootStringError(errors, raw, "source")
|
|
284
|
+
pushRequiredRootStringError(errors, raw, "schema_version")
|
|
285
|
+
pushRequiredRootStringError(errors, raw, "projectDir")
|
|
286
|
+
|
|
287
|
+
if (raw.schema_version !== SCHEMA_VERSION) {
|
|
288
|
+
errors.push({
|
|
289
|
+
field: "schema_version",
|
|
290
|
+
code: "version_mismatch",
|
|
291
|
+
message: `schema_version must be ${SCHEMA_VERSION}`,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!Array.isArray(raw.scope) || !raw.scope.every((item) => typeof item === "string")) {
|
|
296
|
+
errors.push({
|
|
297
|
+
field: "scope",
|
|
298
|
+
code: "invalid",
|
|
299
|
+
message: "scope must be an array of strings",
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!Array.isArray(raw.toolsExecuted)) {
|
|
304
|
+
errors.push({
|
|
305
|
+
field: "toolsExecuted",
|
|
306
|
+
code: "invalid",
|
|
307
|
+
message: "toolsExecuted must be an array",
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (raw.patternVersion != null && typeof raw.patternVersion !== "string") {
|
|
312
|
+
errors.push({
|
|
313
|
+
field: "patternVersion",
|
|
314
|
+
code: "invalid",
|
|
315
|
+
message: "patternVersion must be a string when provided",
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (
|
|
320
|
+
raw.skillsLoaded != null &&
|
|
321
|
+
(!Array.isArray(raw.skillsLoaded) ||
|
|
322
|
+
!raw.skillsLoaded.every((item) => typeof item === "string"))
|
|
323
|
+
) {
|
|
324
|
+
errors.push({
|
|
325
|
+
field: "skillsLoaded",
|
|
326
|
+
code: "invalid",
|
|
327
|
+
message: "skillsLoaded must be an array of strings when provided",
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!Array.isArray(raw.findings)) {
|
|
332
|
+
errors.push({
|
|
333
|
+
field: "findings",
|
|
334
|
+
code: "invalid",
|
|
335
|
+
message: "findings must be an array",
|
|
336
|
+
})
|
|
337
|
+
} else {
|
|
338
|
+
for (const [index, finding] of raw.findings.entries()) {
|
|
339
|
+
const findingValidation = validateCanonicalFinding(finding)
|
|
340
|
+
if (findingValidation.success) continue
|
|
341
|
+
for (const findingError of findingValidation.errors) {
|
|
342
|
+
errors.push({
|
|
343
|
+
field: `findings[${index}].${findingError.field}`,
|
|
344
|
+
code: findingError.code,
|
|
345
|
+
message: findingError.message,
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (errors.length > 0) {
|
|
352
|
+
return { success: false, errors }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return { success: true, data: raw as unknown as ReportInput }
|
|
356
|
+
}
|