solidity-argus 0.3.7 → 0.5.7

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 (108) hide show
  1. package/AGENTS.md +13 -6
  2. package/README.md +24 -12
  3. package/package.json +7 -3
  4. package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +1 -0
  5. package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +1 -0
  6. package/skills/checklists/cyfrin-defi-core/SKILL.md +1 -0
  7. package/skills/checklists/cyfrin-defi-integrations/SKILL.md +1 -0
  8. package/skills/checklists/cyfrin-gas/SKILL.md +1 -0
  9. package/skills/checklists/general-audit/SKILL.md +1 -0
  10. package/skills/methodology/audit-workflow/SKILL.md +1 -0
  11. package/skills/methodology/report-template/SKILL.md +1 -0
  12. package/skills/methodology/severity-classification/SKILL.md +1 -0
  13. package/skills/protocol-patterns/amm-dex/SKILL.md +1 -0
  14. package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +1 -0
  15. package/skills/protocol-patterns/dao-governance/SKILL.md +1 -0
  16. package/skills/protocol-patterns/lending-borrowing/SKILL.md +1 -0
  17. package/skills/protocol-patterns/staking-vesting/SKILL.md +1 -0
  18. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +0 -50
  19. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +0 -63
  20. package/src/agents/argus-prompt.ts +98 -33
  21. package/src/agents/pythia-prompt.ts +24 -2
  22. package/src/agents/scribe-prompt.ts +34 -10
  23. package/src/agents/sentinel-prompt.ts +19 -0
  24. package/src/agents/themis-prompt.ts +110 -0
  25. package/src/cli/commands/doctor.ts +29 -17
  26. package/src/cli/commands/install.ts +74 -33
  27. package/src/config/loader.ts +29 -5
  28. package/src/config/schema.ts +45 -45
  29. package/src/constants/defaults.ts +1 -0
  30. package/src/create-hooks.ts +806 -173
  31. package/src/create-managers.ts +4 -2
  32. package/src/create-tools.ts +5 -1
  33. package/src/features/audit-enforcer/audit-enforcer.ts +1 -11
  34. package/src/features/background-agent/background-manager.ts +32 -5
  35. package/src/features/error-recovery/tool-error-recovery.ts +1 -0
  36. package/src/features/persistent-state/audit-state-manager.ts +272 -29
  37. package/src/features/persistent-state/event-sink.ts +96 -25
  38. package/src/features/persistent-state/findings-materializer.ts +68 -2
  39. package/src/features/persistent-state/global-run-index.ts +86 -8
  40. package/src/features/persistent-state/index.ts +7 -1
  41. package/src/features/persistent-state/run-finalizer.ts +116 -7
  42. package/src/features/persistent-state/run-pruner.ts +93 -0
  43. package/src/hooks/agent-tracker.ts +14 -2
  44. package/src/hooks/compaction-hook.ts +7 -16
  45. package/src/hooks/config-handler.ts +83 -29
  46. package/src/hooks/context-budget.ts +4 -5
  47. package/src/hooks/event-hook.ts +213 -57
  48. package/src/hooks/knowledge-sync-hook.ts +2 -3
  49. package/src/hooks/safe-create-hook.ts +13 -1
  50. package/src/hooks/system-prompt-hook.ts +20 -39
  51. package/src/hooks/tool-tracking-hook.ts +602 -323
  52. package/src/index.ts +15 -1
  53. package/src/knowledge/scvd-client.ts +2 -4
  54. package/src/knowledge/scvd-errors.ts +25 -2
  55. package/src/knowledge/scvd-index.ts +7 -5
  56. package/src/knowledge/scvd-sync.ts +6 -6
  57. package/src/managers/types.ts +20 -2
  58. package/src/shared/agent-names.ts +23 -0
  59. package/src/shared/audit-artifact-resolver.ts +8 -3
  60. package/src/shared/audit-phases.ts +12 -0
  61. package/src/shared/cache-paths.ts +41 -0
  62. package/src/shared/drop-diagnostics.ts +2 -2
  63. package/src/shared/forge-errors.ts +31 -0
  64. package/src/shared/forge-runner.ts +30 -0
  65. package/src/shared/format-error.ts +3 -0
  66. package/src/shared/index.ts +9 -0
  67. package/src/shared/key-tools.ts +39 -0
  68. package/src/shared/logger.ts +7 -7
  69. package/src/shared/path-containment.ts +25 -0
  70. package/src/shared/path-utils.ts +11 -0
  71. package/src/shared/report-path-resolver.ts +4 -2
  72. package/src/shared/safe-emit.ts +24 -0
  73. package/src/shared/token-utils.ts +5 -0
  74. package/src/shared/type-guards.ts +8 -0
  75. package/src/shared/validation-constants.ts +52 -0
  76. package/src/skills/analysis/cluster.ts +1 -114
  77. package/src/skills/analysis/normalize.ts +2 -114
  78. package/src/skills/analysis/stopwords.ts +109 -0
  79. package/src/skills/argus-skill-resolver.ts +6 -3
  80. package/src/solodit-lifecycle.ts +153 -37
  81. package/src/state/adapters.ts +60 -66
  82. package/src/state/finding-aggregation.ts +6 -8
  83. package/src/state/finding-fingerprint.ts +1 -1
  84. package/src/state/finding-store.ts +31 -9
  85. package/src/state/index.ts +1 -1
  86. package/src/state/projectors.ts +27 -19
  87. package/src/state/schemas.ts +8 -32
  88. package/src/state/types.ts +3 -0
  89. package/src/tools/contract-analyzer-tool.ts +4 -6
  90. package/src/tools/forge-coverage-tool.ts +10 -35
  91. package/src/tools/forge-fuzz-tool.ts +21 -51
  92. package/src/tools/forge-test-tool.ts +25 -47
  93. package/src/tools/gas-analysis-tool.ts +12 -41
  94. package/src/tools/pattern-checker-tool.ts +37 -15
  95. package/src/tools/pattern-loader.ts +18 -4
  96. package/src/tools/persist-deduped-tool.ts +94 -0
  97. package/src/tools/proxy-detection-tool.ts +35 -34
  98. package/src/tools/read-findings-tool.ts +390 -0
  99. package/src/tools/record-finding-tool.ts +130 -25
  100. package/src/tools/report-generator-tool.ts +475 -327
  101. package/src/tools/report-preflight.ts +5 -1
  102. package/src/tools/slither-tool.ts +55 -16
  103. package/src/tools/solodit-search-tool.ts +260 -112
  104. package/src/tools/sync-knowledge-tool.ts +2 -3
  105. package/src/utils/solidity-parser.ts +39 -24
  106. package/src/features/migration/index.ts +0 -14
  107. package/src/features/migration/migration-adapter.ts +0 -151
  108. package/src/features/migration/parity-telemetry.ts +0 -133
