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.
@@ -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
+ }