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.
Files changed (169) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +93 -37
  3. package/package.json +34 -7
  4. package/skills/INVENTORY.md +88 -57
  5. package/skills/README.md +26 -23
  6. package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
  7. package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
  8. package/skills/case-studies/cream-finance/SKILL.md +52 -0
  9. package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
  10. package/skills/case-studies/dao-hack/SKILL.md +51 -0
  11. package/skills/case-studies/euler-finance/SKILL.md +52 -0
  12. package/skills/case-studies/harvest-finance/SKILL.md +52 -0
  13. package/skills/case-studies/level-finance/SKILL.md +51 -0
  14. package/skills/case-studies/mango-markets/SKILL.md +53 -0
  15. package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
  16. package/skills/case-studies/parity-multisig/SKILL.md +55 -0
  17. package/skills/case-studies/poly-network/SKILL.md +51 -0
  18. package/skills/case-studies/rari-fuse/SKILL.md +51 -0
  19. package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
  20. package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
  21. package/skills/manifests/smartbugs.json +1 -3
  22. package/skills/manifests/sunweb3sec.json +1 -3
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +14 -0
  24. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
  25. package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
  26. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
  27. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +2 -1
  28. package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
  29. package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
  30. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +2 -1
  31. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
  32. package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
  33. package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
  34. package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
  35. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +1 -0
  36. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
  37. package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
  38. package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
  39. package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
  40. package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
  41. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
  42. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
  43. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
  44. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
  45. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
  46. package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
  47. package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
  48. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
  49. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
  50. package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
  51. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +9 -0
  52. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
  53. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
  54. package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
  55. package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -0
  56. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
  57. package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
  58. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +2 -1
  59. package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
  60. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
  61. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +2 -1
  62. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
  63. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
  64. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
  65. package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
  66. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
  67. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
  68. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
  69. package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
  70. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
  71. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
  72. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
  73. package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
  74. package/src/agents/argus-prompt.ts +34 -7
  75. package/src/agents/pythia-prompt.ts +13 -4
  76. package/src/agents/scribe-prompt.ts +20 -2
  77. package/src/agents/sentinel-prompt.ts +45 -5
  78. package/src/cli/cli-program.ts +29 -26
  79. package/src/cli/commands/check-skills.ts +135 -0
  80. package/src/cli/commands/doctor.ts +48 -26
  81. package/src/cli/commands/init.ts +5 -3
  82. package/src/cli/commands/install.ts +7 -5
  83. package/src/cli/commands/lint-skills.ts +16 -12
  84. package/src/cli/index.ts +5 -5
  85. package/src/cli/types.ts +3 -3
  86. package/src/config/index.ts +1 -1
  87. package/src/config/loader.ts +4 -6
  88. package/src/config/schema.ts +6 -5
  89. package/src/config/types.ts +2 -2
  90. package/src/constants/defaults.ts +2 -0
  91. package/src/create-hooks.ts +145 -34
  92. package/src/create-managers.ts +10 -8
  93. package/src/create-tools.ts +13 -9
  94. package/src/features/background-agent/background-manager.ts +93 -87
  95. package/src/features/background-agent/index.ts +1 -1
  96. package/src/features/context-monitor/context-monitor.ts +3 -3
  97. package/src/features/context-monitor/index.ts +2 -2
  98. package/src/features/error-recovery/session-recovery.ts +2 -4
  99. package/src/features/error-recovery/tool-error-recovery.ts +12 -7
  100. package/src/features/index.ts +5 -5
  101. package/src/features/persistent-state/audit-state-manager.ts +143 -60
  102. package/src/features/persistent-state/global-run-index.ts +38 -0
  103. package/src/features/persistent-state/index.ts +1 -1
  104. package/src/features/persistent-state/run-journal.ts +86 -0
  105. package/src/hooks/config-handler.ts +28 -11
  106. package/src/hooks/context-budget.ts +2 -5
  107. package/src/hooks/event-hook.ts +47 -23
  108. package/src/hooks/hook-system.ts +4 -4
  109. package/src/hooks/index.ts +5 -5
  110. package/src/hooks/knowledge-sync-hook.ts +18 -21
  111. package/src/hooks/recon-context-builder.ts +2 -2
  112. package/src/hooks/safe-create-hook.ts +6 -7
  113. package/src/hooks/system-prompt-hook.ts +18 -1
  114. package/src/hooks/tool-tracking-hook.ts +110 -51
  115. package/src/hooks/types.ts +2 -1
  116. package/src/index.ts +24 -37
  117. package/src/knowledge/retry.ts +22 -22
  118. package/src/knowledge/scvd-client.ts +88 -95
  119. package/src/knowledge/scvd-errors.ts +35 -35
  120. package/src/knowledge/scvd-index.ts +78 -80
  121. package/src/knowledge/scvd-sync.ts +106 -101
  122. package/src/managers/index.ts +1 -1
  123. package/src/managers/types.ts +19 -14
  124. package/src/plugin-interface.ts +7 -9
  125. package/src/shared/binary-utils.ts +44 -35
  126. package/src/shared/deep-merge.ts +55 -36
  127. package/src/shared/file-utils.ts +21 -19
  128. package/src/shared/index.ts +11 -5
  129. package/src/shared/jsonc-parser.ts +123 -28
  130. package/src/shared/logger.ts +16 -3
  131. package/src/shared/project-utils.ts +30 -0
  132. package/src/skills/analysis/cluster.ts +414 -0
  133. package/src/skills/analysis/gates.ts +227 -0
  134. package/src/skills/analysis/index.ts +33 -0
  135. package/src/skills/analysis/normalize.ts +217 -0
  136. package/src/skills/analysis/similarity.ts +224 -0
  137. package/src/skills/argus-skill-resolver.ts +17 -6
  138. package/src/skills/skill-schema.ts +11 -10
  139. package/src/solodit-lifecycle.ts +203 -0
  140. package/src/state/audit-state.ts +8 -8
  141. package/src/state/finding-store.ts +68 -55
  142. package/src/state/types.ts +88 -67
  143. package/src/tools/argus-skill-load-tool.ts +12 -7
  144. package/src/tools/contract-analyzer-tool.ts +142 -77
  145. package/src/tools/forge-coverage-tool.ts +226 -0
  146. package/src/tools/forge-fuzz-tool.ts +127 -127
  147. package/src/tools/forge-test-tool.ts +201 -158
  148. package/src/tools/gas-analysis-tool.ts +264 -0
  149. package/src/tools/pattern-checker-tool.ts +203 -191
  150. package/src/tools/pattern-loader.ts +5 -111
  151. package/src/tools/pattern-schema.ts +3 -0
  152. package/src/tools/proxy-detection-tool.ts +224 -0
  153. package/src/tools/report-generator-tool.ts +305 -206
  154. package/src/tools/slither-tool.ts +266 -218
  155. package/src/tools/solodit-search-tool.ts +235 -119
  156. package/src/tools/sync-knowledge-tool.ts +7 -11
  157. package/src/utils/audit-artifact-detector.ts +28 -29
  158. package/src/utils/dependency-scanner.ts +37 -37
  159. package/src/utils/project-detector.ts +111 -124
  160. package/src/utils/solidity-parser.ts +175 -75
  161. package/skills/patterns/access-control.yaml +0 -31
  162. package/skills/patterns/erc4626.yaml +0 -29
  163. package/skills/patterns/flash-loan.yaml +0 -20
  164. package/skills/patterns/oracle.yaml +0 -30
  165. package/skills/patterns/proxy.yaml +0 -30
  166. package/skills/patterns/reentrancy.yaml +0 -30
  167. package/skills/patterns/signature.yaml +0 -31
  168. package/src/hooks/event-hook-v2.ts +0 -99
  169. package/src/state/plugin-state.ts +0 -14
