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
@@ -1,6 +1,5 @@
1
1
  import { readdirSync, readFileSync, statSync } from "node:fs"
2
- import os from "node:os"
3
- import { dirname, extname, join, resolve } from "node:path"
2
+ import { dirname, extname, isAbsolute, join, resolve } from "node:path"
4
3
  import { type ToolContext, tool } from "@opencode-ai/plugin"
5
4
  import {
6
5
  loadIndex,
@@ -8,7 +7,10 @@ import {
8
7
  type ScvdIndexEntry,
9
8
  searchIndex,
10
9
  } from "../knowledge/scvd-index"
10
+ import { getScvdIndexPath } from "../shared/cache-paths"
11
11
  import { createLogger } from "../shared/logger"
12
+ import { normalizeFilePath } from "../shared/path-utils"
13
+ import { resolveProjectDir } from "../shared/project-utils"
12
14
  import { extractDetectionRulesFromSkills } from "./pattern-loader"
13
15
  import type { PatternDefinition } from "./pattern-schema"
14
16
 
@@ -153,7 +155,7 @@ async function collectScvdMatches(
153
155
  return []
154
156
  }
155
157
 
156
- const indexPath = join(os.homedir(), ".cache", "solidity-argus", "scvd-index.json")
158
+ const indexPath = getScvdIndexPath()
157
159
  const index = await dependencies.loadIndexFn(indexPath)
158
160
 
159
161
  if (!index) {
@@ -241,20 +243,24 @@ function lineWindow(content: string, index: number): [number, number] {
241
243
  return [start, end]
242
244
  }
243
245
 
244
- export function findMatches(file: string, patterns: LoadedPattern[]): Match[] {
246
+ export function findMatches(file: string, patterns: LoadedPattern[], projectDir?: string): Match[] {
245
247
  const content = readFileSync(file, "utf8")
248
+ const normalizedFile = projectDir ? normalizeFilePath(file, projectDir) : file
246
249
  const matches: Match[] = []
247
250
 
248
251
  // Strip comments and string literals to reduce false positives.
249
- // Use a space-preserving approach so line numbers remain valid.
250
- // Order: multi-line comments first (can contain //), then single-line, then strings.
251
- const stripped = content
252
- .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
253
- .replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length))
254
- .replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, (m) => {
252
+ // Single-pass approach: match whichever construct appears first so that
253
+ // a "//" inside a string (e.g. URLs) is NOT treated as a comment.
254
+ const stripped = content.replace(
255
+ /\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g,
256
+ (m) => {
257
+ if (m.startsWith("/*")) return m.replace(/[^\n]/g, " ")
258
+ if (m.startsWith("//")) return " ".repeat(m.length)
259
+ // String literal — preserve quotes, blank interior
255
260
  const quote = m[0]
256
261
  return `${quote}${" ".repeat(Math.max(0, m.length - 2))}${quote}`
257
- })
262
+ },
263
+ )
258
264
 
259
265
  for (const pattern of patterns) {
260
266
  const regex = new RegExp(
@@ -266,7 +272,7 @@ export function findMatches(file: string, patterns: LoadedPattern[]): Match[] {
266
272
  matches.push({
267
273
  pattern: pattern.name,
268
274
  severity: pattern.severity,
269
- file,
275
+ file: normalizedFile,
270
276
  lines: lineWindow(content, index),
271
277
  description: pattern.description,
272
278
  exploitReference: pattern.exploitReference,
@@ -306,14 +312,24 @@ export async function executePatternCheck(
306
312
  context.metadata({ title: `Pattern check: ${args.target}` })
307
313
 
308
314
  const skillsDir = join(dirname(dirname(__dirname)), "skills")
309
- const skillDetectionRules = extractDetectionRulesFromSkills(skillsDir)
315
+ const { patterns: skillDetectionRules, errors: loaderErrors } =
316
+ extractDetectionRulesFromSkills(skillsDir)
317
+ if (loaderErrors.length > 0) {
318
+ for (const err of loaderErrors) {
319
+ logger.warn(`Pattern loader: ${err}`)
320
+ }
321
+ }
310
322
 
311
323
  const allPatterns: LoadedPattern[] = [
312
324
  ...normalizePatternDefinitions(skillDetectionRules, "skill"),
313
325
  ]
314
326
 
315
327
  const selectedPatterns = selectPatterns(allPatterns, args.patterns)
316
- const solidityFiles = collectSolidityFiles(args.target)
328
+ const baseProjectDir = resolveProjectDir(context)
329
+ const resolvedTarget = isAbsolute(args.target)
330
+ ? args.target
331
+ : resolve(baseProjectDir, args.target)
332
+ const solidityFiles = collectSolidityFiles(resolvedTarget)
317
333
  if (solidityFiles.length === 0) {
318
334
  return {
319
335
  success: false,
@@ -328,12 +344,18 @@ export async function executePatternCheck(
328
344
  }
329
345
  }
330
346
 
347
+ const absoluteTarget = resolvedTarget
348
+ let targetStat: ReturnType<typeof statSync> | undefined
349
+ try {
350
+ targetStat = statSync(absoluteTarget)
351
+ } catch {}
352
+ const normalizedProjectDir = targetStat?.isFile() ? dirname(absoluteTarget) : absoluteTarget
331
353
  const sourceMatches: Match[] = []
332
354
  for (const solidityFile of solidityFiles) {
333
355
  if (context.abort.aborted) {
334
356
  throw new Error("pattern check aborted")
335
357
  }
336
- sourceMatches.push(...findMatches(solidityFile, selectedPatterns))
358
+ sourceMatches.push(...findMatches(solidityFile, selectedPatterns, normalizedProjectDir))
337
359
  }
338
360
 
339
361
  const sources: MatchSource[] = [
@@ -36,9 +36,15 @@ function listSkillMarkdownFiles(skillsDir: string): string[] {
36
36
  return files
37
37
  }
38
38
 
39
- export function extractDetectionRulesFromSkills(skillsDir: string): PatternDefinition[] {
39
+ export interface PatternLoaderResult {
40
+ patterns: PatternDefinition[]
41
+ errors: string[]
42
+ }
43
+
44
+ export function extractDetectionRulesFromSkills(skillsDir: string): PatternLoaderResult {
40
45
  const skillFiles = listSkillMarkdownFiles(skillsDir)
41
46
  const extracted: PatternDefinition[] = []
47
+ const errors: string[] = []
42
48
 
43
49
  for (const filePath of skillFiles) {
44
50
  try {
@@ -47,7 +53,13 @@ export function extractDetectionRulesFromSkills(skillsDir: string): PatternDefin
47
53
  if (!frontmatter) continue
48
54
 
49
55
  const parsed = SkillFrontmatterSchema.safeParse(frontmatter)
50
- if (!parsed.success) continue
56
+ if (!parsed.success) {
57
+ const reason = parsed.error.issues.map((i) => i.message).join("; ")
58
+ const msg = `Failed to parse ${filePath}: ${reason}`
59
+ logger.warn(msg)
60
+ errors.push(msg)
61
+ continue
62
+ }
51
63
 
52
64
  const skillName = parsed.data.name
53
65
  const category = parsed.data.pattern_category
@@ -69,9 +81,11 @@ export function extractDetectionRulesFromSkills(skillsDir: string): PatternDefin
69
81
  })
70
82
  }
71
83
  } catch (err) {
72
- logger.warn(`Skipping ${filePath}: ${err instanceof Error ? err.message : "parse error"}`)
84
+ const msg = `Failed to parse ${filePath}: ${err instanceof Error ? err.message : "parse error"}`
85
+ logger.warn(msg)
86
+ errors.push(msg)
73
87
  }
74
88
  }
75
89
 
76
- return extracted
90
+ return { patterns: extracted, errors }
77
91
  }
@@ -0,0 +1,94 @@
1
+ import { mkdir, writeFile } from "node:fs/promises"
2
+ import { dirname } from "node:path"
3
+ import { type ToolContext, tool } from "@opencode-ai/plugin"
4
+ import { createAuditArtifactResolver } from "../shared/audit-artifact-resolver"
5
+ import { createLogger } from "../shared/logger"
6
+ import { resolveProjectDir } from "../shared/project-utils"
7
+ import { isNonEmptyString } from "../shared/type-guards"
8
+ import type { CanonicalFinding } from "../state/schemas"
9
+ import { SCHEMA_VERSION } from "../state/schemas"
10
+
11
+ type PersistDedupedArgs = {
12
+ run_id: string
13
+ deduped_findings: string
14
+ }
15
+
16
+ export interface DedupedFindingsArtifact {
17
+ run_id: string
18
+ schema_version: string
19
+ deduped_at: number
20
+ deduped_by: string
21
+ findings_count: number
22
+ findings: CanonicalFinding[]
23
+ }
24
+
25
+ export async function executePersistDeduped(
26
+ args: PersistDedupedArgs,
27
+ context: ToolContext,
28
+ ): Promise<string> {
29
+ const logger = createLogger()
30
+
31
+ if (!isNonEmptyString(args.run_id)) {
32
+ return JSON.stringify({ success: false, error: "run_id is required" })
33
+ }
34
+ if (!isNonEmptyString(args.deduped_findings)) {
35
+ return JSON.stringify({ success: false, error: "deduped_findings is required" })
36
+ }
37
+
38
+ let findings: CanonicalFinding[]
39
+ try {
40
+ const parsed = JSON.parse(args.deduped_findings)
41
+ findings = Array.isArray(parsed) ? parsed : parsed.findings
42
+ if (!Array.isArray(findings)) {
43
+ return JSON.stringify({
44
+ success: false,
45
+ error: "deduped_findings must be a JSON array or an object with a findings array",
46
+ })
47
+ }
48
+ } catch (err) {
49
+ return JSON.stringify({
50
+ success: false,
51
+ error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`,
52
+ })
53
+ }
54
+
55
+ const projectDir = resolveProjectDir(context)
56
+ const resolver = createAuditArtifactResolver(args.run_id, projectDir)
57
+ const dedupedPath = resolver.paths().dedupedFindingsFile
58
+
59
+ const artifact: DedupedFindingsArtifact = {
60
+ run_id: args.run_id,
61
+ schema_version: SCHEMA_VERSION,
62
+ deduped_at: Date.now(),
63
+ deduped_by: context.agent ?? "scribe",
64
+ findings_count: findings.length,
65
+ findings,
66
+ }
67
+
68
+ await mkdir(dirname(dedupedPath), { recursive: true })
69
+ await writeFile(dedupedPath, JSON.stringify(artifact, null, 2))
70
+ logger.debug(`Persisted ${findings.length} deduped findings to ${dedupedPath}`)
71
+
72
+ return JSON.stringify({
73
+ success: true,
74
+ path: dedupedPath,
75
+ findings_count: findings.length,
76
+ schema_version: SCHEMA_VERSION,
77
+ })
78
+ }
79
+
80
+ export const persistDedupedTool = tool({
81
+ description:
82
+ "Persist deduplicated and enriched findings to disk as the source-of-truth JSON artifact. Call this BEFORE argus_generate_report so the report tool can read from disk instead of requiring inline data.",
83
+ args: {
84
+ run_id: tool.schema.string().describe("The canonical run ID from <argus-context>."),
85
+ deduped_findings: tool.schema
86
+ .string()
87
+ .describe(
88
+ "Serialized JSON array of deduplicated and enriched findings. Each finding should have: check, severity, confidence, description, file, lines, source, impact, recommendation.",
89
+ ),
90
+ },
91
+ async execute(args, context) {
92
+ return executePersistDeduped(args, context)
93
+ },
94
+ })
@@ -115,49 +115,50 @@ function collectIndicators(source: string): Set<string> {
115
115
  return indicators
116
116
  }
117
117
 
118
- function hasAny(indicators: Set<string>, candidates: string[]): boolean {
119
- return candidates.some((candidate) => indicators.has(candidate))
120
- }
121
-
122
- function classifyProxyType(indicators: Set<string>): ProxyType | null {
123
- if (
124
- hasAny(indicators, ["diamond-cut", "diamond-cut-interface", "facet-address", "diamond-loupe"])
125
- ) {
126
- return "diamond"
127
- }
128
-
129
- if (
130
- hasAny(indicators, ["uups-authorize-upgrade", "uups-upgrade-to-and-call", "uups-upgradeable"])
131
- ) {
132
- return "uups"
133
- }
134
-
135
- if (hasAny(indicators, ["beacon-interface", "beacon-proxy", "upgradeable-beacon"])) {
136
- return "beacon"
137
- }
138
-
139
- if (
140
- hasAny(indicators, [
118
+ // Scored classification: each proxy type gets a score based on how many
119
+ // of its specific indicators are present. The type with the highest score
120
+ // wins. This avoids implicit order-dependency when multiple proxy types
121
+ // have matching indicators.
122
+ const PROXY_TYPE_INDICATORS: Array<{ type: ProxyType; indicators: string[] }> = [
123
+ {
124
+ type: "diamond",
125
+ indicators: ["diamond-cut", "diamond-cut-interface", "facet-address", "diamond-loupe"],
126
+ },
127
+ {
128
+ type: "uups",
129
+ indicators: ["uups-authorize-upgrade", "uups-upgrade-to-and-call", "uups-upgradeable"],
130
+ },
131
+ { type: "beacon", indicators: ["beacon-interface", "beacon-proxy", "upgradeable-beacon"] },
132
+ {
133
+ type: "transparent",
134
+ indicators: [
141
135
  "transparent-implementation-getter",
142
136
  "transparent-admin-getter",
143
137
  "transparent-set-implementation",
144
- ])
145
- ) {
146
- return "transparent"
147
- }
148
-
149
- if (
150
- hasAny(indicators, [
138
+ ],
139
+ },
140
+ {
141
+ type: "erc1967",
142
+ indicators: [
151
143
  "erc1967-implementation-slot",
152
144
  "erc1967-admin-slot",
153
145
  "erc1967-beacon-slot",
154
146
  "delegatecall",
155
- ])
156
- ) {
157
- return "erc1967"
147
+ ],
148
+ },
149
+ ]
150
+
151
+ function classifyProxyType(indicators: Set<string>): ProxyType | null {
152
+ let best: { type: ProxyType; score: number } | null = null
153
+
154
+ for (const entry of PROXY_TYPE_INDICATORS) {
155
+ const score = entry.indicators.filter((ind) => indicators.has(ind)).length
156
+ if (score > 0 && (!best || score > best.score)) {
157
+ best = { type: entry.type, score }
158
+ }
158
159
  }
159
160
 
160
- return null
161
+ return best?.type ?? null
161
162
  }
162
163
 
163
164
  export async function executeProxyDetection(