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,453 @@
|
|
|
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
|
+
|
|
264
|
+
export function validateCanonicalToolExecution(
|
|
265
|
+
raw: unknown,
|
|
266
|
+
): ValidationResult<CanonicalToolExecution> {
|
|
267
|
+
if (!isRecord(raw)) {
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
errors: [
|
|
271
|
+
{
|
|
272
|
+
field: "$root",
|
|
273
|
+
code: "type",
|
|
274
|
+
message: "canonical tool execution must be an object",
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const errors: ValidationError[] = []
|
|
281
|
+
|
|
282
|
+
if (typeof raw.tool !== "string" || raw.tool.trim().length === 0) {
|
|
283
|
+
errors.push({
|
|
284
|
+
field: "tool",
|
|
285
|
+
code: "required",
|
|
286
|
+
message: "tool is required and must be a non-empty string",
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (typeof raw.startTime !== "number" || !Number.isInteger(raw.startTime) || raw.startTime <= 0) {
|
|
291
|
+
errors.push({
|
|
292
|
+
field: "startTime",
|
|
293
|
+
code: "invalid",
|
|
294
|
+
message: "startTime must be a positive integer",
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (raw.endTime != null && (typeof raw.endTime !== "number" || !Number.isInteger(raw.endTime))) {
|
|
299
|
+
errors.push({
|
|
300
|
+
field: "endTime",
|
|
301
|
+
code: "invalid",
|
|
302
|
+
message: "endTime must be an integer when provided",
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (typeof raw.success !== "boolean") {
|
|
307
|
+
errors.push({
|
|
308
|
+
field: "success",
|
|
309
|
+
code: "required",
|
|
310
|
+
message: "success is required and must be a boolean",
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (
|
|
315
|
+
typeof raw.findingsCount !== "number" ||
|
|
316
|
+
!Number.isInteger(raw.findingsCount) ||
|
|
317
|
+
raw.findingsCount < 0
|
|
318
|
+
) {
|
|
319
|
+
errors.push({
|
|
320
|
+
field: "findingsCount",
|
|
321
|
+
code: "invalid",
|
|
322
|
+
message: "findingsCount must be a non-negative integer",
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (typeof raw.run_id !== "string" || raw.run_id.trim().length === 0) {
|
|
327
|
+
errors.push({
|
|
328
|
+
field: "run_id",
|
|
329
|
+
code: "required",
|
|
330
|
+
message: "run_id is required and must be a non-empty string",
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (typeof raw.schema_version !== "string" || raw.schema_version.trim().length === 0) {
|
|
335
|
+
errors.push({
|
|
336
|
+
field: "schema_version",
|
|
337
|
+
code: "required",
|
|
338
|
+
message: "schema_version is required and must be a non-empty string",
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (errors.length > 0) {
|
|
343
|
+
return { success: false, errors }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { success: true, data: raw as unknown as CanonicalToolExecution }
|
|
347
|
+
}
|
|
348
|
+
export function validateReportInput(raw: unknown): ValidationResult<ReportInput> {
|
|
349
|
+
if (!isRecord(raw)) {
|
|
350
|
+
return {
|
|
351
|
+
success: false,
|
|
352
|
+
errors: [
|
|
353
|
+
{
|
|
354
|
+
field: "$root",
|
|
355
|
+
code: "type",
|
|
356
|
+
message: "report input must be an object",
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const errors: ValidationError[] = []
|
|
363
|
+
|
|
364
|
+
pushRequiredRootStringError(errors, raw, "run_id")
|
|
365
|
+
pushRequiredRootNumberError(errors, raw, "seq")
|
|
366
|
+
pushRequiredRootStringError(errors, raw, "session_id")
|
|
367
|
+
pushRequiredRootStringError(errors, raw, "tool_call_id")
|
|
368
|
+
pushRequiredRootStringError(errors, raw, "source")
|
|
369
|
+
pushRequiredRootStringError(errors, raw, "schema_version")
|
|
370
|
+
pushRequiredRootStringError(errors, raw, "projectDir")
|
|
371
|
+
|
|
372
|
+
if (raw.schema_version !== SCHEMA_VERSION) {
|
|
373
|
+
errors.push({
|
|
374
|
+
field: "schema_version",
|
|
375
|
+
code: "version_mismatch",
|
|
376
|
+
message: `schema_version must be ${SCHEMA_VERSION}`,
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!Array.isArray(raw.scope) || !raw.scope.every((item) => typeof item === "string")) {
|
|
381
|
+
errors.push({
|
|
382
|
+
field: "scope",
|
|
383
|
+
code: "invalid",
|
|
384
|
+
message: "scope must be an array of strings",
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!Array.isArray(raw.toolsExecuted)) {
|
|
389
|
+
errors.push({
|
|
390
|
+
field: "toolsExecuted",
|
|
391
|
+
code: "invalid",
|
|
392
|
+
message: "toolsExecuted must be an array",
|
|
393
|
+
})
|
|
394
|
+
} else {
|
|
395
|
+
for (const [index, entry] of raw.toolsExecuted.entries()) {
|
|
396
|
+
const toolValidation = validateCanonicalToolExecution(entry)
|
|
397
|
+
if (toolValidation.success) continue
|
|
398
|
+
for (const toolError of toolValidation.errors) {
|
|
399
|
+
errors.push({
|
|
400
|
+
field: `toolsExecuted[${index}].${toolError.field}`,
|
|
401
|
+
code: toolError.code,
|
|
402
|
+
message: toolError.message,
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (raw.patternVersion != null && typeof raw.patternVersion !== "string") {
|
|
409
|
+
errors.push({
|
|
410
|
+
field: "patternVersion",
|
|
411
|
+
code: "invalid",
|
|
412
|
+
message: "patternVersion must be a string when provided",
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (
|
|
417
|
+
raw.skillsLoaded != null &&
|
|
418
|
+
(!Array.isArray(raw.skillsLoaded) ||
|
|
419
|
+
!raw.skillsLoaded.every((item) => typeof item === "string"))
|
|
420
|
+
) {
|
|
421
|
+
errors.push({
|
|
422
|
+
field: "skillsLoaded",
|
|
423
|
+
code: "invalid",
|
|
424
|
+
message: "skillsLoaded must be an array of strings when provided",
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!Array.isArray(raw.findings)) {
|
|
429
|
+
errors.push({
|
|
430
|
+
field: "findings",
|
|
431
|
+
code: "invalid",
|
|
432
|
+
message: "findings must be an array",
|
|
433
|
+
})
|
|
434
|
+
} else {
|
|
435
|
+
for (const [index, finding] of raw.findings.entries()) {
|
|
436
|
+
const findingValidation = validateCanonicalFinding(finding)
|
|
437
|
+
if (findingValidation.success) continue
|
|
438
|
+
for (const findingError of findingValidation.errors) {
|
|
439
|
+
errors.push({
|
|
440
|
+
field: `findings[${index}].${findingError.field}`,
|
|
441
|
+
code: findingError.code,
|
|
442
|
+
message: findingError.message,
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (errors.length > 0) {
|
|
449
|
+
return { success: false, errors }
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return { success: true, data: raw as unknown as ReportInput }
|
|
453
|
+
}
|
package/src/state/types.ts
CHANGED
|
@@ -110,4 +110,10 @@ export interface PersistentAuditState extends AuditState {
|
|
|
110
110
|
savedAt: number
|
|
111
111
|
version: string
|
|
112
112
|
filePath: string
|
|
113
|
+
/** Whether this snapshot was projected from events or loaded from a prior snapshot */
|
|
114
|
+
source_of_truth?: "events" | "snapshot"
|
|
115
|
+
/** Sequence number of the last event included in this snapshot */
|
|
116
|
+
last_event_seq?: number
|
|
117
|
+
/** Hash of the event stream for staleness detection */
|
|
118
|
+
event_stream_hash?: string
|
|
113
119
|
}
|