solidity-argus 0.2.0 → 0.3.2
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 +3 -3
- package/README.md +93 -37
- package/package.json +34 -7
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +26 -23
- package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
- package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
- package/skills/case-studies/cream-finance/SKILL.md +52 -0
- package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
- package/skills/case-studies/dao-hack/SKILL.md +51 -0
- package/skills/case-studies/euler-finance/SKILL.md +52 -0
- package/skills/case-studies/harvest-finance/SKILL.md +52 -0
- package/skills/case-studies/level-finance/SKILL.md +51 -0
- package/skills/case-studies/mango-markets/SKILL.md +53 -0
- package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
- package/skills/case-studies/parity-multisig/SKILL.md +55 -0
- package/skills/case-studies/poly-network/SKILL.md +51 -0
- package/skills/case-studies/rari-fuse/SKILL.md +51 -0
- package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
- package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
- package/skills/manifests/smartbugs.json +1 -3
- package/skills/manifests/sunweb3sec.json +1 -3
- package/skills/vulnerability-patterns/access-control/SKILL.md +14 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +2 -1
- package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +2 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
- package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
- package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +1 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
- package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
- package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
- package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
- package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +9 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +2 -1
- package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +2 -1
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
- package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
- package/src/agents/argus-prompt.ts +34 -7
- package/src/agents/pythia-prompt.ts +13 -4
- package/src/agents/scribe-prompt.ts +20 -2
- package/src/agents/sentinel-prompt.ts +45 -5
- package/src/cli/cli-program.ts +29 -26
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +48 -26
- package/src/cli/commands/init.ts +5 -3
- package/src/cli/commands/install.ts +7 -5
- package/src/cli/commands/lint-skills.ts +16 -12
- package/src/cli/index.ts +5 -5
- package/src/cli/types.ts +3 -3
- package/src/config/index.ts +1 -1
- package/src/config/loader.ts +4 -6
- package/src/config/schema.ts +6 -5
- package/src/config/types.ts +2 -2
- package/src/constants/defaults.ts +2 -0
- package/src/create-hooks.ts +145 -34
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +13 -9
- package/src/features/background-agent/background-manager.ts +93 -87
- package/src/features/background-agent/index.ts +1 -1
- package/src/features/context-monitor/context-monitor.ts +3 -3
- package/src/features/context-monitor/index.ts +2 -2
- package/src/features/error-recovery/session-recovery.ts +2 -4
- package/src/features/error-recovery/tool-error-recovery.ts +12 -7
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +143 -60
- package/src/features/persistent-state/global-run-index.ts +38 -0
- package/src/features/persistent-state/index.ts +1 -1
- package/src/features/persistent-state/run-journal.ts +86 -0
- package/src/hooks/config-handler.ts +28 -11
- package/src/hooks/context-budget.ts +2 -5
- package/src/hooks/event-hook.ts +47 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +18 -21
- package/src/hooks/recon-context-builder.ts +2 -2
- package/src/hooks/safe-create-hook.ts +6 -7
- package/src/hooks/system-prompt-hook.ts +18 -1
- package/src/hooks/tool-tracking-hook.ts +110 -51
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +24 -37
- package/src/knowledge/retry.ts +22 -22
- package/src/knowledge/scvd-client.ts +88 -95
- package/src/knowledge/scvd-errors.ts +35 -35
- package/src/knowledge/scvd-index.ts +78 -80
- package/src/knowledge/scvd-sync.ts +106 -101
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +7 -9
- package/src/shared/binary-utils.ts +44 -35
- package/src/shared/deep-merge.ts +55 -36
- package/src/shared/file-utils.ts +21 -19
- package/src/shared/index.ts +11 -5
- package/src/shared/jsonc-parser.ts +123 -28
- package/src/shared/logger.ts +16 -3
- package/src/shared/project-utils.ts +30 -0
- package/src/skills/analysis/cluster.ts +414 -0
- package/src/skills/analysis/gates.ts +227 -0
- package/src/skills/analysis/index.ts +33 -0
- package/src/skills/analysis/normalize.ts +217 -0
- package/src/skills/analysis/similarity.ts +224 -0
- package/src/skills/argus-skill-resolver.ts +17 -6
- package/src/skills/skill-schema.ts +11 -10
- package/src/solodit-lifecycle.ts +203 -0
- package/src/state/audit-state.ts +8 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +88 -67
- package/src/tools/argus-skill-load-tool.ts +12 -7
- package/src/tools/contract-analyzer-tool.ts +142 -77
- package/src/tools/forge-coverage-tool.ts +226 -0
- package/src/tools/forge-fuzz-tool.ts +127 -127
- package/src/tools/forge-test-tool.ts +201 -158
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +203 -191
- package/src/tools/pattern-loader.ts +5 -111
- package/src/tools/pattern-schema.ts +3 -0
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +305 -206
- package/src/tools/slither-tool.ts +266 -218
- package/src/tools/solodit-search-tool.ts +235 -119
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +28 -29
- package/src/utils/dependency-scanner.ts +37 -37
- package/src/utils/project-detector.ts +111 -124
- package/src/utils/solidity-parser.ts +175 -75
- package/skills/patterns/access-control.yaml +0 -31
- package/skills/patterns/erc4626.yaml +0 -29
- package/skills/patterns/flash-loan.yaml +0 -20
- package/skills/patterns/oracle.yaml +0 -30
- package/skills/patterns/proxy.yaml +0 -30
- package/skills/patterns/reentrancy.yaml +0 -30
- package/skills/patterns/signature.yaml +0 -31
- package/src/hooks/event-hook-v2.ts +0 -99
- package/src/state/plugin-state.ts +0 -14
|
@@ -1,37 +1,41 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
3
|
+
import { loadArgusConfig } from "../config/loader"
|
|
4
|
+
import type { ArgusConfig } from "../config/types"
|
|
5
|
+
import { createLogger } from "../shared/logger"
|
|
6
|
+
import { resolveProjectDir } from "../shared/project-utils"
|
|
7
|
+
import type { AuditState, Finding, FindingSeverity } from "../state/types"
|
|
3
8
|
|
|
4
|
-
type SeverityThreshold = "critical" | "high" | "medium" | "low" | "informational"
|
|
9
|
+
type SeverityThreshold = "critical" | "high" | "medium" | "low" | "informational"
|
|
5
10
|
|
|
6
11
|
type ReportGeneratorArgs = {
|
|
7
|
-
project_name: string
|
|
8
|
-
scope: string[]
|
|
9
|
-
include_executive_summary?: boolean
|
|
10
|
-
severity_threshold?: SeverityThreshold
|
|
11
|
-
audit_state: string
|
|
12
|
-
}
|
|
12
|
+
project_name: string
|
|
13
|
+
scope: string[]
|
|
14
|
+
include_executive_summary?: boolean
|
|
15
|
+
severity_threshold?: SeverityThreshold
|
|
16
|
+
audit_state: string
|
|
17
|
+
}
|
|
13
18
|
|
|
14
19
|
type FindingsCount = {
|
|
15
|
-
critical: number
|
|
16
|
-
high: number
|
|
17
|
-
medium: number
|
|
18
|
-
low: number
|
|
19
|
-
informational: number
|
|
20
|
-
}
|
|
20
|
+
critical: number
|
|
21
|
+
high: number
|
|
22
|
+
medium: number
|
|
23
|
+
low: number
|
|
24
|
+
informational: number
|
|
25
|
+
}
|
|
21
26
|
|
|
22
27
|
export type ReportGenerationResult = {
|
|
23
|
-
report: string
|
|
24
|
-
findingsCount: FindingsCount
|
|
25
|
-
filename: string
|
|
26
|
-
|
|
28
|
+
report: string
|
|
29
|
+
findingsCount: FindingsCount
|
|
30
|
+
filename: string
|
|
31
|
+
filePath?: string
|
|
32
|
+
}
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"Informational",
|
|
34
|
-
];
|
|
34
|
+
export type ReportGenerationDependencies = {
|
|
35
|
+
loadConfig?: (projectDir: string) => ArgusConfig
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SEVERITY_ORDER: FindingSeverity[] = ["Critical", "High", "Medium", "Low", "Informational"]
|
|
35
39
|
|
|
36
40
|
const SEVERITY_PREFIX: Record<FindingSeverity, string> = {
|
|
37
41
|
Critical: "CRIT",
|
|
@@ -39,7 +43,7 @@ const SEVERITY_PREFIX: Record<FindingSeverity, string> = {
|
|
|
39
43
|
Medium: "MED",
|
|
40
44
|
Low: "LOW",
|
|
41
45
|
Informational: "INFO",
|
|
42
|
-
}
|
|
46
|
+
}
|
|
43
47
|
|
|
44
48
|
const THRESHOLD_WEIGHT: Record<SeverityThreshold, number> = {
|
|
45
49
|
critical: 5,
|
|
@@ -47,7 +51,7 @@ const THRESHOLD_WEIGHT: Record<SeverityThreshold, number> = {
|
|
|
47
51
|
medium: 3,
|
|
48
52
|
low: 2,
|
|
49
53
|
informational: 1,
|
|
50
|
-
}
|
|
54
|
+
}
|
|
51
55
|
|
|
52
56
|
const FINDING_WEIGHT: Record<FindingSeverity, number> = {
|
|
53
57
|
Critical: 5,
|
|
@@ -55,7 +59,7 @@ const FINDING_WEIGHT: Record<FindingSeverity, number> = {
|
|
|
55
59
|
Medium: 3,
|
|
56
60
|
Low: 2,
|
|
57
61
|
Informational: 1,
|
|
58
|
-
}
|
|
62
|
+
}
|
|
59
63
|
|
|
60
64
|
function emptyCounts(): FindingsCount {
|
|
61
65
|
return {
|
|
@@ -64,7 +68,7 @@ function emptyCounts(): FindingsCount {
|
|
|
64
68
|
medium: 0,
|
|
65
69
|
low: 0,
|
|
66
70
|
informational: 0,
|
|
67
|
-
}
|
|
71
|
+
}
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
function emptyAuditState(findings: Finding[] = []): AuditState {
|
|
@@ -77,164 +81,247 @@ function emptyAuditState(findings: Finding[] = []): AuditState {
|
|
|
77
81
|
currentPhase: "complete",
|
|
78
82
|
scope: [],
|
|
79
83
|
startTime: 0,
|
|
80
|
-
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hasMinimumFindingFields(
|
|
88
|
+
f: unknown,
|
|
89
|
+
): f is { check: string; file: string; lines: [number, number] } {
|
|
90
|
+
if (typeof f !== "object" || f === null) return false
|
|
91
|
+
const obj = f as Record<string, unknown>
|
|
92
|
+
return (
|
|
93
|
+
typeof obj.check === "string" &&
|
|
94
|
+
obj.check.length > 0 &&
|
|
95
|
+
typeof obj.file === "string" &&
|
|
96
|
+
Array.isArray(obj.lines) &&
|
|
97
|
+
obj.lines.length === 2
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const VALID_SEVERITIES: ReadonlySet<string> = new Set([
|
|
102
|
+
"Critical",
|
|
103
|
+
"High",
|
|
104
|
+
"Medium",
|
|
105
|
+
"Low",
|
|
106
|
+
"Informational",
|
|
107
|
+
])
|
|
108
|
+
const VALID_SOURCES: ReadonlySet<string> = new Set([
|
|
109
|
+
"slither",
|
|
110
|
+
"manual",
|
|
111
|
+
"pattern",
|
|
112
|
+
"scvd",
|
|
113
|
+
"solodit",
|
|
114
|
+
"fuzz",
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
function normalizeFinding(f: Record<string, unknown>): Finding {
|
|
118
|
+
const severity =
|
|
119
|
+
typeof f.severity === "string" && VALID_SEVERITIES.has(f.severity)
|
|
120
|
+
? (f.severity as Finding["severity"])
|
|
121
|
+
: "Informational"
|
|
122
|
+
const confidence =
|
|
123
|
+
typeof f.confidence === "string" && ["High", "Medium", "Low"].includes(f.confidence)
|
|
124
|
+
? (f.confidence as Finding["confidence"])
|
|
125
|
+
: "Low"
|
|
126
|
+
const source =
|
|
127
|
+
typeof f.source === "string" && VALID_SOURCES.has(f.source)
|
|
128
|
+
? (f.source as Finding["source"])
|
|
129
|
+
: "manual"
|
|
130
|
+
const description = typeof f.description === "string" ? f.description : (f.check as string)
|
|
131
|
+
const id = typeof f.id === "string" ? f.id : `${f.check}:${f.file}:${(f.lines as number[])[0]}`
|
|
132
|
+
return {
|
|
133
|
+
id,
|
|
134
|
+
check: f.check as string,
|
|
135
|
+
severity,
|
|
136
|
+
confidence,
|
|
137
|
+
description,
|
|
138
|
+
file: f.file as string,
|
|
139
|
+
lines: f.lines as [number, number],
|
|
140
|
+
source,
|
|
141
|
+
remediation: typeof f.remediation === "string" ? f.remediation : undefined,
|
|
142
|
+
exploitReference: typeof f.exploitReference === "string" ? f.exploitReference : undefined,
|
|
143
|
+
}
|
|
81
144
|
}
|
|
82
145
|
|
|
83
146
|
export function parseAuditState(auditState: string): AuditState {
|
|
84
|
-
let parsed: unknown
|
|
147
|
+
let parsed: unknown
|
|
85
148
|
try {
|
|
86
|
-
parsed = JSON.parse(auditState)
|
|
149
|
+
parsed = JSON.parse(auditState)
|
|
87
150
|
} catch {
|
|
88
|
-
throw new Error(
|
|
151
|
+
throw new Error(
|
|
152
|
+
"audit_state is not valid JSON — expected an AuditState object or Finding[] array",
|
|
153
|
+
)
|
|
89
154
|
}
|
|
90
155
|
|
|
91
156
|
if (Array.isArray(parsed)) {
|
|
92
|
-
|
|
157
|
+
const validFindings = (parsed as unknown[])
|
|
158
|
+
.filter(hasMinimumFindingFields)
|
|
159
|
+
.map((f) => normalizeFinding(f as Record<string, unknown>))
|
|
160
|
+
return emptyAuditState(validFindings)
|
|
93
161
|
}
|
|
94
162
|
|
|
95
|
-
if (
|
|
96
|
-
|
|
163
|
+
if (
|
|
164
|
+
typeof parsed === "object" &&
|
|
165
|
+
parsed !== null &&
|
|
166
|
+
Array.isArray((parsed as AuditState).findings)
|
|
167
|
+
) {
|
|
168
|
+
const state = parsed as AuditState
|
|
169
|
+
const validFindings = state.findings
|
|
170
|
+
.filter(hasMinimumFindingFields)
|
|
171
|
+
.map((f) => normalizeFinding(f as unknown as Record<string, unknown>))
|
|
97
172
|
return {
|
|
98
173
|
...emptyAuditState(),
|
|
99
174
|
...state,
|
|
100
|
-
|
|
175
|
+
findings: validFindings,
|
|
176
|
+
}
|
|
101
177
|
}
|
|
102
178
|
|
|
103
|
-
return emptyAuditState()
|
|
179
|
+
return emptyAuditState()
|
|
104
180
|
}
|
|
105
181
|
|
|
106
182
|
function normalizeTitle(check: string): string {
|
|
183
|
+
if (!check || typeof check !== "string") return "Unknown Check"
|
|
107
184
|
return check
|
|
108
185
|
.split(/[-_\s]+/)
|
|
109
186
|
.filter((part) => part.length > 0)
|
|
110
187
|
.map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
|
|
111
|
-
.join(" ")
|
|
188
|
+
.join(" ")
|
|
112
189
|
}
|
|
113
190
|
|
|
114
191
|
function formatLocation(finding: Finding): string {
|
|
115
|
-
|
|
192
|
+
if (!finding.file || !Array.isArray(finding.lines) || finding.lines.length < 2)
|
|
193
|
+
return "unknown location"
|
|
194
|
+
return `${finding.file}:${finding.lines[0]}-${finding.lines[1]}`
|
|
116
195
|
}
|
|
117
196
|
|
|
118
197
|
function shouldIncludeFinding(finding: Finding, threshold: SeverityThreshold): boolean {
|
|
119
|
-
return FINDING_WEIGHT[finding.severity] >= THRESHOLD_WEIGHT[threshold]
|
|
198
|
+
return FINDING_WEIGHT[finding.severity] >= THRESHOLD_WEIGHT[threshold]
|
|
120
199
|
}
|
|
121
200
|
|
|
122
201
|
function calculateCounts(findings: Finding[]): FindingsCount {
|
|
123
|
-
const counts = emptyCounts()
|
|
202
|
+
const counts = emptyCounts()
|
|
124
203
|
|
|
125
204
|
for (const finding of findings) {
|
|
126
|
-
if (finding.severity === "Critical") counts.critical += 1
|
|
127
|
-
if (finding.severity === "High") counts.high += 1
|
|
128
|
-
if (finding.severity === "Medium") counts.medium += 1
|
|
129
|
-
if (finding.severity === "Low") counts.low += 1
|
|
130
|
-
if (finding.severity === "Informational") counts.informational += 1
|
|
205
|
+
if (finding.severity === "Critical") counts.critical += 1
|
|
206
|
+
if (finding.severity === "High") counts.high += 1
|
|
207
|
+
if (finding.severity === "Medium") counts.medium += 1
|
|
208
|
+
if (finding.severity === "Low") counts.low += 1
|
|
209
|
+
if (finding.severity === "Informational") counts.informational += 1
|
|
131
210
|
}
|
|
132
211
|
|
|
133
|
-
return counts
|
|
212
|
+
return counts
|
|
134
213
|
}
|
|
135
214
|
|
|
136
215
|
function overallRiskAssessment(counts: FindingsCount): string {
|
|
137
|
-
if (counts.critical > 0) return "Critical risk"
|
|
138
|
-
if (counts.high > 0) return "High risk"
|
|
139
|
-
if (counts.medium > 0) return "Medium risk"
|
|
140
|
-
if (counts.low > 0) return "Low risk"
|
|
141
|
-
if (counts.informational > 0) return "Informational only"
|
|
142
|
-
return "No significant risk identified"
|
|
216
|
+
if (counts.critical > 0) return "Critical risk"
|
|
217
|
+
if (counts.high > 0) return "High risk"
|
|
218
|
+
if (counts.medium > 0) return "Medium risk"
|
|
219
|
+
if (counts.low > 0) return "Low risk"
|
|
220
|
+
if (counts.informational > 0) return "Informational only"
|
|
221
|
+
return "No significant risk identified"
|
|
143
222
|
}
|
|
144
223
|
|
|
145
224
|
function genericImpact(severity: FindingSeverity): string {
|
|
146
225
|
if (severity === "Critical") {
|
|
147
|
-
return "Could lead to immediate and severe compromise of funds or protocol control."
|
|
226
|
+
return "Could lead to immediate and severe compromise of funds or protocol control."
|
|
148
227
|
}
|
|
149
228
|
if (severity === "High") {
|
|
150
|
-
return "Could materially impact protocol security, user funds, or system integrity."
|
|
229
|
+
return "Could materially impact protocol security, user funds, or system integrity."
|
|
151
230
|
}
|
|
152
231
|
if (severity === "Medium") {
|
|
153
|
-
return "Could cause operational issues or increase exploitability under specific conditions."
|
|
232
|
+
return "Could cause operational issues or increase exploitability under specific conditions."
|
|
154
233
|
}
|
|
155
234
|
if (severity === "Low") {
|
|
156
|
-
return "Limited direct impact but should be addressed to improve security posture."
|
|
235
|
+
return "Limited direct impact but should be addressed to improve security posture."
|
|
157
236
|
}
|
|
158
|
-
return "No immediate exploit impact, but useful for hardening and maintainability."
|
|
237
|
+
return "No immediate exploit impact, but useful for hardening and maintainability."
|
|
159
238
|
}
|
|
160
239
|
|
|
161
240
|
function genericRecommendation(severity: FindingSeverity): string {
|
|
162
241
|
if (severity === "Critical" || severity === "High") {
|
|
163
|
-
return "Prioritize remediation before production deployment and validate with focused regression tests."
|
|
242
|
+
return "Prioritize remediation before production deployment and validate with focused regression tests."
|
|
164
243
|
}
|
|
165
244
|
if (severity === "Medium") {
|
|
166
|
-
return "Address in the near term and include unit/integration tests to prevent regressions."
|
|
245
|
+
return "Address in the near term and include unit/integration tests to prevent regressions."
|
|
167
246
|
}
|
|
168
247
|
if (severity === "Low") {
|
|
169
|
-
return "Schedule remediation in regular hardening cycles."
|
|
248
|
+
return "Schedule remediation in regular hardening cycles."
|
|
170
249
|
}
|
|
171
|
-
return "Track and resolve during routine code quality and documentation improvements."
|
|
250
|
+
return "Track and resolve during routine code quality and documentation improvements."
|
|
172
251
|
}
|
|
173
252
|
|
|
174
253
|
function buildRecommendations(counts: FindingsCount): string[] {
|
|
175
|
-
const items: string[] = []
|
|
254
|
+
const items: string[] = []
|
|
176
255
|
|
|
177
256
|
if (counts.critical > 0) {
|
|
178
|
-
items.push(
|
|
257
|
+
items.push(
|
|
258
|
+
"1. Immediately remediate all Critical findings and block release until fixes are verified.",
|
|
259
|
+
)
|
|
179
260
|
}
|
|
180
261
|
if (counts.high > 0) {
|
|
181
|
-
items.push(
|
|
262
|
+
items.push(
|
|
263
|
+
"2. Prioritize High findings in the next patch cycle with dedicated security test coverage.",
|
|
264
|
+
)
|
|
182
265
|
}
|
|
183
266
|
if (counts.medium > 0) {
|
|
184
|
-
items.push("3. Resolve Medium findings to reduce attack surface and improve resilience.")
|
|
267
|
+
items.push("3. Resolve Medium findings to reduce attack surface and improve resilience.")
|
|
185
268
|
}
|
|
186
269
|
if (counts.low > 0 || counts.informational > 0) {
|
|
187
|
-
items.push(
|
|
270
|
+
items.push(
|
|
271
|
+
"4. Address Low/Informational findings as part of ongoing hardening and code quality efforts.",
|
|
272
|
+
)
|
|
188
273
|
}
|
|
189
274
|
|
|
190
275
|
if (items.length === 0) {
|
|
191
|
-
items.push(
|
|
276
|
+
items.push(
|
|
277
|
+
"1. Maintain current controls, monitor code changes, and re-audit before major upgrades.",
|
|
278
|
+
)
|
|
192
279
|
}
|
|
193
280
|
|
|
194
|
-
return items
|
|
281
|
+
return items
|
|
195
282
|
}
|
|
196
283
|
|
|
197
284
|
function buildFindingsSection(findings: Finding[]): string {
|
|
198
285
|
if (findings.length === 0) {
|
|
199
|
-
return "## Findings\nNo findings meet the configured severity threshold."
|
|
286
|
+
return "## Findings\nNo findings meet the configured severity threshold."
|
|
200
287
|
}
|
|
201
288
|
|
|
202
|
-
const lines: string[] = ["## Findings"]
|
|
289
|
+
const lines: string[] = ["## Findings"]
|
|
203
290
|
|
|
204
291
|
for (const severity of SEVERITY_ORDER) {
|
|
205
|
-
const severityFindings = findings.filter((finding) => finding.severity === severity)
|
|
292
|
+
const severityFindings = findings.filter((finding) => finding.severity === severity)
|
|
206
293
|
if (severityFindings.length === 0) {
|
|
207
|
-
continue
|
|
294
|
+
continue
|
|
208
295
|
}
|
|
209
296
|
|
|
210
|
-
lines.push(`### ${severity}`)
|
|
297
|
+
lines.push(`### ${severity}`)
|
|
211
298
|
|
|
212
299
|
severityFindings.forEach((finding, index) => {
|
|
213
|
-
const prefix = SEVERITY_PREFIX[severity]
|
|
214
|
-
const findingId = `[${prefix}-${index + 1}]
|
|
215
|
-
const title = normalizeTitle(finding.check)
|
|
216
|
-
const recommendation = finding.remediation ?? genericRecommendation(severity)
|
|
217
|
-
|
|
218
|
-
lines.push(`### ${findingId} ${title}`)
|
|
219
|
-
lines.push(`**Severity**: ${finding.severity}`)
|
|
220
|
-
lines.push(`**Confidence**: ${finding.confidence}`)
|
|
221
|
-
lines.push(`**Location**: ${formatLocation(finding)}`)
|
|
222
|
-
lines.push("")
|
|
223
|
-
lines.push(`**Description**: ${finding.description}`)
|
|
224
|
-
lines.push("")
|
|
225
|
-
lines.push(`**Impact**: ${genericImpact(finding.severity)}`)
|
|
226
|
-
lines.push("")
|
|
227
|
-
lines.push(`**Recommendation**: ${recommendation}`)
|
|
228
|
-
lines.push("")
|
|
229
|
-
})
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return lines.join("\n")
|
|
300
|
+
const prefix = SEVERITY_PREFIX[severity]
|
|
301
|
+
const findingId = `[${prefix}-${index + 1}]`
|
|
302
|
+
const title = normalizeTitle(finding.check)
|
|
303
|
+
const recommendation = finding.remediation ?? genericRecommendation(severity)
|
|
304
|
+
|
|
305
|
+
lines.push(`### ${findingId} ${title}`)
|
|
306
|
+
lines.push(`**Severity**: ${finding.severity}`)
|
|
307
|
+
lines.push(`**Confidence**: ${finding.confidence}`)
|
|
308
|
+
lines.push(`**Location**: ${formatLocation(finding)}`)
|
|
309
|
+
lines.push("")
|
|
310
|
+
lines.push(`**Description**: ${finding.description}`)
|
|
311
|
+
lines.push("")
|
|
312
|
+
lines.push(`**Impact**: ${genericImpact(finding.severity)}`)
|
|
313
|
+
lines.push("")
|
|
314
|
+
lines.push(`**Recommendation**: ${recommendation}`)
|
|
315
|
+
lines.push("")
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return lines.join("\n")
|
|
233
320
|
}
|
|
234
321
|
|
|
235
322
|
function formatDuration(ms: number): string {
|
|
236
|
-
if (ms < 1000) return `${ms}ms
|
|
237
|
-
return `${(ms / 1000).toFixed(1)}s
|
|
323
|
+
if (ms < 1000) return `${ms}ms`
|
|
324
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
238
325
|
}
|
|
239
326
|
|
|
240
327
|
export function buildProvenanceAppendix(
|
|
@@ -242,174 +329,186 @@ export function buildProvenanceAppendix(
|
|
|
242
329
|
threshold: SeverityThreshold,
|
|
243
330
|
includedCount: number,
|
|
244
331
|
): string {
|
|
245
|
-
const lines: string[] = ["## Appendix: Data Provenance"]
|
|
332
|
+
const lines: string[] = ["## Appendix: Data Provenance"]
|
|
246
333
|
|
|
247
|
-
lines.push("- Data source: `audit_state` payload")
|
|
248
|
-
lines.push(`- Severity threshold applied: ${threshold}`)
|
|
249
|
-
lines.push(`- Findings included in report: ${includedCount}`)
|
|
334
|
+
lines.push("- Data source: `audit_state` payload")
|
|
335
|
+
lines.push(`- Severity threshold applied: ${threshold}`)
|
|
336
|
+
lines.push(`- Findings included in report: ${includedCount}`)
|
|
250
337
|
|
|
251
338
|
if (state.findings.length > 0) {
|
|
252
|
-
const sourceCounts: Record<string, number> = {}
|
|
339
|
+
const sourceCounts: Record<string, number> = {}
|
|
253
340
|
for (const f of state.findings) {
|
|
254
|
-
sourceCounts[f.source] = (sourceCounts[f.source] ?? 0) + 1
|
|
341
|
+
sourceCounts[f.source] = (sourceCounts[f.source] ?? 0) + 1
|
|
255
342
|
}
|
|
256
|
-
lines.push("")
|
|
257
|
-
lines.push("### Source Breakdown")
|
|
258
|
-
lines.push("")
|
|
259
|
-
lines.push("| Source | Count |")
|
|
260
|
-
lines.push("| --- | ---: |")
|
|
261
|
-
for (const [source, count] of Object.entries(sourceCounts).sort(
|
|
262
|
-
(
|
|
263
|
-
)) {
|
|
264
|
-
lines.push(`| ${source} | ${count} |`);
|
|
343
|
+
lines.push("")
|
|
344
|
+
lines.push("### Source Breakdown")
|
|
345
|
+
lines.push("")
|
|
346
|
+
lines.push("| Source | Count |")
|
|
347
|
+
lines.push("| --- | ---: |")
|
|
348
|
+
for (const [source, count] of Object.entries(sourceCounts).sort((a, b) => b[1] - a[1])) {
|
|
349
|
+
lines.push(`| ${source} | ${count} |`)
|
|
265
350
|
}
|
|
266
351
|
}
|
|
267
352
|
|
|
268
353
|
if (state.toolsExecuted.length > 0) {
|
|
269
|
-
lines.push("")
|
|
270
|
-
lines.push("### Tool Execution Summary")
|
|
271
|
-
lines.push("")
|
|
272
|
-
lines.push("| Tool | Duration | Status | Findings |")
|
|
273
|
-
lines.push("| --- | --- | --- | ---: |")
|
|
354
|
+
lines.push("")
|
|
355
|
+
lines.push("### Tool Execution Summary")
|
|
356
|
+
lines.push("")
|
|
357
|
+
lines.push("| Tool | Duration | Status | Findings |")
|
|
358
|
+
lines.push("| --- | --- | --- | ---: |")
|
|
274
359
|
for (const exec of state.toolsExecuted) {
|
|
275
|
-
const duration =
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
: "—";
|
|
279
|
-
const status = exec.success ? "✅ success" : "❌ failure";
|
|
280
|
-
lines.push(
|
|
281
|
-
`| ${exec.tool} | ${duration} | ${status} | ${exec.findingsCount} |`,
|
|
282
|
-
);
|
|
360
|
+
const duration = exec.endTime != null ? formatDuration(exec.endTime - exec.startTime) : "—"
|
|
361
|
+
const status = exec.success ? "✅ success" : "❌ failure"
|
|
362
|
+
lines.push(`| ${exec.tool} | ${duration} | ${status} | ${exec.findingsCount} |`)
|
|
283
363
|
}
|
|
284
364
|
}
|
|
285
365
|
|
|
286
|
-
const syncExec = state.toolsExecuted.find((t) => t.tool === "argus_sync_knowledge")
|
|
366
|
+
const syncExec = state.toolsExecuted.find((t) => t.tool === "argus_sync_knowledge")
|
|
287
367
|
if (state.patternVersion || syncExec) {
|
|
288
|
-
lines.push("")
|
|
289
|
-
lines.push("### Data Freshness")
|
|
290
|
-
lines.push("")
|
|
368
|
+
lines.push("")
|
|
369
|
+
lines.push("### Data Freshness")
|
|
370
|
+
lines.push("")
|
|
291
371
|
if (state.patternVersion) {
|
|
292
|
-
lines.push(`- Pattern pack version: \`${state.patternVersion}\``)
|
|
372
|
+
lines.push(`- Pattern pack version: \`${state.patternVersion}\``)
|
|
293
373
|
}
|
|
294
374
|
if (syncExec) {
|
|
295
|
-
lines.push(`- SCVD last synced: ${new Date(syncExec.startTime).toISOString()}`)
|
|
375
|
+
lines.push(`- SCVD last synced: ${new Date(syncExec.startTime).toISOString()}`)
|
|
296
376
|
}
|
|
297
377
|
}
|
|
298
378
|
|
|
299
379
|
if (state.soloditResults && state.soloditResults.length > 0) {
|
|
300
|
-
lines.push("")
|
|
301
|
-
lines.push("### Solodit Cross-References")
|
|
302
|
-
lines.push("")
|
|
380
|
+
lines.push("")
|
|
381
|
+
lines.push("### Solodit Cross-References")
|
|
382
|
+
lines.push("")
|
|
303
383
|
for (const result of state.soloditResults) {
|
|
304
|
-
lines.push(`**Query**: "${result.query}" — ${result.resultCount} results`)
|
|
384
|
+
lines.push(`**Query**: "${result.query}" — ${result.resultCount} results`)
|
|
305
385
|
if (result.topResults.length > 0) {
|
|
306
|
-
lines.push("")
|
|
307
|
-
lines.push("| Title | Severity | Protocol |")
|
|
308
|
-
lines.push("| --- | --- | --- |")
|
|
386
|
+
lines.push("")
|
|
387
|
+
lines.push("| Title | Severity | Protocol |")
|
|
388
|
+
lines.push("| --- | --- | --- |")
|
|
309
389
|
for (const top of result.topResults) {
|
|
310
|
-
lines.push(`| ${top.title} | ${top.severity} | ${top.protocol} |`)
|
|
390
|
+
lines.push(`| ${top.title} | ${top.severity} | ${top.protocol} |`)
|
|
311
391
|
}
|
|
312
392
|
}
|
|
313
|
-
lines.push("")
|
|
393
|
+
lines.push("")
|
|
314
394
|
}
|
|
315
395
|
}
|
|
316
396
|
|
|
317
397
|
if (state.fuzzCounterexamples && state.fuzzCounterexamples.length > 0) {
|
|
318
|
-
lines.push("")
|
|
319
|
-
lines.push("### Fuzz Evidence")
|
|
320
|
-
lines.push("")
|
|
321
|
-
lines.push("| Test | Inputs | Runs | Revert Reason |")
|
|
322
|
-
lines.push("| --- | --- | ---: | --- |")
|
|
398
|
+
lines.push("")
|
|
399
|
+
lines.push("### Fuzz Evidence")
|
|
400
|
+
lines.push("")
|
|
401
|
+
lines.push("| Test | Inputs | Runs | Revert Reason |")
|
|
402
|
+
lines.push("| --- | --- | ---: | --- |")
|
|
323
403
|
for (const cx of state.fuzzCounterexamples) {
|
|
324
|
-
const inputs = cx.inputs.join(", ")
|
|
325
|
-
const reason = cx.revertReason ?? "—"
|
|
326
|
-
lines.push(`| ${cx.testName} | ${inputs} | ${cx.runs} | ${reason} |`)
|
|
404
|
+
const inputs = cx.inputs.join(", ")
|
|
405
|
+
const reason = cx.revertReason ?? "—"
|
|
406
|
+
lines.push(`| ${cx.testName} | ${inputs} | ${cx.runs} | ${reason} |`)
|
|
327
407
|
}
|
|
328
408
|
}
|
|
329
409
|
|
|
330
410
|
if (state.skillsLoaded && state.skillsLoaded.length > 0) {
|
|
331
|
-
lines.push("")
|
|
332
|
-
lines.push("### Knowledge Sources")
|
|
333
|
-
lines.push("")
|
|
334
|
-
lines.push("Skills loaded during this audit:")
|
|
335
|
-
lines.push("")
|
|
411
|
+
lines.push("")
|
|
412
|
+
lines.push("### Knowledge Sources")
|
|
413
|
+
lines.push("")
|
|
414
|
+
lines.push("Skills loaded during this audit:")
|
|
415
|
+
lines.push("")
|
|
336
416
|
for (const skill of state.skillsLoaded) {
|
|
337
|
-
lines.push(`- ${skill}`)
|
|
417
|
+
lines.push(`- ${skill}`)
|
|
338
418
|
}
|
|
339
419
|
}
|
|
340
420
|
|
|
341
|
-
return lines.join("\n")
|
|
421
|
+
return lines.join("\n")
|
|
342
422
|
}
|
|
343
423
|
|
|
344
424
|
export async function executeReportGeneration(
|
|
345
425
|
args: ReportGeneratorArgs,
|
|
346
|
-
context: ToolContext
|
|
426
|
+
context: ToolContext,
|
|
427
|
+
deps: ReportGenerationDependencies = {},
|
|
347
428
|
): Promise<ReportGenerationResult> {
|
|
348
|
-
const includeExecutiveSummary = args.include_executive_summary ?? true
|
|
349
|
-
const threshold = args.severity_threshold ?? "low"
|
|
350
|
-
const state = parseAuditState(args.audit_state)
|
|
351
|
-
const findings = state.findings.filter((finding) =>
|
|
352
|
-
|
|
353
|
-
)
|
|
354
|
-
const counts = calculateCounts(findings);
|
|
355
|
-
const auditDate = new Date().toISOString().slice(0, 10);
|
|
429
|
+
const includeExecutiveSummary = args.include_executive_summary ?? true
|
|
430
|
+
const threshold = args.severity_threshold ?? "low"
|
|
431
|
+
const state = parseAuditState(args.audit_state)
|
|
432
|
+
const findings = state.findings.filter((finding) => shouldIncludeFinding(finding, threshold))
|
|
433
|
+
const counts = calculateCounts(findings)
|
|
434
|
+
const auditDate = new Date().toISOString().slice(0, 10)
|
|
356
435
|
|
|
357
|
-
context.metadata({ title: `Generate audit report: ${args.project_name}` })
|
|
436
|
+
context.metadata({ title: `Generate audit report: ${args.project_name}` })
|
|
358
437
|
|
|
359
|
-
const sections: string[] = [`# Security Audit Report — ${args.project_name}`]
|
|
438
|
+
const sections: string[] = [`# Security Audit Report — ${args.project_name}`]
|
|
360
439
|
|
|
361
440
|
if (includeExecutiveSummary) {
|
|
362
|
-
sections.push("## Executive Summary")
|
|
441
|
+
sections.push("## Executive Summary")
|
|
363
442
|
sections.push(
|
|
364
|
-
`This report summarizes security findings identified for ${args.project_name} based on static analysis, testing, and pattern-based review
|
|
365
|
-
)
|
|
366
|
-
sections.push("")
|
|
367
|
-
sections.push("| Severity | Count |")
|
|
368
|
-
sections.push("| --- | ---: |")
|
|
369
|
-
sections.push(`| Critical | ${counts.critical} |`)
|
|
370
|
-
sections.push(`| High | ${counts.high} |`)
|
|
371
|
-
sections.push(`| Medium | ${counts.medium} |`)
|
|
372
|
-
sections.push(`| Low | ${counts.low} |`)
|
|
373
|
-
sections.push(`| Informational | ${counts.informational} |`)
|
|
374
|
-
sections.push("")
|
|
375
|
-
sections.push(`Overall risk assessment: ${overallRiskAssessment(counts)}.`)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
sections.push("## Scope")
|
|
379
|
-
sections.push("Contracts in scope:")
|
|
443
|
+
`This report summarizes security findings identified for ${args.project_name} based on static analysis, testing, and pattern-based review.`,
|
|
444
|
+
)
|
|
445
|
+
sections.push("")
|
|
446
|
+
sections.push("| Severity | Count |")
|
|
447
|
+
sections.push("| --- | ---: |")
|
|
448
|
+
sections.push(`| Critical | ${counts.critical} |`)
|
|
449
|
+
sections.push(`| High | ${counts.high} |`)
|
|
450
|
+
sections.push(`| Medium | ${counts.medium} |`)
|
|
451
|
+
sections.push(`| Low | ${counts.low} |`)
|
|
452
|
+
sections.push(`| Informational | ${counts.informational} |`)
|
|
453
|
+
sections.push("")
|
|
454
|
+
sections.push(`Overall risk assessment: ${overallRiskAssessment(counts)}.`)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
sections.push("## Scope")
|
|
458
|
+
sections.push("Contracts in scope:")
|
|
380
459
|
if (args.scope.length === 0) {
|
|
381
|
-
sections.push("- None provided")
|
|
460
|
+
sections.push("- None provided")
|
|
382
461
|
} else {
|
|
383
462
|
for (const contract of args.scope) {
|
|
384
|
-
sections.push(`- ${contract}`)
|
|
463
|
+
sections.push(`- ${contract}`)
|
|
385
464
|
}
|
|
386
465
|
}
|
|
387
|
-
sections.push(`Audit date: ${auditDate}`)
|
|
388
|
-
|
|
389
|
-
sections.push("## Methodology")
|
|
390
|
-
sections.push("Tools and techniques used:")
|
|
391
|
-
sections.push("- Slither static analysis")
|
|
392
|
-
sections.push("- Foundry tests and fuzzing")
|
|
393
|
-
sections.push("- Pattern Analysis")
|
|
394
|
-
sections.push("- Solodit research cross-referencing")
|
|
466
|
+
sections.push(`Audit date: ${auditDate}`)
|
|
467
|
+
|
|
468
|
+
sections.push("## Methodology")
|
|
469
|
+
sections.push("Tools and techniques used:")
|
|
470
|
+
sections.push("- Slither static analysis")
|
|
471
|
+
sections.push("- Foundry tests and fuzzing")
|
|
472
|
+
sections.push("- Pattern Analysis")
|
|
473
|
+
sections.push("- Solodit research cross-referencing")
|
|
395
474
|
sections.push(
|
|
396
|
-
"Approach: Findings were normalized, deduplicated by detector signature and location, then prioritized by severity and confidence."
|
|
397
|
-
)
|
|
475
|
+
"Approach: Findings were normalized, deduplicated by detector signature and location, then prioritized by severity and confidence.",
|
|
476
|
+
)
|
|
398
477
|
|
|
399
|
-
sections.push(buildFindingsSection(findings))
|
|
478
|
+
sections.push(buildFindingsSection(findings))
|
|
400
479
|
|
|
401
|
-
sections.push("## Recommendations")
|
|
480
|
+
sections.push("## Recommendations")
|
|
402
481
|
for (const item of buildRecommendations(counts)) {
|
|
403
|
-
sections.push(`- ${item}`)
|
|
482
|
+
sections.push(`- ${item}`)
|
|
404
483
|
}
|
|
405
484
|
|
|
406
|
-
sections.push(buildProvenanceAppendix(state, threshold, findings.length))
|
|
485
|
+
sections.push(buildProvenanceAppendix(state, threshold, findings.length))
|
|
407
486
|
|
|
408
|
-
|
|
409
|
-
|
|
487
|
+
const reportMarkdown = sections.join("\n\n")
|
|
488
|
+
const safeName = args.project_name.replace(/[^a-zA-Z0-9-_]/g, "-")
|
|
489
|
+
const diskFilename = `${safeName}-${Date.now()}.md`
|
|
490
|
+
|
|
491
|
+
const result: ReportGenerationResult = {
|
|
492
|
+
report: reportMarkdown,
|
|
410
493
|
findingsCount: counts,
|
|
411
494
|
filename: `${args.project_name}-audit-report-${auditDate}.md`,
|
|
412
|
-
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const loadConfig = deps.loadConfig ?? loadArgusConfig
|
|
499
|
+
const projectDir = resolveProjectDir(context)
|
|
500
|
+
const config = loadConfig(projectDir)
|
|
501
|
+
const outputDir = config.reporting?.output_dir ?? ".opencode/reports/"
|
|
502
|
+
const fullPath = path.join(projectDir, outputDir, diskFilename)
|
|
503
|
+
await Bun.write(fullPath, reportMarkdown)
|
|
504
|
+
result.filePath = fullPath
|
|
505
|
+
} catch (err: unknown) {
|
|
506
|
+
const logger = createLogger()
|
|
507
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
508
|
+
logger.warn(`Failed to write report to disk: ${message}`)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return result
|
|
413
512
|
}
|
|
414
513
|
|
|
415
514
|
export const reportGeneratorTool = tool({
|
|
@@ -425,7 +524,7 @@ export const reportGeneratorTool = tool({
|
|
|
425
524
|
audit_state: tool.schema.string(),
|
|
426
525
|
},
|
|
427
526
|
async execute(args, context) {
|
|
428
|
-
const result = await executeReportGeneration(args, context)
|
|
429
|
-
return JSON.stringify(result)
|
|
527
|
+
const result = await executeReportGeneration(args, context)
|
|
528
|
+
return JSON.stringify(result)
|
|
430
529
|
},
|
|
431
|
-
})
|
|
530
|
+
})
|