@@ -140,6 +140,36 @@ export function parseExternalCalls(sourceText: string): string[] {
140
140
  }
141
141
  }
142
142
 
143
+ async function spawnForgeInspect(
144
+ contractName: string,
145
+ inspectType: string,
146
+ cwd: string,
147
+ ): Promise<{ success: boolean; stdout: string; stderr: string }> {
148
+ const proc = Bun.spawn(["forge", "inspect", contractName, inspectType, "--json"], {
149
+ cwd,
150
+ stdout: "pipe",
151
+ stderr: "pipe",
152
+ })
153
+
154
+ const timeout = 15_000
155
+ let timerId: ReturnType<typeof setTimeout> | undefined
156
+ const timer = new Promise<never>((_, reject) => {
157
+ timerId = setTimeout(() => {
158
+ proc.kill()
159
+ reject(new Error(`forge inspect ${inspectType} timed out after ${timeout}ms`))
160
+ }, timeout)
161
+ })
162
+
163
+ try {
164
+ const exitCode = await Promise.race([proc.exited, timer])
165
+ const stdout = await new Response(proc.stdout).text()
166
+ const stderr = await new Response(proc.stderr).text()
167
+ return { success: exitCode === 0, stdout, stderr }
168
+ } finally {
169
+ if (timerId !== undefined) clearTimeout(timerId)
170
+ }
171
+ }
172
+
143
173
  /**
144
174
  * Extract contract information using forge inspect
145
175
  * Runs forge inspect <contractName> abi and storage-layout
@@ -162,39 +192,24 @@ export async function extractContractInfo(
162
192
  }
163
193
 
164
194
  try {
165
- // Run forge inspect abi
166
- const abiResult = Bun.spawnSync(["forge", "inspect", contractName, "abi", "--json"], {
167
- cwd: projectDir,
168
- stdout: "pipe",
169
- stderr: "pipe",
170
- timeout: 15_000,
171
- })
195
+ // Run both forge inspect commands in parallel (async, non-blocking)
196
+ const [abiResult, storageResult] = await Promise.all([
197
+ spawnForgeInspect(contractName, "abi", projectDir),
198
+ spawnForgeInspect(contractName, "storage-layout", projectDir),
199
+ ])
172
200
 
173
201
  if (!abiResult.success) {
174
- const errorMsg = abiResult.stderr?.toString() || "Unknown error"
175
- result.error = `Failed to inspect ABI: ${errorMsg}`
202
+ result.error = `Failed to inspect ABI: ${abiResult.stderr}`
176
203
  return result
177
204
  }
178
205
 
179
- // Run forge inspect storage-layout
180
- const storageResult = Bun.spawnSync(
181
- ["forge", "inspect", contractName, "storage-layout", "--json"],
182
- {
183
- cwd: projectDir,
184
- stdout: "pipe",
185
- stderr: "pipe",
186
- timeout: 15_000,
187
- },
188
- )
189
-
190
206
  if (!storageResult.success) {
191
- const errorMsg = storageResult.stderr?.toString() || "Unknown error"
192
- result.error = `Failed to inspect storage layout: ${errorMsg}`
207
+ result.error = `Failed to inspect storage layout: ${storageResult.stderr}`
193
208
  return result
194
209
  }
195
210
 
196
211
  // Parse ABI
197
- const abiRaw = abiResult.stdout?.toString() || "[]"
212
+ const abiRaw = abiResult.stdout || "[]"
198
213
  const abiOutput = extractJson(abiRaw, "[")
199
214
  let abi: ABIFunction[] = []
200
215
  try {
@@ -205,7 +220,7 @@ export async function extractContractInfo(
205
220
  }
206
221
 
207
222
  // Parse storage layout
208
- const storageRaw = storageResult.stdout?.toString() || "{}"
223
+ const storageRaw = storageResult.stdout || "{}"
209
224
  const storageOutput = extractJson(storageRaw, "{")
210
225
  let storageLayout: StorageLayout = { storage: [], types: {} }
211
226
  try {
@@ -1,14 +0,0 @@
1
- export {
2
- adaptLegacyFindings,
3
- adaptLegacyStateToReportInput,
4
- getMigrationMode,
5
- type MigrationMode,
6
- validateStrictCompatibility,
7
- } from "./migration-adapter"
8
-
9
- export {
10
- computeParityMetrics,
11
- formatParityReport,
12
- type ParityMetrics,
13
- type SeverityDistribution,
14
- } from "./parity-telemetry"
@@ -1,151 +0,0 @@
1
- import {
2
- createDropDiagnosticsCollector,
3
- type DropDiagnosticsCollector,
4
- } from "../../shared/drop-diagnostics"
5
- import { normalizeLegacyFindingsArray, normalizeToCanonicalFinding } from "../../state/adapters"
6
- import type { CanonicalFinding, ReportInput } from "../../state/schemas"
7
- import { SCHEMA_VERSION } from "../../state/schemas"
8
- import type { AuditState, Finding } from "../../state/types"
9
-
10
- export type MigrationMode = "legacy" | "dual" | "strict"
11
-
12
- /**
13
- * Returns the active migration mode from config, defaulting to "legacy".
14
- */
15
- export function getMigrationMode(config: { migration?: { mode?: MigrationMode } }): MigrationMode {
16
- return config.migration?.mode ?? "legacy"
17
- }
18
-
19
- /**
20
- * Adapts a legacy `AuditState` into canonical `CanonicalFinding[]`.
21
- *
22
- * In legacy mode: returns the raw findings as-is (backward compatible).
23
- * In dual mode: normalizes findings to canonical AND returns both.
24
- * In strict mode: normalizes to canonical, rejects payloads missing required canonical fields.
25
- */
26
- export function adaptLegacyFindings(
27
- state: AuditState,
28
- mode: MigrationMode,
29
- runId: string,
30
- ): {
31
- legacyFindings: Finding[]
32
- canonicalFindings: CanonicalFinding[]
33
- diagnostics: ReturnType<DropDiagnosticsCollector["getDiagnostics"]>
34
- } {
35
- const legacyFindings = state.findings
36
-
37
- if (mode === "legacy") {
38
- return {
39
- legacyFindings,
40
- canonicalFindings: [],
41
- diagnostics: [],
42
- }
43
- }
44
-
45
- const policy = mode === "strict" ? "strict-fail" : "warn"
46
- const diag = createDropDiagnosticsCollector(policy, "migration-adapter")
47
-
48
- const { findings: canonicalFindings, diagnostics: adapterDiags } = normalizeLegacyFindingsArray(
49
- legacyFindings as unknown as unknown[],
50
- runId,
51
- )
52
-
53
- for (const d of adapterDiags) {
54
- if (d.level === "error") {
55
- diag.error(d.code, d.message, d.field)
56
- } else {
57
- diag.warn(d.code, d.message, d.field)
58
- }
59
- }
60
-
61
- // In strict mode, validate that all legacy findings survived normalization
62
- if (mode === "strict" && canonicalFindings.length < legacyFindings.length) {
63
- const dropped = legacyFindings.length - canonicalFindings.length
64
- diag.error(
65
- "STRICT_FINDINGS_DROPPED",
66
- `${dropped} legacy finding(s) could not be normalized to canonical format`,
67
- )
68
- }
69
-
70
- // Throws DropDiagnosticsError in strict mode if errors exist
71
- diag.throwIfStrict()
72
-
73
- return {
74
- legacyFindings,
75
- canonicalFindings,
76
- diagnostics: diag.getDiagnostics(),
77
- }
78
- }
79
-
80
- /**
81
- * Adapts a legacy `AuditState` into a canonical `ReportInput`.
82
- *
83
- * Maps legacy AuditState fields to the canonical ReportInput contract.
84
- */
85
- export function adaptLegacyStateToReportInput(
86
- state: AuditState,
87
- mode: MigrationMode,
88
- runId: string,
89
- ): {
90
- reportInput: ReportInput
91
- diagnostics: ReturnType<DropDiagnosticsCollector["getDiagnostics"]>
92
- } {
93
- const { canonicalFindings, diagnostics } = adaptLegacyFindings(
94
- state,
95
- mode === "legacy" ? "dual" : mode,
96
- runId,
97
- )
98
-
99
- const reportInput: ReportInput = {
100
- run_id: runId,
101
- seq: 0,
102
- session_id: state.sessionId,
103
- tool_call_id: "",
104
- source: "migration-adapter",
105
- schema_version: SCHEMA_VERSION,
106
- projectDir: state.projectDir,
107
- findings: canonicalFindings,
108
- toolsExecuted: state.toolsExecuted.map((t) => ({
109
- ...t,
110
- run_id: runId,
111
- schema_version: SCHEMA_VERSION,
112
- })),
113
- scope: state.scope,
114
- soloditResults: state.soloditResults,
115
- fuzzCounterexamples: state.fuzzCounterexamples,
116
- coverageReport: state.coverageReport,
117
- gasHotspots: state.gasHotspots,
118
- proxyContracts: state.proxyContracts,
119
- }
120
-
121
- return { reportInput, diagnostics }
122
- }
123
-
124
- /**
125
- * Validates that a legacy AuditState is compatible with strict mode.
126
- * Returns true if ALL findings can be normalized without errors.
127
- */
128
- export function validateStrictCompatibility(
129
- state: AuditState,
130
- runId: string,
131
- ): { compatible: boolean; errors: string[] } {
132
- const errors: string[] = []
133
-
134
- for (const [index, finding] of state.findings.entries()) {
135
- const result = normalizeToCanonicalFinding(
136
- finding as unknown as Record<string, unknown>,
137
- runId,
138
- index + 1,
139
- )
140
- const hasErrors = result.diagnostics.some((d) => d.level === "error")
141
- if (hasErrors) {
142
- errors.push(
143
- ...result.diagnostics
144
- .filter((d) => d.level === "error")
145
- .map((d) => `[finding:${index}] ${d.message}`),
146
- )
147
- }
148
- }
149
-
150
- return { compatible: errors.length === 0, errors }
151
- }
@@ -1,133 +0,0 @@
1
- import { stableHash } from "../../state/projectors"
2
- import type { CanonicalFinding } from "../../state/schemas"
3
- import type { Finding, FindingSeverity } from "../../state/types"
4
-
5
- const SEVERITIES: readonly FindingSeverity[] = [
6
- "Critical",
7
- "High",
8
- "Medium",
9
- "Low",
10
- "Informational",
11
- ] as const
12
-
13
- export interface SeverityDistribution {
14
- Critical: number
15
- High: number
16
- Medium: number
17
- Low: number
18
- Informational: number
19
- }
20
-
21
- export interface ParityMetrics {
22
- legacyFindingCount: number
23
- canonicalFindingCount: number
24
- findingCountDiff: number
25
- legacySeverityDistribution: SeverityDistribution
26
- canonicalSeverityDistribution: SeverityDistribution
27
- severityDiffs: Partial<Record<FindingSeverity, number>>
28
- legacyContentHash: string
29
- canonicalContentHash: string
30
- hashMatch: boolean
31
- onlyInLegacy: string[]
32
- onlyInCanonical: string[]
33
- timestamp: number
34
- }
35
-
36
- function computeSeverityDistribution(
37
- findings: Array<{ severity: FindingSeverity }>,
38
- ): SeverityDistribution {
39
- const dist: SeverityDistribution = {
40
- Critical: 0,
41
- High: 0,
42
- Medium: 0,
43
- Low: 0,
44
- Informational: 0,
45
- }
46
- for (const f of findings) {
47
- if (f.severity in dist) {
48
- dist[f.severity]++
49
- }
50
- }
51
- return dist
52
- }
53
-
54
- function findingIds(findings: Array<{ id: string }>): Set<string> {
55
- return new Set(findings.map((f) => f.id))
56
- }
57
-
58
- export function computeParityMetrics(
59
- legacyFindings: Finding[],
60
- canonicalFindings: CanonicalFinding[],
61
- ): ParityMetrics {
62
- const legacySeverity = computeSeverityDistribution(legacyFindings)
63
- const canonicalSeverity = computeSeverityDistribution(canonicalFindings)
64
-
65
- const severityDiffs: Partial<Record<FindingSeverity, number>> = {}
66
- for (const sev of SEVERITIES) {
67
- const diff = canonicalSeverity[sev] - legacySeverity[sev]
68
- if (diff !== 0) {
69
- severityDiffs[sev] = diff
70
- }
71
- }
72
-
73
- const legacyIds = findingIds(legacyFindings)
74
- const canonicalIds = findingIds(canonicalFindings)
75
-
76
- const onlyInLegacy = [...legacyIds].filter((id) => !canonicalIds.has(id))
77
- const onlyInCanonical = [...canonicalIds].filter((id) => !legacyIds.has(id))
78
-
79
- const legacyContentHash = stableHash(
80
- legacyFindings.map((f) => ({ id: f.id, check: f.check, severity: f.severity, file: f.file })),
81
- )
82
- const canonicalContentHash = stableHash(
83
- canonicalFindings.map((f) => ({
84
- id: f.id,
85
- check: f.check,
86
- severity: f.severity,
87
- file: f.file,
88
- })),
89
- )
90
-
91
- return {
92
- legacyFindingCount: legacyFindings.length,
93
- canonicalFindingCount: canonicalFindings.length,
94
- findingCountDiff: canonicalFindings.length - legacyFindings.length,
95
- legacySeverityDistribution: legacySeverity,
96
- canonicalSeverityDistribution: canonicalSeverity,
97
- severityDiffs,
98
- legacyContentHash,
99
- canonicalContentHash,
100
- hashMatch: legacyContentHash === canonicalContentHash,
101
- onlyInLegacy,
102
- onlyInCanonical,
103
- timestamp: Date.now(),
104
- }
105
- }
106
-
107
- export function formatParityReport(metrics: ParityMetrics): string {
108
- const lines: string[] = [
109
- "=== Migration Parity Report ===",
110
- `Finding count: legacy=${metrics.legacyFindingCount} canonical=${metrics.canonicalFindingCount} diff=${metrics.findingCountDiff}`,
111
- `Content hash match: ${metrics.hashMatch}`,
112
- ]
113
-
114
- const sevDiffs = Object.entries(metrics.severityDiffs)
115
- if (sevDiffs.length > 0) {
116
- lines.push(
117
- `Severity diffs: ${sevDiffs.map(([k, v]) => `${k}=${v > 0 ? "+" : ""}${v}`).join(", ")}`,
118
- )
119
- }
120
-
121
- if (metrics.onlyInLegacy.length > 0) {
122
- lines.push(
123
- `Only in legacy (${metrics.onlyInLegacy.length}): ${metrics.onlyInLegacy.join(", ")}`,
124
- )
125
- }
126
- if (metrics.onlyInCanonical.length > 0) {
127
- lines.push(
128
- `Only in canonical (${metrics.onlyInCanonical.length}): ${metrics.onlyInCanonical.join(", ")}`,
129
- )
130
- }
131
-
132
- return lines.join("\n")
133
- }