solidity-argus 0.5.10 → 0.6.1
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/AGENTS.md +8 -1
- package/README.md +27 -21
- package/package.json +2 -2
- package/skills/INVENTORY.md +14 -1
- package/skills/README.md +4 -2
- package/skills/references/attack-vector-deck/SKILL.md +62 -0
- package/skills/specialist-profiles/access-control-specialist/SKILL.md +31 -0
- package/skills/specialist-profiles/economic-security/SKILL.md +31 -0
- package/skills/specialist-profiles/execution-trace/SKILL.md +31 -0
- package/skills/specialist-profiles/first-principles/SKILL.md +31 -0
- package/skills/specialist-profiles/invariant/SKILL.md +31 -0
- package/skills/specialist-profiles/math-precision/SKILL.md +31 -0
- package/skills/specialist-profiles/periphery/SKILL.md +31 -0
- package/skills/specialist-profiles/vector-scan/SKILL.md +28 -0
- package/src/agents/argus-prompt.ts +59 -6
- package/src/agents/audit-specialist-prompt.ts +94 -0
- package/src/agents/pythia-prompt.ts +7 -4
- package/src/agents/scribe-prompt.ts +9 -0
- package/src/agents/sentinel-prompt.ts +12 -0
- package/src/agents/themis-prompt.ts +4 -0
- package/src/config/schema.ts +2 -0
- package/src/constants/defaults.ts +1 -0
- package/src/create-hooks.ts +9 -1
- package/src/features/background-agent/background-manager.ts +85 -2
- package/src/features/persistent-state/run-finalizer.ts +37 -3
- package/src/hooks/config-handler.ts +23 -0
- package/src/hooks/system-prompt-hook.ts +72 -2
- package/src/hooks/tool-tracking-hook.ts +50 -6
- package/src/managers/types.ts +21 -0
- package/src/shared/agent-names.ts +1 -0
- package/src/shared/lineage-validator.ts +96 -0
- package/src/shared/report-path-resolver.ts +8 -2
- package/src/state/adapters.ts +1 -1
- package/src/state/projectors.ts +50 -0
- package/src/state/schemas.ts +86 -1
- package/src/state/types.ts +25 -1
- package/src/tools/forge-coverage-tool.ts +41 -5
- package/src/tools/persist-deduped-tool.ts +45 -1
- package/src/tools/read-findings-tool.ts +46 -5
- package/src/tools/record-finding-tool.ts +10 -30
- package/src/tools/report-generator-tool.ts +135 -37
- package/src/tools/slither-tool.ts +62 -2
|
@@ -35,6 +35,73 @@ export function buildFallbackDirectives(unavailableTools: string[]): string[] {
|
|
|
35
35
|
return directives
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function formatDuration(startTime: number, endTime?: number): string {
|
|
39
|
+
if (typeof endTime !== "number" || endTime < startTime) return "pending"
|
|
40
|
+
return `${endTime - startTime}ms`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildToolLedgerLine(auditState: AuditState): string {
|
|
44
|
+
const taskTools = auditState.toolsExecuted.filter((tool) => tool.tool === "task")
|
|
45
|
+
const taskDispatches = taskTools.length
|
|
46
|
+
const argusTools = auditState.toolsExecuted.filter((tool) => tool.tool !== "task").slice(-5)
|
|
47
|
+
const entries = argusTools.map((tool) => {
|
|
48
|
+
const status = tool.success ? "ok" : "failed"
|
|
49
|
+
return `${tool.tool}=${status} findings=${tool.findingsCount} duration=${formatDuration(tool.startTime, tool.endTime)}`
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (taskDispatches > 0) {
|
|
53
|
+
const bySubagent = new Map<string, number>()
|
|
54
|
+
for (const tool of taskTools) {
|
|
55
|
+
const subagent = tool.subagent_type ?? "unknown"
|
|
56
|
+
bySubagent.set(subagent, (bySubagent.get(subagent) ?? 0) + 1)
|
|
57
|
+
}
|
|
58
|
+
const subagentSummary = [...bySubagent.entries()]
|
|
59
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
60
|
+
.map(([subagent, count]) => `${subagent}=${count}`)
|
|
61
|
+
.join(" ")
|
|
62
|
+
entries.push(
|
|
63
|
+
subagentSummary.length > 0
|
|
64
|
+
? `task dispatches=${taskDispatches} (${subagentSummary})`
|
|
65
|
+
: `task dispatches=${taskDispatches}`,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
return entries.length > 0 ? entries.join("; ") : "none"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildToolsLine(auditState: AuditState): string {
|
|
72
|
+
const tools = auditState.toolsExecuted
|
|
73
|
+
.filter((tool) => tool.tool !== "task")
|
|
74
|
+
.map((tool) => tool.tool)
|
|
75
|
+
return tools.length > 0 ? tools.join(", ") : "none"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildFindingCountsLine(auditState: AuditState): string | null {
|
|
79
|
+
const counts = auditState.findingCounts
|
|
80
|
+
if (!counts) return null
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
"Finding Counts:",
|
|
84
|
+
`raw_observations=${counts.rawObservations ?? 0}`,
|
|
85
|
+
`recorded=${counts.recordedFindings ?? 0}`,
|
|
86
|
+
`deduped=${counts.dedupedFindings ?? 0}`,
|
|
87
|
+
`actionable=${counts.actionableFindings ?? 0}`,
|
|
88
|
+
`non_actionable=${counts.nonActionableFindings ?? 0}`,
|
|
89
|
+
].join(" ")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildCoverageLine(auditState: AuditState): string {
|
|
93
|
+
const attempt = auditState.coverageAttempt
|
|
94
|
+
if (attempt) {
|
|
95
|
+
return attempt.reason
|
|
96
|
+
? `Coverage: ${attempt.status} — ${attempt.reason}`
|
|
97
|
+
: `Coverage: ${attempt.status}`
|
|
98
|
+
}
|
|
99
|
+
const unavailable = auditState.unavailableTools ?? []
|
|
100
|
+
return unavailable.includes("forge")
|
|
101
|
+
? "Coverage: skipped — forge unavailable"
|
|
102
|
+
: "Coverage: pending"
|
|
103
|
+
}
|
|
104
|
+
|
|
38
105
|
export function buildDynamicContext(
|
|
39
106
|
auditState: AuditState,
|
|
40
107
|
agent: string,
|
|
@@ -45,7 +112,7 @@ export function buildDynamicContext(
|
|
|
45
112
|
const executedToolNames = new Set(
|
|
46
113
|
auditState.toolsExecuted.map((t) => TOOL_SHORT_NAMES[t.tool] ?? t.tool),
|
|
47
114
|
)
|
|
48
|
-
const
|
|
115
|
+
const findingCountsLine = buildFindingCountsLine(auditState)
|
|
49
116
|
const taskStatus = KEY_TOOLS.map(
|
|
50
117
|
(t) => `${t}=${executedToolNames.has(t) ? "done" : "pending"}`,
|
|
51
118
|
).join(" ")
|
|
@@ -62,7 +129,10 @@ export function buildDynamicContext(
|
|
|
62
129
|
`Phase: ${auditState.currentPhase}`,
|
|
63
130
|
`Contracts: ${auditState.contractsReviewed.length} reviewed`,
|
|
64
131
|
`Findings: Critical=${severityCounts.Critical} High=${severityCounts.High} Medium=${severityCounts.Medium} Low=${severityCounts.Low} Info=${severityCounts.Informational}`,
|
|
65
|
-
|
|
132
|
+
...(findingCountsLine ? [findingCountsLine] : []),
|
|
133
|
+
`Tools: ${buildToolsLine(auditState)}`,
|
|
134
|
+
`Tool Ledger: ${buildToolLedgerLine(auditState)}`,
|
|
135
|
+
buildCoverageLine(auditState),
|
|
66
136
|
`Tasks: ${taskStatus}`,
|
|
67
137
|
]
|
|
68
138
|
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
ArgusAgentName,
|
|
21
21
|
AuditState,
|
|
22
22
|
Finding,
|
|
23
|
+
FindingCounts,
|
|
23
24
|
FindingSeverity,
|
|
24
25
|
FuzzCounterexample,
|
|
25
26
|
SoloditResult,
|
|
@@ -465,14 +466,38 @@ function processSoloditResult(parsed: Record<string, unknown>, state: AuditState
|
|
|
465
466
|
})
|
|
466
467
|
}
|
|
467
468
|
|
|
468
|
-
function
|
|
469
|
+
function buildFindingCounts(state: AuditState, findingsCount: number): FindingCounts {
|
|
470
|
+
return {
|
|
471
|
+
rawObservations: Math.max(0, findingsCount),
|
|
472
|
+
recordedFindings: state.findings.length,
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function readErrorReason(record: Record<string, unknown>): string | undefined {
|
|
477
|
+
if (typeof record.error === "string" && record.error.trim().length > 0) return record.error
|
|
478
|
+
const errorRecord = toRecord(record.error)
|
|
479
|
+
if (typeof errorRecord?.message === "string" && errorRecord.message.trim().length > 0) {
|
|
480
|
+
return errorRecord.message
|
|
481
|
+
}
|
|
482
|
+
if (typeof record.stderr === "string" && record.stderr.trim().length > 0) return record.stderr
|
|
483
|
+
return undefined
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function recordToolExecution(
|
|
487
|
+
state: AuditState,
|
|
488
|
+
toolName: string,
|
|
489
|
+
findingsCount: number,
|
|
490
|
+
success: boolean,
|
|
491
|
+
findingCounts?: FindingCounts,
|
|
492
|
+
): void {
|
|
469
493
|
const now = Date.now()
|
|
470
494
|
state.toolsExecuted.push({
|
|
471
495
|
tool: toolName,
|
|
472
496
|
startTime: now,
|
|
473
497
|
endTime: now,
|
|
474
|
-
success
|
|
498
|
+
success,
|
|
475
499
|
findingsCount,
|
|
500
|
+
findingCounts,
|
|
476
501
|
})
|
|
477
502
|
}
|
|
478
503
|
|
|
@@ -616,7 +641,7 @@ export function createToolTrackingHook(
|
|
|
616
641
|
}
|
|
617
642
|
|
|
618
643
|
if (resolved) {
|
|
619
|
-
recordToolExecution(resolved.state, "task", 0)
|
|
644
|
+
recordToolExecution(resolved.state, "task", 0, true, buildFindingCounts(resolved.state, 0))
|
|
620
645
|
onStateChanged?.({ tool: "task", findingsCount: 0, sessionId: input.sessionID })
|
|
621
646
|
}
|
|
622
647
|
|
|
@@ -875,9 +900,16 @@ export function createToolTrackingHook(
|
|
|
875
900
|
break
|
|
876
901
|
}
|
|
877
902
|
case "argus_forge_coverage": {
|
|
903
|
+
const now = Date.now()
|
|
878
904
|
const reportObj = toRecord(record.report)
|
|
879
905
|
const files = reportObj?.files
|
|
880
|
-
if (
|
|
906
|
+
if (record.success === false) {
|
|
907
|
+
auditState.coverageAttempt = {
|
|
908
|
+
status: "failed",
|
|
909
|
+
attemptedAt: now,
|
|
910
|
+
reason: readErrorReason(record),
|
|
911
|
+
}
|
|
912
|
+
} else if (Array.isArray(files)) {
|
|
881
913
|
auditState.coverageReport = {
|
|
882
914
|
files: files
|
|
883
915
|
.filter((f): f is Record<string, unknown> => !!f && typeof f === "object")
|
|
@@ -889,6 +921,13 @@ export function createToolTrackingHook(
|
|
|
889
921
|
functionsPct: typeof f.functionsPct === "number" ? f.functionsPct : 0,
|
|
890
922
|
})),
|
|
891
923
|
}
|
|
924
|
+
auditState.coverageAttempt = { status: "run", attemptedAt: now }
|
|
925
|
+
} else {
|
|
926
|
+
auditState.coverageAttempt = {
|
|
927
|
+
status: "failed",
|
|
928
|
+
attemptedAt: now,
|
|
929
|
+
reason: "coverage report was missing or invalid",
|
|
930
|
+
}
|
|
892
931
|
}
|
|
893
932
|
break
|
|
894
933
|
}
|
|
@@ -963,10 +1002,12 @@ export function createToolTrackingHook(
|
|
|
963
1002
|
}
|
|
964
1003
|
}
|
|
965
1004
|
|
|
966
|
-
completedSuccess =
|
|
1005
|
+
completedSuccess = record.success !== false
|
|
967
1006
|
}
|
|
968
1007
|
|
|
969
|
-
|
|
1008
|
+
const findingCounts = buildFindingCounts(auditState, findingsCount)
|
|
1009
|
+
auditState.findingCounts = findingCounts
|
|
1010
|
+
recordToolExecution(auditState, input.tool, findingsCount, completedSuccess, findingCounts)
|
|
970
1011
|
|
|
971
1012
|
const nextPhase = inferPhaseAdvancement(auditState, input.tool)
|
|
972
1013
|
if (nextPhase) {
|
|
@@ -1003,6 +1044,8 @@ export function createToolTrackingHook(
|
|
|
1003
1044
|
break
|
|
1004
1045
|
case "argus_forge_coverage":
|
|
1005
1046
|
if (auditState.coverageReport) enrichment.coverageReport = auditState.coverageReport
|
|
1047
|
+
if (auditState.coverageAttempt)
|
|
1048
|
+
enrichment.coverageAttempt = auditState.coverageAttempt
|
|
1006
1049
|
break
|
|
1007
1050
|
case "argus_gas_analysis":
|
|
1008
1051
|
if (auditState.gasHotspots) enrichment.gasHotspots = auditState.gasHotspots
|
|
@@ -1028,6 +1071,7 @@ export function createToolTrackingHook(
|
|
|
1028
1071
|
buildEvent("tool.completed", runId, sessionId, toolCallId, {
|
|
1029
1072
|
tool: input.tool,
|
|
1030
1073
|
findingsCount,
|
|
1074
|
+
findingCounts: completedSuccess ? auditState.findingCounts : undefined,
|
|
1031
1075
|
success: completedSuccess,
|
|
1032
1076
|
...(completionError ? { error: completionError } : {}),
|
|
1033
1077
|
...enrichment,
|
package/src/managers/types.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import type { AuditState } from "../state/types"
|
|
2
2
|
|
|
3
|
+
export type BackgroundTaskStatus = "queued" | "running" | "completed" | "failed" | "cancelled"
|
|
4
|
+
|
|
5
|
+
export type BackgroundFailureDiagnostic = {
|
|
6
|
+
category: "model_error" | "tool_error" | "timeout" | "cancelled" | "unknown"
|
|
7
|
+
retry_recommendation: "safe_to_retry" | "retry_with_changes" | "do_not_retry"
|
|
8
|
+
summary: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type BackgroundTaskDiagnostic = {
|
|
12
|
+
status: BackgroundTaskStatus
|
|
13
|
+
result?: unknown
|
|
14
|
+
error?: unknown
|
|
15
|
+
diagnostic?: BackgroundFailureDiagnostic
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
/**
|
|
4
19
|
* BackgroundManager interface
|
|
5
20
|
* Handles dispatching and managing background agent tasks
|
|
@@ -27,6 +42,12 @@ export interface BackgroundManager {
|
|
|
27
42
|
*/
|
|
28
43
|
getResult(taskId: string): Promise<unknown>
|
|
29
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Get task status and structured diagnostics for completed, failed, queued, and cancelled tasks.
|
|
47
|
+
* Unknown task IDs resolve to undefined.
|
|
48
|
+
*/
|
|
49
|
+
getTaskStatus(taskId: string): Promise<BackgroundTaskDiagnostic | undefined>
|
|
50
|
+
|
|
30
51
|
/**
|
|
31
52
|
* Register a callback to be invoked when a task completes
|
|
32
53
|
* @param callback - Function called with (taskId, result) when task finishes
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { CanonicalFinding } from "../state/schemas"
|
|
2
|
+
import type { Finding } from "../state/types"
|
|
3
|
+
|
|
4
|
+
export type LineageCountMismatch = {
|
|
5
|
+
check: string
|
|
6
|
+
observation_count?: number
|
|
7
|
+
observation_ids_length: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type FindingLineageResult = {
|
|
11
|
+
valid: boolean
|
|
12
|
+
raw_count: number
|
|
13
|
+
mapped_count: number
|
|
14
|
+
duplicate_observation_ids: string[]
|
|
15
|
+
phantom_observation_ids: string[]
|
|
16
|
+
missing_observation_ids: string[]
|
|
17
|
+
count_mismatches: LineageCountMismatch[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type FindingLike = Pick<Finding, "check"> & {
|
|
21
|
+
id?: string
|
|
22
|
+
observation_id?: string
|
|
23
|
+
observation_ids?: unknown
|
|
24
|
+
observation_count?: unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sorted(values: Iterable<string>): string[] {
|
|
28
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function observationIds(value: FindingLike): string[] {
|
|
32
|
+
if (!Array.isArray(value.observation_ids)) return []
|
|
33
|
+
return value.observation_ids.filter((id): id is string => typeof id === "string" && id.length > 0)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function rawObservationIds(rawFindings: CanonicalFinding[]): string[] {
|
|
37
|
+
return rawFindings
|
|
38
|
+
.map((finding) => finding.observation_id)
|
|
39
|
+
.filter((id): id is string => typeof id === "string" && id.length > 0)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateFindingLineage(
|
|
43
|
+
rawFindings: CanonicalFinding[],
|
|
44
|
+
dedupedFindings: FindingLike[],
|
|
45
|
+
): FindingLineageResult {
|
|
46
|
+
const rawIds = new Set(rawObservationIds(rawFindings))
|
|
47
|
+
const mappedIds: string[] = []
|
|
48
|
+
const seen = new Set<string>()
|
|
49
|
+
const duplicates = new Set<string>()
|
|
50
|
+
const countMismatches: LineageCountMismatch[] = []
|
|
51
|
+
|
|
52
|
+
for (const finding of dedupedFindings) {
|
|
53
|
+
const ids = observationIds(finding)
|
|
54
|
+
const suppliedCount = finding.observation_count
|
|
55
|
+
|
|
56
|
+
if (ids.length === 0 || (suppliedCount != null && suppliedCount !== ids.length)) {
|
|
57
|
+
countMismatches.push({
|
|
58
|
+
check: finding.check || finding.id || "(unknown finding)",
|
|
59
|
+
observation_count: typeof suppliedCount === "number" ? suppliedCount : undefined,
|
|
60
|
+
observation_ids_length: ids.length,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const id of ids) {
|
|
65
|
+
mappedIds.push(id)
|
|
66
|
+
if (seen.has(id)) {
|
|
67
|
+
duplicates.add(id)
|
|
68
|
+
}
|
|
69
|
+
seen.add(id)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const mappedSet = new Set(mappedIds)
|
|
74
|
+
const phantom = mappedIds.filter((id) => !rawIds.has(id))
|
|
75
|
+
const missing = Array.from(rawIds).filter((id) => !mappedSet.has(id))
|
|
76
|
+
const duplicateIds = sorted(duplicates)
|
|
77
|
+
const phantomIds = sorted(phantom)
|
|
78
|
+
const missingIds = sorted(missing)
|
|
79
|
+
|
|
80
|
+
countMismatches.sort((a, b) => a.check.localeCompare(b.check))
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
valid:
|
|
84
|
+
duplicateIds.length === 0 &&
|
|
85
|
+
phantomIds.length === 0 &&
|
|
86
|
+
missingIds.length === 0 &&
|
|
87
|
+
countMismatches.length === 0 &&
|
|
88
|
+
mappedIds.length === rawIds.size,
|
|
89
|
+
raw_count: rawIds.size,
|
|
90
|
+
mapped_count: mappedIds.length,
|
|
91
|
+
duplicate_observation_ids: duplicateIds,
|
|
92
|
+
phantom_observation_ids: phantomIds,
|
|
93
|
+
missing_observation_ids: missingIds,
|
|
94
|
+
count_mismatches: countMismatches,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -16,6 +16,8 @@ export interface ReportPathOptions {
|
|
|
16
16
|
outputDir: string
|
|
17
17
|
/** Optional run_id for run-scoped naming */
|
|
18
18
|
runId?: string
|
|
19
|
+
/** Optional caller-supplied report revision. Base report is revision 1. */
|
|
20
|
+
revision?: number
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export interface ResolvedReportPath {
|
|
@@ -46,7 +48,7 @@ export function sanitizeContractName(name: string): string {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export function resolveReportPath(options: ReportPathOptions): ResolvedReportPath {
|
|
49
|
-
const { contractName, date, outputDir, runId } = options
|
|
51
|
+
const { contractName, date, outputDir, runId, revision } = options
|
|
50
52
|
|
|
51
53
|
if (!contractName || contractName.trim() === "") {
|
|
52
54
|
throw new ReportPathError("contractName must not be empty")
|
|
@@ -54,12 +56,16 @@ export function resolveReportPath(options: ReportPathOptions): ResolvedReportPat
|
|
|
54
56
|
if (!outputDir || outputDir.trim() === "") {
|
|
55
57
|
throw new ReportPathError("outputDir must not be empty")
|
|
56
58
|
}
|
|
59
|
+
if (revision != null && (!Number.isInteger(revision) || revision < 2)) {
|
|
60
|
+
throw new ReportPathError("revision must be an integer greater than or equal to 2")
|
|
61
|
+
}
|
|
57
62
|
|
|
58
63
|
const resolvedDate = date ?? new Date()
|
|
59
64
|
const dateStr = formatReportDate(resolvedDate)
|
|
60
65
|
const sanitizedName = sanitizeContractName(contractName)
|
|
61
66
|
const runIdSuffix = runId ? `-${runId.substring(0, 8)}` : ""
|
|
62
|
-
const
|
|
67
|
+
const revisionSuffix = revision == null ? "" : `-r${revision}`
|
|
68
|
+
const filename = `${sanitizedName}-security-audit-${dateStr}${runIdSuffix}${revisionSuffix}.md`
|
|
63
69
|
const filePath = join(outputDir, filename)
|
|
64
70
|
const canonicalId = runId ?? filename
|
|
65
71
|
|
package/src/state/adapters.ts
CHANGED
|
@@ -246,9 +246,9 @@ export function normalizeToCanonicalFinding(
|
|
|
246
246
|
: "manual"
|
|
247
247
|
|
|
248
248
|
const reportedByAgentRaw =
|
|
249
|
+
options.reportedByAgent ??
|
|
249
250
|
(typeof input.reported_by_agent === "string" ? input.reported_by_agent : undefined) ??
|
|
250
251
|
(typeof input.reportedByAgent === "string" ? input.reportedByAgent : undefined) ??
|
|
251
|
-
options.reportedByAgent ??
|
|
252
252
|
"unknown"
|
|
253
253
|
const reportedByAgent: ArgusAgentName = VALID_AGENTS.has(reportedByAgentRaw as ArgusAgentName)
|
|
254
254
|
? (reportedByAgentRaw as ArgusAgentName)
|
package/src/state/projectors.ts
CHANGED
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
import type {
|
|
13
13
|
AuditPhase,
|
|
14
14
|
AuditState,
|
|
15
|
+
CoverageAttemptState,
|
|
15
16
|
Finding,
|
|
17
|
+
FindingCounts,
|
|
16
18
|
FuzzCounterexample,
|
|
17
19
|
SoloditResult,
|
|
18
20
|
ToolExecution,
|
|
@@ -99,6 +101,48 @@ function resolveToolSuccess(payload: Record<string, unknown>): boolean {
|
|
|
99
101
|
return payload.success !== false
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
const FINDING_COUNT_FIELDS = [
|
|
105
|
+
"rawObservations",
|
|
106
|
+
"recordedFindings",
|
|
107
|
+
"dedupedFindings",
|
|
108
|
+
"actionableFindings",
|
|
109
|
+
"nonActionableFindings",
|
|
110
|
+
] as const
|
|
111
|
+
|
|
112
|
+
function asFindingCounts(value: unknown): FindingCounts | undefined {
|
|
113
|
+
if (!isRecord(value)) return undefined
|
|
114
|
+
const counts: FindingCounts = {}
|
|
115
|
+
for (const field of FINDING_COUNT_FIELDS) {
|
|
116
|
+
const count = value[field]
|
|
117
|
+
if (
|
|
118
|
+
typeof count === "number" &&
|
|
119
|
+
Number.isFinite(count) &&
|
|
120
|
+
Number.isInteger(count) &&
|
|
121
|
+
count >= 0
|
|
122
|
+
) {
|
|
123
|
+
counts[field] = count
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return Object.keys(counts).length > 0 ? counts : undefined
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function asCoverageAttempt(value: unknown): CoverageAttemptState | undefined {
|
|
130
|
+
if (!isRecord(value)) return undefined
|
|
131
|
+
if (
|
|
132
|
+
value.status !== "pending" &&
|
|
133
|
+
value.status !== "run" &&
|
|
134
|
+
value.status !== "skipped" &&
|
|
135
|
+
value.status !== "failed"
|
|
136
|
+
) {
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
status: value.status,
|
|
141
|
+
attemptedAt: typeof value.attemptedAt === "number" ? value.attemptedAt : undefined,
|
|
142
|
+
reason: typeof value.reason === "string" ? value.reason : undefined,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
102
146
|
function asStringArray(value: unknown): string[] | undefined {
|
|
103
147
|
if (!Array.isArray(value)) return undefined
|
|
104
148
|
return value.filter((item): item is string => typeof item === "string")
|
|
@@ -321,6 +365,7 @@ export function projectToolExecutions(events: AuditEvent[]): CanonicalToolExecut
|
|
|
321
365
|
endTime: existing?.endTime,
|
|
322
366
|
success: existing?.success ?? false,
|
|
323
367
|
findingsCount: existing?.findingsCount ?? 0,
|
|
368
|
+
findingCounts: existing?.findingCounts,
|
|
324
369
|
})
|
|
325
370
|
continue
|
|
326
371
|
}
|
|
@@ -340,6 +385,7 @@ export function projectToolExecutions(events: AuditEvent[]): CanonicalToolExecut
|
|
|
340
385
|
endTime: event.timestamp,
|
|
341
386
|
success: resolveToolSuccess(payload),
|
|
342
387
|
findingsCount: resolveFindingsCount(payload),
|
|
388
|
+
findingCounts: asFindingCounts(payload.findingCounts),
|
|
343
389
|
run_id: event.run_id,
|
|
344
390
|
schema_version: event.schema_version,
|
|
345
391
|
})
|
|
@@ -408,6 +454,8 @@ export function projectReportInput(
|
|
|
408
454
|
asFuzzCounterexamples,
|
|
409
455
|
)
|
|
410
456
|
const coverageReport = extractLatestFromPayload(events, "coverageReport", asCoverageReport)
|
|
457
|
+
const coverageAttempt = extractLatestFromPayload(events, "coverageAttempt", asCoverageAttempt)
|
|
458
|
+
const findingCounts = extractLatestFromPayload(events, "findingCounts", asFindingCounts)
|
|
411
459
|
const gasHotspots = extractLatestFromPayload(events, "gasHotspots", asGasHotspots)
|
|
412
460
|
const proxyContracts = extractLatestFromPayload(events, "proxyContracts", asProxyContracts)
|
|
413
461
|
const patternVersion = extractLatestFromPayload(events, "patternVersion", asString)
|
|
@@ -424,10 +472,12 @@ export function projectReportInput(
|
|
|
424
472
|
projectDir,
|
|
425
473
|
findings,
|
|
426
474
|
toolsExecuted,
|
|
475
|
+
findingCounts,
|
|
427
476
|
scope,
|
|
428
477
|
soloditResults,
|
|
429
478
|
fuzzCounterexamples,
|
|
430
479
|
coverageReport,
|
|
480
|
+
coverageAttempt,
|
|
431
481
|
gasHotspots,
|
|
432
482
|
proxyContracts,
|
|
433
483
|
patternVersion,
|
package/src/state/schemas.ts
CHANGED
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
import type {
|
|
9
9
|
ArgusAgentName,
|
|
10
10
|
AuditPhase,
|
|
11
|
+
CoverageAttemptState,
|
|
11
12
|
Finding,
|
|
13
|
+
FindingCounts,
|
|
12
14
|
FindingSeverity,
|
|
13
15
|
FuzzCounterexample,
|
|
14
16
|
SoloditResult,
|
|
@@ -111,6 +113,7 @@ export interface ReportInput {
|
|
|
111
113
|
projectDir: string
|
|
112
114
|
findings: CanonicalFinding[]
|
|
113
115
|
toolsExecuted: CanonicalToolExecution[]
|
|
116
|
+
findingCounts?: FindingCounts
|
|
114
117
|
scope: string[]
|
|
115
118
|
soloditResults?: SoloditResult[]
|
|
116
119
|
fuzzCounterexamples?: FuzzCounterexample[]
|
|
@@ -120,6 +123,82 @@ export interface ReportInput {
|
|
|
120
123
|
patternVersion?: string
|
|
121
124
|
skillsLoaded?: string[]
|
|
122
125
|
unavailableTools?: string[]
|
|
126
|
+
coverageAttempt?: CoverageAttemptState
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const FINDING_COUNT_FIELDS = [
|
|
130
|
+
"rawObservations",
|
|
131
|
+
"recordedFindings",
|
|
132
|
+
"dedupedFindings",
|
|
133
|
+
"actionableFindings",
|
|
134
|
+
"nonActionableFindings",
|
|
135
|
+
] as const
|
|
136
|
+
|
|
137
|
+
const COVERAGE_ATTEMPT_STATUSES = new Set(["pending", "run", "skipped", "failed"])
|
|
138
|
+
|
|
139
|
+
function pushFindingCountsErrors(errors: ValidationError[], raw: unknown, prefix: string): void {
|
|
140
|
+
if (raw == null) return
|
|
141
|
+
if (!isRecord(raw)) {
|
|
142
|
+
errors.push({
|
|
143
|
+
field: prefix,
|
|
144
|
+
code: "invalid",
|
|
145
|
+
message: `${prefix} must be an object when provided`,
|
|
146
|
+
})
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const field of FINDING_COUNT_FIELDS) {
|
|
151
|
+
const value = raw[field]
|
|
152
|
+
if (value == null) continue
|
|
153
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
154
|
+
errors.push({
|
|
155
|
+
field: `${prefix}.${field}`,
|
|
156
|
+
code: "invalid",
|
|
157
|
+
message: `${prefix}.${field} must be a non-negative integer when provided`,
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function pushCoverageAttemptErrors(errors: ValidationError[], raw: unknown): void {
|
|
164
|
+
if (raw == null) return
|
|
165
|
+
if (!isRecord(raw)) {
|
|
166
|
+
errors.push({
|
|
167
|
+
field: "coverageAttempt",
|
|
168
|
+
code: "invalid",
|
|
169
|
+
message: "coverageAttempt must be an object when provided",
|
|
170
|
+
})
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (typeof raw.status !== "string" || !COVERAGE_ATTEMPT_STATUSES.has(raw.status)) {
|
|
175
|
+
errors.push({
|
|
176
|
+
field: "coverageAttempt.status",
|
|
177
|
+
code: "enum",
|
|
178
|
+
message: "coverageAttempt.status must be one of: pending, run, skipped, failed",
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
raw.attemptedAt != null &&
|
|
184
|
+
(typeof raw.attemptedAt !== "number" ||
|
|
185
|
+
!Number.isInteger(raw.attemptedAt) ||
|
|
186
|
+
raw.attemptedAt <= 0)
|
|
187
|
+
) {
|
|
188
|
+
errors.push({
|
|
189
|
+
field: "coverageAttempt.attemptedAt",
|
|
190
|
+
code: "invalid",
|
|
191
|
+
message: "coverageAttempt.attemptedAt must be a positive integer when provided",
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (raw.reason != null && (typeof raw.reason !== "string" || raw.reason.trim().length === 0)) {
|
|
196
|
+
errors.push({
|
|
197
|
+
field: "coverageAttempt.reason",
|
|
198
|
+
code: "invalid",
|
|
199
|
+
message: "coverageAttempt.reason must be a non-empty string when provided",
|
|
200
|
+
})
|
|
201
|
+
}
|
|
123
202
|
}
|
|
124
203
|
|
|
125
204
|
function pushRequiredRootStringError(
|
|
@@ -253,7 +332,8 @@ export function validateCanonicalFinding(raw: unknown): ValidationResult<Canonic
|
|
|
253
332
|
errors.push({
|
|
254
333
|
field: "reported_by_agent",
|
|
255
334
|
code: "enum",
|
|
256
|
-
message:
|
|
335
|
+
message:
|
|
336
|
+
"reported_by_agent must be one of: argus, sentinel, pythia, audit-specialist, scribe, unknown",
|
|
257
337
|
})
|
|
258
338
|
}
|
|
259
339
|
|
|
@@ -346,6 +426,8 @@ export function validateCanonicalToolExecution(
|
|
|
346
426
|
})
|
|
347
427
|
}
|
|
348
428
|
|
|
429
|
+
pushFindingCountsErrors(errors, raw.findingCounts, "findingCounts")
|
|
430
|
+
|
|
349
431
|
if (typeof raw.run_id !== "string" || raw.run_id.trim().length === 0) {
|
|
350
432
|
errors.push({
|
|
351
433
|
field: "run_id",
|
|
@@ -400,6 +482,9 @@ export function validateReportInput(raw: unknown): ValidationResult<ReportInput>
|
|
|
400
482
|
})
|
|
401
483
|
}
|
|
402
484
|
|
|
485
|
+
pushFindingCountsErrors(errors, raw.findingCounts, "findingCounts")
|
|
486
|
+
pushCoverageAttemptErrors(errors, raw.coverageAttempt)
|
|
487
|
+
|
|
403
488
|
if (!Array.isArray(raw.scope) || !raw.scope.every((item) => typeof item === "string")) {
|
|
404
489
|
errors.push({
|
|
405
490
|
field: "scope",
|
package/src/state/types.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export type FindingSeverity = "Critical" | "High" | "Medium" | "Low" | "Informational"
|
|
2
|
-
export type ArgusAgentName =
|
|
2
|
+
export type ArgusAgentName =
|
|
3
|
+
| "argus"
|
|
4
|
+
| "sentinel"
|
|
5
|
+
| "pythia"
|
|
6
|
+
| "audit-specialist"
|
|
7
|
+
| "scribe"
|
|
8
|
+
| "unknown"
|
|
3
9
|
export type AuditPhase =
|
|
4
10
|
| "reconnaissance"
|
|
5
11
|
| "scanning"
|
|
@@ -88,6 +94,22 @@ export interface ToolExecution {
|
|
|
88
94
|
endTime?: number
|
|
89
95
|
success: boolean
|
|
90
96
|
findingsCount: number
|
|
97
|
+
findingCounts?: FindingCounts
|
|
98
|
+
subagent_type?: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface FindingCounts {
|
|
102
|
+
rawObservations?: number
|
|
103
|
+
recordedFindings?: number
|
|
104
|
+
dedupedFindings?: number
|
|
105
|
+
actionableFindings?: number
|
|
106
|
+
nonActionableFindings?: number
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface CoverageAttemptState {
|
|
110
|
+
status: "pending" | "run" | "skipped" | "failed"
|
|
111
|
+
attemptedAt?: number
|
|
112
|
+
reason?: string
|
|
91
113
|
}
|
|
92
114
|
|
|
93
115
|
export interface AuditState {
|
|
@@ -105,7 +127,9 @@ export interface AuditState {
|
|
|
105
127
|
skillsLoaded?: string[]
|
|
106
128
|
unavailableTools?: string[]
|
|
107
129
|
reportGenerated?: boolean
|
|
130
|
+
findingCounts?: FindingCounts
|
|
108
131
|
knowledgeSynced?: { success: boolean; timestamp: number }
|
|
132
|
+
coverageAttempt?: CoverageAttemptState
|
|
109
133
|
coverageReport?: {
|
|
110
134
|
files: Array<{
|
|
111
135
|
path: string
|