solidity-argus 0.3.4 → 0.3.6
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 +4 -4
- package/src/agents/argus-prompt.ts +56 -2
- package/src/agents/pythia-prompt.ts +11 -0
- package/src/agents/scribe-prompt.ts +9 -4
- package/src/agents/sentinel-prompt.ts +10 -0
- package/src/cli/commands/init.ts +1 -1
- package/src/config/schema.ts +2 -2
- package/src/create-hooks.ts +95 -12
- package/src/create-tools.ts +2 -0
- package/src/features/audit-enforcer/audit-enforcer.ts +30 -2
- package/src/features/persistent-state/audit-state-manager.ts +180 -10
- package/src/features/persistent-state/event-sink.ts +15 -6
- package/src/features/persistent-state/findings-materializer.ts +52 -0
- package/src/features/persistent-state/index.ts +1 -1
- package/src/features/persistent-state/run-finalizer.ts +26 -7
- package/src/features/persistent-state/run-journal.ts +12 -4
- package/src/hooks/event-hook.ts +4 -1
- package/src/hooks/system-prompt-hook.ts +15 -0
- package/src/hooks/tool-tracking-hook.ts +168 -10
- package/src/shared/audit-artifact-resolver.ts +13 -12
- package/src/shared/file-utils.ts +7 -2
- package/src/shared/index.ts +8 -8
- package/src/shared/path-root-resolver.ts +34 -0
- package/src/shared/plugin-metadata.ts +23 -0
- package/src/shared/report-path-resolver.ts +3 -3
- package/src/state/adapters.ts +99 -5
- package/src/state/finding-aggregation.ts +100 -0
- package/src/state/finding-fingerprint.ts +47 -0
- package/src/state/finding-store.ts +19 -29
- package/src/state/projectors.ts +18 -4
- package/src/state/schemas.ts +145 -1
- package/src/state/types.ts +17 -1
- package/src/tools/record-finding-tool.ts +125 -0
- package/src/tools/report-generator-tool.ts +116 -7
- package/src/tools/report-preflight.ts +79 -0
- package/src/tools/solodit-search-tool.ts +6 -2
|
@@ -12,7 +12,14 @@ import type { FindingStore } from "../state/finding-store"
|
|
|
12
12
|
import { createFindingStore } from "../state/finding-store"
|
|
13
13
|
import type { AuditEvent } from "../state/schemas"
|
|
14
14
|
import { SCHEMA_VERSION } from "../state/schemas"
|
|
15
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
ArgusAgentName,
|
|
17
|
+
AuditState,
|
|
18
|
+
Finding,
|
|
19
|
+
FindingSeverity,
|
|
20
|
+
FuzzCounterexample,
|
|
21
|
+
SoloditResult,
|
|
22
|
+
} from "../state/types"
|
|
16
23
|
|
|
17
24
|
const logger = createLogger()
|
|
18
25
|
|
|
@@ -30,6 +37,7 @@ type ToolExecutionMetadata = {
|
|
|
30
37
|
export type ToolTrackingOptions = {
|
|
31
38
|
getEventSink?: () => EventSink | null
|
|
32
39
|
getSessionId?: () => string
|
|
40
|
+
getAgentName?: () => ArgusAgentName | undefined
|
|
33
41
|
dropPolicy?: DropPolicy
|
|
34
42
|
onChildSessionDetected?: (parentSessionId: string, childSessionId: string) => void
|
|
35
43
|
}
|
|
@@ -77,13 +85,35 @@ function toRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
77
85
|
return undefined
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
|
|
88
|
+
function toFindingSource(value: unknown): Finding["source"] {
|
|
89
|
+
if (
|
|
90
|
+
value === "slither" ||
|
|
91
|
+
value === "manual" ||
|
|
92
|
+
value === "pattern" ||
|
|
93
|
+
value === "scvd" ||
|
|
94
|
+
value === "solodit" ||
|
|
95
|
+
value === "fuzz"
|
|
96
|
+
) {
|
|
97
|
+
return value
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return "manual"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function emitToSink(
|
|
104
|
+
sink: EventSink,
|
|
105
|
+
event: AuditEvent,
|
|
106
|
+
options?: { failFast?: boolean },
|
|
107
|
+
): Promise<void> {
|
|
81
108
|
try {
|
|
82
109
|
await sink.append(event)
|
|
83
110
|
} catch (error) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
const message = `Failed to emit ${event.type} event to sink: ${error instanceof Error ? error.message : String(error)}`
|
|
112
|
+
logger.error(message)
|
|
113
|
+
|
|
114
|
+
if (options?.failFast) {
|
|
115
|
+
throw new Error(message)
|
|
116
|
+
}
|
|
87
117
|
}
|
|
88
118
|
}
|
|
89
119
|
|
|
@@ -155,11 +185,13 @@ function identifyMissingFields(
|
|
|
155
185
|
|
|
156
186
|
const SLITHER_REQUIRED = ["check", "description", "file", "lines"] as const
|
|
157
187
|
const PATTERN_REQUIRED = ["pattern", "description", "file", "lines"] as const
|
|
188
|
+
const MANUAL_REQUIRED = ["check", "description", "file", "lines"] as const
|
|
158
189
|
|
|
159
190
|
function processSlitherResult(
|
|
160
191
|
parsed: Record<string, unknown>,
|
|
161
192
|
store: FindingStore,
|
|
162
193
|
diag: DropDiagnosticsCollector,
|
|
194
|
+
metadata: { reportedByAgent: ArgusAgentName; reportedBySessionId: string },
|
|
163
195
|
): number {
|
|
164
196
|
const findings = parsed.findings
|
|
165
197
|
if (!Array.isArray(findings)) return 0
|
|
@@ -197,6 +229,8 @@ function processSlitherResult(
|
|
|
197
229
|
file,
|
|
198
230
|
lines,
|
|
199
231
|
source: "slither",
|
|
232
|
+
reported_by_agent: metadata.reportedByAgent,
|
|
233
|
+
reported_by_session_id: metadata.reportedBySessionId,
|
|
200
234
|
})
|
|
201
235
|
count++
|
|
202
236
|
}
|
|
@@ -208,6 +242,7 @@ function processPatternResult(
|
|
|
208
242
|
parsed: Record<string, unknown>,
|
|
209
243
|
store: FindingStore,
|
|
210
244
|
diag: DropDiagnosticsCollector,
|
|
245
|
+
metadata: { reportedByAgent: ArgusAgentName; reportedBySessionId: string },
|
|
211
246
|
): number {
|
|
212
247
|
const sources = parsed.sources
|
|
213
248
|
if (!Array.isArray(sources)) return 0
|
|
@@ -252,6 +287,8 @@ function processPatternResult(
|
|
|
252
287
|
file,
|
|
253
288
|
lines,
|
|
254
289
|
source: "pattern",
|
|
290
|
+
reported_by_agent: metadata.reportedByAgent,
|
|
291
|
+
reported_by_session_id: metadata.reportedBySessionId,
|
|
255
292
|
})
|
|
256
293
|
count++
|
|
257
294
|
}
|
|
@@ -260,6 +297,89 @@ function processPatternResult(
|
|
|
260
297
|
return count
|
|
261
298
|
}
|
|
262
299
|
|
|
300
|
+
function processRecordedFindingResult(
|
|
301
|
+
parsed: Record<string, unknown>,
|
|
302
|
+
store: FindingStore,
|
|
303
|
+
diag: DropDiagnosticsCollector,
|
|
304
|
+
metadata: { reportedByAgent: ArgusAgentName; reportedBySessionId: string },
|
|
305
|
+
): number {
|
|
306
|
+
const findings = parsed.findings
|
|
307
|
+
if (!Array.isArray(findings)) {
|
|
308
|
+
diag.error(
|
|
309
|
+
"MISSING_REQUIRED_FIELD",
|
|
310
|
+
"argus_record_finding result missing findings array",
|
|
311
|
+
"findings",
|
|
312
|
+
)
|
|
313
|
+
return 0
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let count = 0
|
|
317
|
+
for (const raw of findings) {
|
|
318
|
+
const finding = toRecord(raw)
|
|
319
|
+
if (!finding) continue
|
|
320
|
+
|
|
321
|
+
const check = finding.check
|
|
322
|
+
const description = finding.description
|
|
323
|
+
const file = finding.file
|
|
324
|
+
const lines = toLines(finding.lines)
|
|
325
|
+
|
|
326
|
+
if (
|
|
327
|
+
typeof check !== "string" ||
|
|
328
|
+
typeof description !== "string" ||
|
|
329
|
+
typeof file !== "string" ||
|
|
330
|
+
!lines
|
|
331
|
+
) {
|
|
332
|
+
const missing = identifyMissingFields(finding, MANUAL_REQUIRED)
|
|
333
|
+
diag.error(
|
|
334
|
+
"MISSING_REQUIRED_FIELD",
|
|
335
|
+
`Recorded finding skipped: missing ${missing.join(", ")}`,
|
|
336
|
+
missing[0],
|
|
337
|
+
)
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const reportedByAgentRaw = finding.reported_by_agent
|
|
342
|
+
const reportedByAgent =
|
|
343
|
+
reportedByAgentRaw === "argus" ||
|
|
344
|
+
reportedByAgentRaw === "sentinel" ||
|
|
345
|
+
reportedByAgentRaw === "pythia" ||
|
|
346
|
+
reportedByAgentRaw === "scribe" ||
|
|
347
|
+
reportedByAgentRaw === "unknown"
|
|
348
|
+
? (reportedByAgentRaw as ArgusAgentName)
|
|
349
|
+
: metadata.reportedByAgent
|
|
350
|
+
|
|
351
|
+
store.addFinding({
|
|
352
|
+
check,
|
|
353
|
+
severity: toSeverity(finding.severity),
|
|
354
|
+
confidence: toConfidence(finding.confidence),
|
|
355
|
+
description,
|
|
356
|
+
file,
|
|
357
|
+
lines,
|
|
358
|
+
source: toFindingSource(finding.source),
|
|
359
|
+
remediation: typeof finding.remediation === "string" ? finding.remediation : undefined,
|
|
360
|
+
exploitReference:
|
|
361
|
+
typeof finding.exploitReference === "string" ? finding.exploitReference : undefined,
|
|
362
|
+
reported_by_agent: reportedByAgent,
|
|
363
|
+
reported_by_session_id:
|
|
364
|
+
typeof finding.reported_by_session_id === "string" &&
|
|
365
|
+
finding.reported_by_session_id.length > 0
|
|
366
|
+
? finding.reported_by_session_id
|
|
367
|
+
: metadata.reportedBySessionId,
|
|
368
|
+
issue_fingerprint:
|
|
369
|
+
typeof finding.issue_fingerprint === "string" ? finding.issue_fingerprint : undefined,
|
|
370
|
+
observation_fingerprint:
|
|
371
|
+
typeof finding.observation_fingerprint === "string"
|
|
372
|
+
? finding.observation_fingerprint
|
|
373
|
+
: undefined,
|
|
374
|
+
observation_id:
|
|
375
|
+
typeof finding.observation_id === "string" ? finding.observation_id : undefined,
|
|
376
|
+
})
|
|
377
|
+
count++
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return count
|
|
381
|
+
}
|
|
382
|
+
|
|
263
383
|
function processContractAnalyzerResult(parsed: Record<string, unknown>, state: AuditState): void {
|
|
264
384
|
if (typeof parsed.filePath === "string") {
|
|
265
385
|
if (!state.contractsReviewed.includes(parsed.filePath)) {
|
|
@@ -427,6 +547,10 @@ export function createToolTrackingHook(
|
|
|
427
547
|
|
|
428
548
|
const resolved = resolveStateAndStore()
|
|
429
549
|
if (!resolved) {
|
|
550
|
+
if (input.tool === "argus_record_finding") {
|
|
551
|
+
throw new Error("argus_record_finding requires active audit state")
|
|
552
|
+
}
|
|
553
|
+
|
|
430
554
|
const sinkForNoState = options?.getEventSink?.()
|
|
431
555
|
if (sinkForNoState) {
|
|
432
556
|
const toolCallId = randomUUID()
|
|
@@ -453,6 +577,11 @@ export function createToolTrackingHook(
|
|
|
453
577
|
const sink = options?.getEventSink?.()
|
|
454
578
|
const runId = auditState.sessionId
|
|
455
579
|
const sessionId = options?.getSessionId?.() ?? ""
|
|
580
|
+
const reportedByAgent = options?.getAgentName?.() ?? "unknown"
|
|
581
|
+
const findingMetadata = {
|
|
582
|
+
reportedByAgent,
|
|
583
|
+
reportedBySessionId: sessionId,
|
|
584
|
+
}
|
|
456
585
|
const toolCallId = randomUUID()
|
|
457
586
|
const policy = options?.dropPolicy ?? "warn"
|
|
458
587
|
const diag = createDropDiagnosticsCollector(policy, "tool-tracking-hook", input.tool)
|
|
@@ -464,6 +593,7 @@ export function createToolTrackingHook(
|
|
|
464
593
|
tool: input.tool,
|
|
465
594
|
args: input.args,
|
|
466
595
|
}),
|
|
596
|
+
{ failFast: input.tool === "argus_record_finding" },
|
|
467
597
|
)
|
|
468
598
|
}
|
|
469
599
|
|
|
@@ -502,6 +632,9 @@ export function createToolTrackingHook(
|
|
|
502
632
|
} catch {
|
|
503
633
|
diag.error("MALFORMED_JSON", `Failed to parse JSON result from ${input.tool}`)
|
|
504
634
|
lastDiagnostics = diag.getDiagnostics()
|
|
635
|
+
if (input.tool === "argus_record_finding") {
|
|
636
|
+
throw new Error("argus_record_finding returned malformed JSON")
|
|
637
|
+
}
|
|
505
638
|
diag.throwIfStrict()
|
|
506
639
|
return
|
|
507
640
|
}
|
|
@@ -509,6 +642,9 @@ export function createToolTrackingHook(
|
|
|
509
642
|
const record = toRecord(parsed)
|
|
510
643
|
if (!record) {
|
|
511
644
|
lastDiagnostics = diag.getDiagnostics()
|
|
645
|
+
if (input.tool === "argus_record_finding") {
|
|
646
|
+
throw new Error("argus_record_finding response must be a JSON object")
|
|
647
|
+
}
|
|
512
648
|
return
|
|
513
649
|
}
|
|
514
650
|
|
|
@@ -516,10 +652,13 @@ export function createToolTrackingHook(
|
|
|
516
652
|
|
|
517
653
|
switch (input.tool) {
|
|
518
654
|
case "argus_slither_analyze":
|
|
519
|
-
findingsCount = processSlitherResult(record, store, diag)
|
|
655
|
+
findingsCount = processSlitherResult(record, store, diag, findingMetadata)
|
|
520
656
|
break
|
|
521
657
|
case "argus_check_patterns":
|
|
522
|
-
findingsCount = processPatternResult(record, store, diag)
|
|
658
|
+
findingsCount = processPatternResult(record, store, diag, findingMetadata)
|
|
659
|
+
break
|
|
660
|
+
case "argus_record_finding":
|
|
661
|
+
findingsCount = processRecordedFindingResult(record, store, diag, findingMetadata)
|
|
523
662
|
break
|
|
524
663
|
case "argus_analyze_contract":
|
|
525
664
|
processContractAnalyzerResult(record, auditState)
|
|
@@ -595,14 +734,32 @@ export function createToolTrackingHook(
|
|
|
595
734
|
lastDiagnostics = diag.getDiagnostics()
|
|
596
735
|
diag.throwIfStrict()
|
|
597
736
|
|
|
737
|
+
if (input.tool === "argus_record_finding" && findingsCount === 0) {
|
|
738
|
+
throw new Error("argus_record_finding did not persist any findings")
|
|
739
|
+
}
|
|
740
|
+
|
|
598
741
|
recordToolExecution(auditState, input.tool, findingsCount)
|
|
599
742
|
onStateChanged?.({ tool: input.tool, findingsCount })
|
|
600
743
|
|
|
744
|
+
if (input.tool === "argus_record_finding" && !sink) {
|
|
745
|
+
throw new Error("argus_record_finding requires an active event sink for durable persistence")
|
|
746
|
+
}
|
|
747
|
+
|
|
601
748
|
if (sink) {
|
|
749
|
+
const failFast = input.tool === "argus_record_finding"
|
|
602
750
|
const newFindings = auditState.findings.slice(findingsCountBefore)
|
|
603
|
-
for (const finding of newFindings) {
|
|
604
|
-
const { data: canonical } = normalizeToCanonicalFinding(finding, runId, 0
|
|
605
|
-
|
|
751
|
+
for (const [index, finding] of newFindings.entries()) {
|
|
752
|
+
const { data: canonical } = normalizeToCanonicalFinding(finding, runId, 0, {
|
|
753
|
+
reportedByAgent,
|
|
754
|
+
reportedBySessionId: sessionId,
|
|
755
|
+
toolCallId,
|
|
756
|
+
observationId: `${toolCallId}:${index + 1}`,
|
|
757
|
+
})
|
|
758
|
+
await emitToSink(
|
|
759
|
+
sink,
|
|
760
|
+
buildEvent("finding.added", runId, sessionId, toolCallId, canonical),
|
|
761
|
+
{ failFast },
|
|
762
|
+
)
|
|
606
763
|
}
|
|
607
764
|
|
|
608
765
|
await emitToSink(
|
|
@@ -612,6 +769,7 @@ export function createToolTrackingHook(
|
|
|
612
769
|
findingsCount,
|
|
613
770
|
success: true,
|
|
614
771
|
}),
|
|
772
|
+
{ failFast },
|
|
615
773
|
)
|
|
616
774
|
}
|
|
617
775
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path"
|
|
2
|
+
import { defaultRootResolver } from "./path-root-resolver"
|
|
2
3
|
|
|
3
4
|
export class ArtifactResolverError extends Error {
|
|
4
5
|
constructor(message: string) {
|
|
@@ -8,19 +9,19 @@ export class ArtifactResolverError extends Error {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export interface AuditArtifactPaths {
|
|
11
|
-
/** {projectDir}/.
|
|
12
|
+
/** {projectDir}/.argus/argus-state.json */
|
|
12
13
|
stateFile: string
|
|
13
|
-
/** {projectDir}/.
|
|
14
|
+
/** {projectDir}/.argus/runs/{runId}/events.jsonl */
|
|
14
15
|
journalFile: string
|
|
15
|
-
/** {projectDir}/.
|
|
16
|
+
/** {projectDir}/.argus/runs/{runId}/findings.json */
|
|
16
17
|
findingsFile: string
|
|
17
|
-
/** {projectDir}/.
|
|
18
|
+
/** {projectDir}/.argus/reports */
|
|
18
19
|
reportDir: string
|
|
19
|
-
/** {projectDir}/.
|
|
20
|
+
/** {projectDir}/.argus/runs/{runId}/evidence */
|
|
20
21
|
evidenceDir: string
|
|
21
|
-
/** {projectDir}/.
|
|
22
|
+
/** {projectDir}/.argus/archives */
|
|
22
23
|
archiveDir: string
|
|
23
|
-
/** {projectDir}/.
|
|
24
|
+
/** {projectDir}/.argus/runs/{runId} */
|
|
24
25
|
runDir: string
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -45,16 +46,16 @@ export function createAuditArtifactResolver(
|
|
|
45
46
|
throw new ArtifactResolverError("projectDir must not be empty")
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
const
|
|
49
|
-
const runDir = join(
|
|
49
|
+
const writeRoot = defaultRootResolver.writeRoot(projectDir)
|
|
50
|
+
const runDir = join(writeRoot, "runs", runId)
|
|
50
51
|
|
|
51
52
|
const cachedPaths: AuditArtifactPaths = {
|
|
52
|
-
stateFile: join(
|
|
53
|
+
stateFile: join(writeRoot, "argus-state.json"),
|
|
53
54
|
journalFile: join(runDir, "events.jsonl"),
|
|
54
55
|
findingsFile: join(runDir, "findings.json"),
|
|
55
|
-
reportDir: join(
|
|
56
|
+
reportDir: join(writeRoot, "reports"),
|
|
56
57
|
evidenceDir: join(runDir, "evidence"),
|
|
57
|
-
archiveDir: join(
|
|
58
|
+
archiveDir: join(writeRoot, "archives"),
|
|
58
59
|
runDir,
|
|
59
60
|
}
|
|
60
61
|
|
package/src/shared/file-utils.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs"
|
|
2
2
|
import { join } from "node:path"
|
|
3
3
|
import { stripJsoncComments } from "./jsonc-parser"
|
|
4
|
+
import { defaultRootResolver } from "./path-root-resolver"
|
|
4
5
|
|
|
5
6
|
export type ConfigFormat = "json" | "jsonc" | "none"
|
|
6
7
|
|
|
@@ -10,9 +11,13 @@ export interface ConfigFileInfo {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function detectConfigFile(basePath: string): ConfigFileInfo {
|
|
14
|
+
const rootCandidates = defaultRootResolver.readRoots(basePath).flatMap((rootPath) => [
|
|
15
|
+
{ path: join(rootPath, "solidity-argus.jsonc"), format: "jsonc" as const },
|
|
16
|
+
{ path: join(rootPath, "solidity-argus.json"), format: "json" as const },
|
|
17
|
+
])
|
|
18
|
+
|
|
13
19
|
const candidates = [
|
|
14
|
-
|
|
15
|
-
{ path: join(basePath, ".opencode", "solidity-argus.json"), format: "json" as const },
|
|
20
|
+
...rootCandidates,
|
|
16
21
|
{ path: join(basePath, "solidity-argus.jsonc"), format: "jsonc" as const },
|
|
17
22
|
{ path: join(basePath, "solidity-argus.json"), format: "json" as const },
|
|
18
23
|
]
|
package/src/shared/index.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ArtifactResolverError,
|
|
3
|
+
type AuditArtifactPaths,
|
|
4
|
+
type AuditArtifactResolver,
|
|
5
|
+
createAuditArtifactResolver,
|
|
6
|
+
} from "./audit-artifact-resolver"
|
|
1
7
|
export { extractContractNames, hasBinary, parseSolcVersion } from "./binary-utils"
|
|
2
8
|
export { deepMerge } from "./deep-merge"
|
|
3
9
|
export {
|
|
@@ -10,16 +16,10 @@ export { stripJsoncComments } from "./jsonc-parser"
|
|
|
10
16
|
export { createLogger, type Logger, type LoggerConfig } from "./logger"
|
|
11
17
|
export { findFoundryProjectDir, resolveProjectDir } from "./project-utils"
|
|
12
18
|
export {
|
|
13
|
-
|
|
14
|
-
type AuditArtifactPaths,
|
|
15
|
-
type AuditArtifactResolver,
|
|
16
|
-
createAuditArtifactResolver,
|
|
17
|
-
} from "./audit-artifact-resolver"
|
|
18
|
-
export {
|
|
19
|
+
formatReportDate,
|
|
19
20
|
ReportPathError,
|
|
20
21
|
type ReportPathOptions,
|
|
21
22
|
type ResolvedReportPath,
|
|
22
|
-
formatReportDate,
|
|
23
|
-
sanitizeContractName,
|
|
24
23
|
resolveReportPath,
|
|
24
|
+
sanitizeContractName,
|
|
25
25
|
} from "./report-path-resolver"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
|
|
4
|
+
export interface ArgusRootResolver {
|
|
5
|
+
writeRoot(projectDir: string): string
|
|
6
|
+
readRoots(projectDir: string): string[]
|
|
7
|
+
resolveReadPath(projectDir: string, relativePath: string): string | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class DefaultArgusRootResolver implements ArgusRootResolver {
|
|
11
|
+
writeRoot(projectDir: string): string {
|
|
12
|
+
return join(projectDir, ".argus")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
readRoots(projectDir: string): string[] {
|
|
16
|
+
return [this.writeRoot(projectDir), join(projectDir, ".opencode")]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
resolveReadPath(projectDir: string, relativePath: string): string | null {
|
|
20
|
+
for (const root of this.readRoots(projectDir)) {
|
|
21
|
+
const candidatePath = join(root, relativePath)
|
|
22
|
+
if (existsSync(candidatePath)) {
|
|
23
|
+
return candidatePath
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createArgusRootResolver(): ArgusRootResolver {
|
|
31
|
+
return new DefaultArgusRootResolver()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const defaultRootResolver: ArgusRootResolver = createArgusRootResolver()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { dirname, resolve } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
|
|
5
|
+
function resolvePluginVersion(): string {
|
|
6
|
+
try {
|
|
7
|
+
const currentDir = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const packageJsonPath = resolve(currentDir, "../../package.json")
|
|
9
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
|
10
|
+
version?: unknown
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
14
|
+
return packageJson.version
|
|
15
|
+
}
|
|
16
|
+
} catch (_error) {
|
|
17
|
+
return "unknown"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return "unknown"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ARGUS_PLUGIN_VERSION = resolvePluginVersion()
|
|
@@ -30,9 +30,9 @@ export interface ResolvedReportPath {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function formatReportDate(date: Date): string {
|
|
33
|
-
const year = date.
|
|
34
|
-
const month = String(date.
|
|
35
|
-
const day = String(date.
|
|
33
|
+
const year = date.getUTCFullYear()
|
|
34
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0")
|
|
35
|
+
const day = String(date.getUTCDate()).padStart(2, "0")
|
|
36
36
|
return `${year}-${month}-${day}`
|
|
37
37
|
}
|
|
38
38
|
|
package/src/state/adapters.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { computeIssueFingerprint, computeObservationFingerprint } from "./finding-fingerprint"
|
|
1
2
|
import {
|
|
2
3
|
type CanonicalFinding,
|
|
3
4
|
SCHEMA_VERSION,
|
|
4
5
|
type ValidationError,
|
|
5
6
|
validateCanonicalFinding,
|
|
6
7
|
} from "./schemas"
|
|
7
|
-
import type { AuditPhase, Finding, FindingSeverity } from "./types"
|
|
8
|
+
import type { ArgusAgentName, AuditPhase, Finding, FindingSeverity } from "./types"
|
|
8
9
|
|
|
9
10
|
export interface Diagnostic {
|
|
10
11
|
level: "warn" | "error"
|
|
@@ -35,6 +36,13 @@ const VALID_SOURCES: ReadonlySet<CanonicalFinding["source"]> = new Set([
|
|
|
35
36
|
"solodit",
|
|
36
37
|
"fuzz",
|
|
37
38
|
])
|
|
39
|
+
const VALID_REPORTED_AGENTS: ReadonlySet<ArgusAgentName> = new Set([
|
|
40
|
+
"argus",
|
|
41
|
+
"sentinel",
|
|
42
|
+
"pythia",
|
|
43
|
+
"scribe",
|
|
44
|
+
"unknown",
|
|
45
|
+
])
|
|
38
46
|
|
|
39
47
|
const KNOWN_INPUT_FIELDS = new Set([
|
|
40
48
|
"id",
|
|
@@ -59,9 +67,26 @@ const KNOWN_INPUT_FIELDS = new Set([
|
|
|
59
67
|
"session_id",
|
|
60
68
|
"tool_call_id",
|
|
61
69
|
"schema_version",
|
|
70
|
+
"observation_id",
|
|
71
|
+
"observation_fingerprint",
|
|
72
|
+
"issue_fingerprint",
|
|
73
|
+
"reported_by_agent",
|
|
74
|
+
"reported_by_session_id",
|
|
75
|
+
"reportedByAgent",
|
|
76
|
+
"reportedBySessionId",
|
|
77
|
+
"observationId",
|
|
78
|
+
"observationFingerprint",
|
|
79
|
+
"issueFingerprint",
|
|
62
80
|
"elements",
|
|
63
81
|
])
|
|
64
82
|
|
|
83
|
+
export interface NormalizeFindingOptions {
|
|
84
|
+
reportedByAgent?: ArgusAgentName
|
|
85
|
+
reportedBySessionId?: string
|
|
86
|
+
toolCallId?: string
|
|
87
|
+
observationId?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
65
90
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
66
91
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
67
92
|
}
|
|
@@ -143,6 +168,7 @@ export function normalizeToCanonicalFinding(
|
|
|
143
168
|
raw: Finding | Record<string, unknown>,
|
|
144
169
|
runId: string,
|
|
145
170
|
seq: number,
|
|
171
|
+
options: NormalizeFindingOptions = {},
|
|
146
172
|
): AdapterResult<CanonicalFinding> {
|
|
147
173
|
const diagnostics: Diagnostic[] = []
|
|
148
174
|
const input = isRecord(raw) ? raw : {}
|
|
@@ -189,11 +215,74 @@ export function normalizeToCanonicalFinding(
|
|
|
189
215
|
? (input.source as CanonicalFinding["source"])
|
|
190
216
|
: "manual"
|
|
191
217
|
|
|
218
|
+
const reportedByAgentRaw =
|
|
219
|
+
(typeof input.reported_by_agent === "string" ? input.reported_by_agent : undefined) ??
|
|
220
|
+
(typeof input.reportedByAgent === "string" ? input.reportedByAgent : undefined) ??
|
|
221
|
+
options.reportedByAgent ??
|
|
222
|
+
"unknown"
|
|
223
|
+
const reportedByAgent: ArgusAgentName = VALID_REPORTED_AGENTS.has(
|
|
224
|
+
reportedByAgentRaw as ArgusAgentName,
|
|
225
|
+
)
|
|
226
|
+
? (reportedByAgentRaw as ArgusAgentName)
|
|
227
|
+
: "unknown"
|
|
228
|
+
|
|
229
|
+
const reportedBySessionId =
|
|
230
|
+
(typeof input.reported_by_session_id === "string" && input.reported_by_session_id.length > 0
|
|
231
|
+
? input.reported_by_session_id
|
|
232
|
+
: undefined) ??
|
|
233
|
+
(typeof input.reportedBySessionId === "string" && input.reportedBySessionId.length > 0
|
|
234
|
+
? input.reportedBySessionId
|
|
235
|
+
: undefined) ??
|
|
236
|
+
options.reportedBySessionId
|
|
237
|
+
|
|
238
|
+
const issueFingerprint =
|
|
239
|
+
(typeof input.issue_fingerprint === "string" && input.issue_fingerprint.length > 0
|
|
240
|
+
? input.issue_fingerprint
|
|
241
|
+
: undefined) ??
|
|
242
|
+
(typeof input.issueFingerprint === "string" && input.issueFingerprint.length > 0
|
|
243
|
+
? input.issueFingerprint
|
|
244
|
+
: undefined) ??
|
|
245
|
+
computeIssueFingerprint({
|
|
246
|
+
check,
|
|
247
|
+
file,
|
|
248
|
+
lines: lines ?? [0, 0],
|
|
249
|
+
severity: VALID_SEVERITIES.has(severity) ? severity : "Informational",
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const observationId =
|
|
253
|
+
(typeof input.observation_id === "string" && input.observation_id.length > 0
|
|
254
|
+
? input.observation_id
|
|
255
|
+
: undefined) ??
|
|
256
|
+
(typeof input.observationId === "string" && input.observationId.length > 0
|
|
257
|
+
? input.observationId
|
|
258
|
+
: undefined) ??
|
|
259
|
+
options.observationId ??
|
|
260
|
+
`${runId}:${seq}:${computeObservationFingerprint({
|
|
261
|
+
issueFingerprint,
|
|
262
|
+
source,
|
|
263
|
+
reportedByAgent,
|
|
264
|
+
toolCallId: options.toolCallId,
|
|
265
|
+
sessionId: reportedBySessionId,
|
|
266
|
+
})}`
|
|
267
|
+
|
|
268
|
+
const observationFingerprint =
|
|
269
|
+
(typeof input.observation_fingerprint === "string" && input.observation_fingerprint.length > 0
|
|
270
|
+
? input.observation_fingerprint
|
|
271
|
+
: undefined) ??
|
|
272
|
+
(typeof input.observationFingerprint === "string" && input.observationFingerprint.length > 0
|
|
273
|
+
? input.observationFingerprint
|
|
274
|
+
: undefined) ??
|
|
275
|
+
computeObservationFingerprint({
|
|
276
|
+
issueFingerprint,
|
|
277
|
+
source,
|
|
278
|
+
reportedByAgent,
|
|
279
|
+
toolCallId: options.toolCallId,
|
|
280
|
+
sessionId: reportedBySessionId,
|
|
281
|
+
observationId,
|
|
282
|
+
})
|
|
283
|
+
|
|
192
284
|
const canonical: CanonicalFinding = {
|
|
193
|
-
id:
|
|
194
|
-
typeof input.id === "string" && input.id.length > 0
|
|
195
|
-
? input.id
|
|
196
|
-
: `${check}:${file}:${lines?.[0] ?? 0}`,
|
|
285
|
+
id: observationId,
|
|
197
286
|
check,
|
|
198
287
|
severity: VALID_SEVERITIES.has(severity) ? severity : "Informational",
|
|
199
288
|
confidence: VALID_CONFIDENCES.has(confidence) ? confidence : "Low",
|
|
@@ -201,6 +290,11 @@ export function normalizeToCanonicalFinding(
|
|
|
201
290
|
file,
|
|
202
291
|
lines: lines ?? [0, 0],
|
|
203
292
|
source,
|
|
293
|
+
reported_by_agent: reportedByAgent,
|
|
294
|
+
reported_by_session_id: reportedBySessionId,
|
|
295
|
+
issue_fingerprint: issueFingerprint,
|
|
296
|
+
observation_fingerprint: observationFingerprint,
|
|
297
|
+
observation_id: observationId,
|
|
204
298
|
remediation: typeof input.remediation === "string" ? input.remediation : undefined,
|
|
205
299
|
exploitReference:
|
|
206
300
|
typeof input.exploitReference === "string" ? input.exploitReference : undefined,
|