@@ -1,37 +1,41 @@
1
- import { tool, type ToolContext } from "@opencode-ai/plugin";
2
- import type { AuditState, Finding, FindingSeverity, ToolExecution } from "../state/types";
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
- const SEVERITY_ORDER: FindingSeverity[] = [
29
- "Critical",
30
- "High",
31
- "Medium",
32
- "Low",
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("audit_state is not valid JSON — expected an AuditState object or Finding[] array");
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
- return emptyAuditState(parsed as Finding[]);
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 (typeof parsed === "object" && parsed !== null && Array.isArray((parsed as AuditState).findings)) {
96
- const state = parsed as AuditState;
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
- return `${finding.file}:${finding.lines[0]}-${finding.lines[1]}`;
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("1. Immediately remediate all Critical findings and block release until fixes are verified.");
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("2. Prioritize High findings in the next patch cycle with dedicated security test coverage.");
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("4. Address Low/Informational findings as part of ongoing hardening and code quality efforts.");
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("1. Maintain current controls, monitor code changes, and re-audit before major upgrades.");
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
- (a, b) => b[1] - a[1],
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
- exec.endTime != null
277
- ? formatDuration(exec.endTime - exec.startTime)
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
- shouldIncludeFinding(finding, threshold)
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
- return {
409
- report: sections.join("\n\n"),
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
+ })