solidity-argus 0.1.0
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 +37 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/package.json +43 -0
- package/skills/INVENTORY.md +79 -0
- package/skills/README.md +56 -0
- package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +424 -0
- package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +157 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +373 -0
- package/skills/checklists/cyfrin-defi-integrations/SKILL.md +412 -0
- package/skills/checklists/cyfrin-gas/SKILL.md +55 -0
- package/skills/checklists/general-audit/SKILL.md +433 -0
- package/skills/methodology/audit-workflow/SKILL.md +129 -0
- package/skills/methodology/report-template/SKILL.md +190 -0
- package/skills/methodology/severity-classification/SKILL.md +179 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +229 -0
- package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +317 -0
- package/skills/protocol-patterns/dao-governance/SKILL.md +281 -0
- package/skills/protocol-patterns/lending-borrowing/SKILL.md +221 -0
- package/skills/protocol-patterns/staking-vesting/SKILL.md +247 -0
- package/skills/references/exploit-reference/SKILL.md +259 -0
- package/skills/references/smartbugs-examples/SKILL.md +296 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +298 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +59 -0
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +59 -0
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +61 -0
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +55 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +62 -0
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +60 -0
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +59 -0
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +72 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +249 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +51 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +52 -0
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +61 -0
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +60 -0
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +59 -0
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +61 -0
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +61 -0
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +333 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +60 -0
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +66 -0
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +67 -0
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +252 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +65 -0
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +61 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +266 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +72 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +59 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +63 -0
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +52 -0
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +65 -0
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +61 -0
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +63 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +56 -0
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +80 -0
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +69 -0
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +70 -0
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +81 -0
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +77 -0
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +294 -0
- package/src/agents/argus-prompt.ts +407 -0
- package/src/agents/pythia-prompt.ts +134 -0
- package/src/agents/scribe-prompt.ts +87 -0
- package/src/agents/sentinel-prompt.ts +133 -0
- package/src/cli/cli-program.ts +67 -0
- package/src/cli/commands/doctor.ts +83 -0
- package/src/cli/commands/init.ts +46 -0
- package/src/cli/commands/install.ts +55 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/tui-prompts.ts +75 -0
- package/src/cli/types.ts +9 -0
- package/src/config/index.ts +3 -0
- package/src/config/loader.ts +36 -0
- package/src/config/schema.ts +82 -0
- package/src/config/types.ts +4 -0
- package/src/constants/defaults.ts +6 -0
- package/src/create-hooks.ts +84 -0
- package/src/create-managers.ts +26 -0
- package/src/create-tools.ts +30 -0
- package/src/features/audit-enforcer/audit-enforcer.ts +34 -0
- package/src/features/audit-enforcer/index.ts +1 -0
- package/src/features/background-agent/background-manager.ts +200 -0
- package/src/features/background-agent/index.ts +1 -0
- package/src/features/context-monitor/context-monitor.ts +48 -0
- package/src/features/context-monitor/index.ts +4 -0
- package/src/features/context-monitor/tool-output-truncator.ts +17 -0
- package/src/features/error-recovery/index.ts +2 -0
- package/src/features/error-recovery/session-recovery.ts +27 -0
- package/src/features/error-recovery/tool-error-recovery.ts +35 -0
- package/src/features/index.ts +5 -0
- package/src/features/persistent-state/audit-state-manager.ts +121 -0
- package/src/features/persistent-state/index.ts +1 -0
- package/src/hooks/compaction-hook.ts +50 -0
- package/src/hooks/config-handler.ts +116 -0
- package/src/hooks/event-hook-v2.ts +93 -0
- package/src/hooks/event-hook.ts +74 -0
- package/src/hooks/hook-system.ts +9 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/knowledge-sync-hook.ts +57 -0
- package/src/hooks/safe-create-hook.ts +15 -0
- package/src/hooks/system-prompt-hook.ts +126 -0
- package/src/hooks/tool-tracking-hook.ts +234 -0
- package/src/hooks/types.ts +16 -0
- package/src/index.ts +36 -0
- package/src/knowledge/scvd-client.ts +242 -0
- package/src/knowledge/scvd-index.ts +183 -0
- package/src/knowledge/scvd-sync.ts +85 -0
- package/src/managers/index.ts +1 -0
- package/src/managers/types.ts +85 -0
- package/src/plugin-interface.ts +38 -0
- package/src/shared/binary-utils.ts +63 -0
- package/src/shared/deep-merge.ts +71 -0
- package/src/shared/file-utils.ts +56 -0
- package/src/shared/index.ts +5 -0
- package/src/shared/jsonc-parser.ts +39 -0
- package/src/shared/logger.ts +36 -0
- package/src/state/audit-state.ts +27 -0
- package/src/state/finding-store.ts +126 -0
- package/src/state/plugin-state.ts +14 -0
- package/src/state/types.ts +61 -0
- package/src/tools/contract-analyzer-tool.ts +184 -0
- package/src/tools/forge-fuzz-tool.ts +311 -0
- package/src/tools/forge-test-tool.ts +397 -0
- package/src/tools/pattern-checker-tool.ts +337 -0
- package/src/tools/report-generator-tool.ts +308 -0
- package/src/tools/slither-tool.ts +465 -0
- package/src/tools/solodit-search-tool.ts +131 -0
- package/src/tools/sync-knowledge-tool.ts +116 -0
- package/src/utils/project-detector.ts +133 -0
- package/src/utils/solidity-parser.ts +174 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AuditState, FindingSeverity } from "../state/types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a compaction hook that serializes audit state into XML format
|
|
5
|
+
* so findings survive context window compression.
|
|
6
|
+
*
|
|
7
|
+
* The returned hook is called by OpenCode's `experimental.session.compacting`
|
|
8
|
+
* event, receiving `{ summary: string }` and returning the enriched summary.
|
|
9
|
+
*/
|
|
10
|
+
export function createCompactionHook(
|
|
11
|
+
getAuditState: () => AuditState | null
|
|
12
|
+
): (input: { summary: string }) => Promise<string | null> {
|
|
13
|
+
return async (_input: { summary: string }): Promise<string | null> => {
|
|
14
|
+
const state = getAuditState()
|
|
15
|
+
if (!state) {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const severityCounts: Record<FindingSeverity, number> = {
|
|
20
|
+
Critical: 0,
|
|
21
|
+
High: 0,
|
|
22
|
+
Medium: 0,
|
|
23
|
+
Low: 0,
|
|
24
|
+
Informational: 0,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const finding of state.findings) {
|
|
28
|
+
severityCounts[finding.severity]++
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const toolNames = state.toolsExecuted.map((t) => t.tool).join(", ")
|
|
32
|
+
const contracts = state.contractsReviewed.join(", ")
|
|
33
|
+
const started = new Date(state.startTime).toISOString()
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
"<argus-audit-state>",
|
|
37
|
+
`Phase: ${state.currentPhase}`,
|
|
38
|
+
`Contracts Reviewed: ${contracts}`,
|
|
39
|
+
"Findings:",
|
|
40
|
+
` Critical: ${severityCounts.Critical}`,
|
|
41
|
+
` High: ${severityCounts.High}`,
|
|
42
|
+
` Medium: ${severityCounts.Medium}`,
|
|
43
|
+
` Low: ${severityCounts.Low}`,
|
|
44
|
+
` Informational: ${severityCounts.Informational}`,
|
|
45
|
+
`Tools Executed: ${toolNames}`,
|
|
46
|
+
`Started: ${started}`,
|
|
47
|
+
"</argus-audit-state>",
|
|
48
|
+
].join("\n")
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { resolve, join } from "node:path"
|
|
2
|
+
import { existsSync } from "node:fs"
|
|
3
|
+
import { homedir } from "node:os"
|
|
4
|
+
import { execSync } from "node:child_process"
|
|
5
|
+
import type { Config } from "@opencode-ai/sdk/v2"
|
|
6
|
+
import type { ArgusConfig } from "../config/types"
|
|
7
|
+
import { DEFAULT_MODELS } from "../constants/defaults"
|
|
8
|
+
import { createKnowledgeSyncHook } from "./knowledge-sync-hook"
|
|
9
|
+
import { ARGUS_PROMPT } from "../agents/argus-prompt"
|
|
10
|
+
import { SENTINEL_PROMPT } from "../agents/sentinel-prompt"
|
|
11
|
+
import { PYTHIA_PROMPT } from "../agents/pythia-prompt"
|
|
12
|
+
import { SCRIBE_PROMPT } from "../agents/scribe-prompt"
|
|
13
|
+
|
|
14
|
+
const TOB_CACHE_DIR = join(homedir(), ".cache", "solidity-argus", "trailofbits-skills")
|
|
15
|
+
const TOB_REPO_URL = "https://github.com/trailofbits/skills.git"
|
|
16
|
+
|
|
17
|
+
function ensureTrailOfBitsSkills(): string | undefined {
|
|
18
|
+
if (existsSync(TOB_CACHE_DIR)) return TOB_CACHE_DIR
|
|
19
|
+
try {
|
|
20
|
+
execSync(`git clone --depth 1 ${TOB_REPO_URL} "${TOB_CACHE_DIR}"`, {
|
|
21
|
+
stdio: "ignore",
|
|
22
|
+
timeout: 30_000,
|
|
23
|
+
})
|
|
24
|
+
return TOB_CACHE_DIR
|
|
25
|
+
} catch (_e) {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createConfigHandler(
|
|
31
|
+
argusConfig: ArgusConfig
|
|
32
|
+
): (config: Config) => Promise<void> {
|
|
33
|
+
const triggerKnowledgeSync = createKnowledgeSyncHook(argusConfig)
|
|
34
|
+
|
|
35
|
+
return async (config: Config): Promise<void> => {
|
|
36
|
+
config.agent = {
|
|
37
|
+
...config.agent,
|
|
38
|
+
argus: {
|
|
39
|
+
mode: "primary",
|
|
40
|
+
model: argusConfig.agents?.argus?.model ?? DEFAULT_MODELS.argus,
|
|
41
|
+
description: "Solidity security auditor — the All-Seeing Guardian",
|
|
42
|
+
prompt: ARGUS_PROMPT,
|
|
43
|
+
tools: {
|
|
44
|
+
"argus_*": false,
|
|
45
|
+
"solodit-mcp_*": false,
|
|
46
|
+
},
|
|
47
|
+
permission: {
|
|
48
|
+
task: {
|
|
49
|
+
sentinel: "allow",
|
|
50
|
+
pythia: "allow",
|
|
51
|
+
scribe: "allow",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
sentinel: {
|
|
56
|
+
mode: "subagent",
|
|
57
|
+
model: argusConfig.agents?.sentinel?.model ?? DEFAULT_MODELS.sentinel,
|
|
58
|
+
description: "Static analysis and testing specialist",
|
|
59
|
+
prompt: SENTINEL_PROMPT,
|
|
60
|
+
tools: {
|
|
61
|
+
argus_slither_analyze: true,
|
|
62
|
+
argus_forge_test: true,
|
|
63
|
+
argus_forge_fuzz: true,
|
|
64
|
+
argus_analyze_contract: true,
|
|
65
|
+
argus_check_patterns: true,
|
|
66
|
+
} satisfies Record<string, boolean>,
|
|
67
|
+
},
|
|
68
|
+
pythia: {
|
|
69
|
+
mode: "subagent",
|
|
70
|
+
model: argusConfig.agents?.pythia?.model ?? DEFAULT_MODELS.pythia,
|
|
71
|
+
description: "Vulnerability researcher",
|
|
72
|
+
prompt: PYTHIA_PROMPT,
|
|
73
|
+
tools: {
|
|
74
|
+
argus_solodit_search: true,
|
|
75
|
+
argus_check_patterns: true,
|
|
76
|
+
} satisfies Record<string, boolean>,
|
|
77
|
+
},
|
|
78
|
+
scribe: {
|
|
79
|
+
mode: "subagent",
|
|
80
|
+
model: argusConfig.agents?.scribe?.model ?? DEFAULT_MODELS.scribe,
|
|
81
|
+
description: "Audit report writer",
|
|
82
|
+
prompt: SCRIBE_PROMPT,
|
|
83
|
+
tools: {
|
|
84
|
+
argus_generate_report: true,
|
|
85
|
+
} satisfies Record<string, boolean>,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (argusConfig.solodit?.enabled !== false) {
|
|
90
|
+
const port = argusConfig.solodit?.port ?? 3000
|
|
91
|
+
config.mcp = {
|
|
92
|
+
...(config.mcp ?? {}),
|
|
93
|
+
"solodit-mcp": {
|
|
94
|
+
type: "remote",
|
|
95
|
+
url: `http://localhost:${port}/mcp`,
|
|
96
|
+
enabled: true,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const skillsPaths = [...(config.skills?.paths ?? [])]
|
|
102
|
+
skillsPaths.push(resolve(import.meta.dir, "../../skills"))
|
|
103
|
+
|
|
104
|
+
const tobDir = ensureTrailOfBitsSkills()
|
|
105
|
+
if (tobDir) skillsPaths.push(tobDir)
|
|
106
|
+
|
|
107
|
+
config.skills = {
|
|
108
|
+
...(config.skills ?? {}),
|
|
109
|
+
paths: skillsPaths,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (argusConfig.knowledge?.autoSync !== false) {
|
|
113
|
+
triggerKnowledgeSync()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { AuditState, AuditPhase } from "../state/types"
|
|
2
|
+
import { createAuditState } from "../state/audit-state"
|
|
3
|
+
import { createLogger } from "../shared/logger"
|
|
4
|
+
|
|
5
|
+
export type AuditEventType =
|
|
6
|
+
| "session.created"
|
|
7
|
+
| "session.idle"
|
|
8
|
+
| "session.error"
|
|
9
|
+
| "session.deleted"
|
|
10
|
+
| "audit.phase-changed"
|
|
11
|
+
| "audit.finding-added"
|
|
12
|
+
| "audit.complete"
|
|
13
|
+
|
|
14
|
+
export type EventHookV2Fn = (input: {
|
|
15
|
+
event: { type: string; sessionId?: string; properties?: Record<string, unknown> }
|
|
16
|
+
}) => Promise<void>
|
|
17
|
+
|
|
18
|
+
export type EventSubHandler = (event: {
|
|
19
|
+
type: string
|
|
20
|
+
sessionId?: string
|
|
21
|
+
auditState: AuditState | null
|
|
22
|
+
}) => Promise<void>
|
|
23
|
+
|
|
24
|
+
export function createEventHookV2(
|
|
25
|
+
projectDir?: string,
|
|
26
|
+
subHandlers: EventSubHandler[] = [],
|
|
27
|
+
): {
|
|
28
|
+
hook: EventHookV2Fn
|
|
29
|
+
getAuditState: () => AuditState | null
|
|
30
|
+
setAuditState: (state: AuditState | null) => void
|
|
31
|
+
} {
|
|
32
|
+
const logger = createLogger()
|
|
33
|
+
let currentAuditState: AuditState | null = null
|
|
34
|
+
|
|
35
|
+
const getAuditState = (): AuditState | null => currentAuditState
|
|
36
|
+
const setAuditState = (state: AuditState | null): void => {
|
|
37
|
+
currentAuditState = state
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const hook: EventHookV2Fn = async (input): Promise<void> => {
|
|
41
|
+
const { type, sessionId } = input.event
|
|
42
|
+
|
|
43
|
+
switch (type) {
|
|
44
|
+
case "session.created": {
|
|
45
|
+
const dir = projectDir ?? process.cwd()
|
|
46
|
+
const { state } = createAuditState(dir)
|
|
47
|
+
currentAuditState = state
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
case "session.idle": {
|
|
52
|
+
if (currentAuditState) {
|
|
53
|
+
logger.debug(
|
|
54
|
+
`Session idle — phase: ${currentAuditState.currentPhase}, findings: ${currentAuditState.findings.length}`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case "session.error": {
|
|
61
|
+
if (currentAuditState) {
|
|
62
|
+
logger.error(
|
|
63
|
+
`Session error — state snapshot: ${JSON.stringify({
|
|
64
|
+
sessionId: currentAuditState.sessionId,
|
|
65
|
+
phase: currentAuditState.currentPhase,
|
|
66
|
+
findingsCount: currentAuditState.findings.length,
|
|
67
|
+
contractsReviewed: currentAuditState.contractsReviewed,
|
|
68
|
+
})}`,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
case "session.deleted": {
|
|
75
|
+
currentAuditState = null
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
default:
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const handler of subHandlers) {
|
|
84
|
+
try {
|
|
85
|
+
await handler({ type, sessionId, auditState: currentAuditState })
|
|
86
|
+
} catch (error) {
|
|
87
|
+
logger.error(`Sub-handler failed for event ${type}:`, error)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { hook, getAuditState, setAuditState }
|
|
93
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AuditState } from "../state/types"
|
|
2
|
+
import { createAuditState } from "../state/audit-state"
|
|
3
|
+
|
|
4
|
+
export type EventHookFn = (input: {
|
|
5
|
+
event: { type: string; sessionId?: string }
|
|
6
|
+
}) => Promise<void>
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a session lifecycle event hook that manages audit state.
|
|
10
|
+
*
|
|
11
|
+
* Returns the hook function plus accessors for reading/writing the
|
|
12
|
+
* closure-held audit state. Other hooks (compaction, tool tracking,
|
|
13
|
+
* system prompt) share the same state instance via these accessors.
|
|
14
|
+
*/
|
|
15
|
+
export function createEventHook(projectDir?: string): {
|
|
16
|
+
hook: EventHookFn
|
|
17
|
+
getAuditState: () => AuditState | null
|
|
18
|
+
setAuditState: (state: AuditState | null) => void
|
|
19
|
+
} {
|
|
20
|
+
let currentAuditState: AuditState | null = null
|
|
21
|
+
|
|
22
|
+
const getAuditState = (): AuditState | null => currentAuditState
|
|
23
|
+
|
|
24
|
+
const setAuditState = (state: AuditState | null): void => {
|
|
25
|
+
currentAuditState = state
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const hook: EventHookFn = async (input): Promise<void> => {
|
|
29
|
+
const { type } = input.event
|
|
30
|
+
|
|
31
|
+
switch (type) {
|
|
32
|
+
case "session.created": {
|
|
33
|
+
const dir = projectDir ?? process.cwd()
|
|
34
|
+
const { state } = createAuditState(dir)
|
|
35
|
+
currentAuditState = state
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case "session.idle": {
|
|
40
|
+
if (currentAuditState) {
|
|
41
|
+
console.error(
|
|
42
|
+
`[argus-state] Session idle — phase: ${currentAuditState.currentPhase}, findings: ${currentAuditState.findings.length}, contracts: ${currentAuditState.contractsReviewed.length}`
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case "session.error": {
|
|
49
|
+
if (currentAuditState) {
|
|
50
|
+
console.error(
|
|
51
|
+
`[argus-error] Session error — state snapshot: ${JSON.stringify({
|
|
52
|
+
sessionId: currentAuditState.sessionId,
|
|
53
|
+
phase: currentAuditState.currentPhase,
|
|
54
|
+
findingsCount: currentAuditState.findings.length,
|
|
55
|
+
contractsReviewed: currentAuditState.contractsReviewed,
|
|
56
|
+
})}`
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case "session.deleted": {
|
|
63
|
+
currentAuditState = null
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Unknown events: no-op — never throw
|
|
68
|
+
default:
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { hook, getAuditState, setAuditState }
|
|
74
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createHookGuard } from "./hook-system";
|
|
2
|
+
export { safeCreateHook } from "./safe-create-hook";
|
|
3
|
+
export { createEventHookV2 } from "./event-hook-v2";
|
|
4
|
+
export type { HookName } from "./types";
|
|
5
|
+
export type { EventHookV2Fn, AuditEventType, EventSubHandler } from "./event-hook-v2";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os from "node:os"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { ScvdClient } from "../knowledge/scvd-client"
|
|
4
|
+
import { syncIncremental, type SyncResult } from "../knowledge/scvd-sync"
|
|
5
|
+
import type { ArgusConfig } from "../config/types"
|
|
6
|
+
|
|
7
|
+
export type KnowledgeSyncDependencies = {
|
|
8
|
+
createClient?: (apiUrl: string) => unknown
|
|
9
|
+
syncIncrementalFn?: (client: unknown, indexPath: string) => Promise<SyncResult>
|
|
10
|
+
log?: (message: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_SCVD_API_URL = "https://api.scvd.dev"
|
|
14
|
+
|
|
15
|
+
function defaultDependencies(): Required<KnowledgeSyncDependencies> {
|
|
16
|
+
return {
|
|
17
|
+
createClient: (apiUrl: string) => new ScvdClient(apiUrl),
|
|
18
|
+
syncIncrementalFn: async (client: unknown, indexPath: string) =>
|
|
19
|
+
syncIncremental(client as ScvdClient, indexPath),
|
|
20
|
+
log: (message: string) => {
|
|
21
|
+
console.error(message)
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createKnowledgeSyncHook(
|
|
27
|
+
argusConfig: ArgusConfig,
|
|
28
|
+
deps: KnowledgeSyncDependencies = {}
|
|
29
|
+
): () => void {
|
|
30
|
+
const dependencies = { ...defaultDependencies(), ...deps }
|
|
31
|
+
|
|
32
|
+
return function triggerAutoSync(): void {
|
|
33
|
+
if (!argusConfig.knowledge?.scvd?.enabled) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const apiUrl = argusConfig.knowledge?.scvd?.apiUrl ?? DEFAULT_SCVD_API_URL
|
|
38
|
+
const indexPath = path.join(
|
|
39
|
+
os.homedir(),
|
|
40
|
+
".cache",
|
|
41
|
+
"solidity-argus",
|
|
42
|
+
"scvd-index.json"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
Promise.resolve().then(async () => {
|
|
46
|
+
try {
|
|
47
|
+
const client = dependencies.createClient(apiUrl)
|
|
48
|
+
const result = await dependencies.syncIncrementalFn(client, indexPath)
|
|
49
|
+
if (result.newFindings > 0) {
|
|
50
|
+
dependencies.log(
|
|
51
|
+
`[argus] SCVD index updated: ${result.newFindings} new findings (total: ${result.totalIndexed})`
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
} catch (_e) { /* non-critical: sync errors are logged above */ }
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function safeCreateHook<T>(
|
|
2
|
+
factory: () => T,
|
|
3
|
+
hookName: string
|
|
4
|
+
): T | undefined {
|
|
5
|
+
try {
|
|
6
|
+
return factory();
|
|
7
|
+
} catch (error) {
|
|
8
|
+
console.error(
|
|
9
|
+
`[argus-hook-error] Failed to create hook "${hookName}": ${
|
|
10
|
+
error instanceof Error ? error.message : String(error)
|
|
11
|
+
}`
|
|
12
|
+
);
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { AuditState, FindingSeverity } from "../state/types";
|
|
3
|
+
|
|
4
|
+
interface SystemPromptInput {
|
|
5
|
+
system: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Checks if the given directory contains a Solidity project
|
|
11
|
+
* by looking for foundry.toml or hardhat.config.{js,ts}
|
|
12
|
+
*/
|
|
13
|
+
async function isSolidityProject(cwd: string): Promise<boolean> {
|
|
14
|
+
const checks = [
|
|
15
|
+
Bun.file(join(cwd, "foundry.toml")).exists(),
|
|
16
|
+
Bun.file(join(cwd, "hardhat.config.js")).exists(),
|
|
17
|
+
Bun.file(join(cwd, "hardhat.config.ts")).exists(),
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const results = await Promise.all(checks);
|
|
21
|
+
return results.some(Boolean);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Counts findings by severity from the audit state
|
|
26
|
+
*/
|
|
27
|
+
function countFindingsBySeverity(
|
|
28
|
+
findings: AuditState["findings"]
|
|
29
|
+
): Record<FindingSeverity, number> {
|
|
30
|
+
const counts: Record<FindingSeverity, number> = {
|
|
31
|
+
Critical: 0,
|
|
32
|
+
High: 0,
|
|
33
|
+
Medium: 0,
|
|
34
|
+
Low: 0,
|
|
35
|
+
Informational: 0,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
for (const finding of findings) {
|
|
39
|
+
counts[finding.severity]++;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return counts;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds the audit state summary section for the injected context
|
|
47
|
+
*/
|
|
48
|
+
function buildAuditStateSummary(state: AuditState | null): string {
|
|
49
|
+
if (!state) {
|
|
50
|
+
return "No active audit session. Use @argus to start an audit.";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const counts = countFindingsBySeverity(state.findings);
|
|
54
|
+
const scopeList =
|
|
55
|
+
state.scope.length > 0 ? state.scope.join(", ") : "not defined";
|
|
56
|
+
const reviewedList =
|
|
57
|
+
state.contractsReviewed.length > 0
|
|
58
|
+
? state.contractsReviewed.join(", ")
|
|
59
|
+
: "none yet";
|
|
60
|
+
|
|
61
|
+
return [
|
|
62
|
+
`Phase: ${state.currentPhase}`,
|
|
63
|
+
`Scope: ${scopeList}`,
|
|
64
|
+
`Contracts reviewed: ${reviewedList}`,
|
|
65
|
+
`Findings: ${state.findings.length} total — Critical: ${counts.Critical}, High: ${counts.High}, Medium: ${counts.Medium}, Low: ${counts.Low}, Info: ${counts.Informational}`,
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Builds the full audit context block to inject into the system prompt.
|
|
71
|
+
* Designed to be concise (500-800 tokens).
|
|
72
|
+
*/
|
|
73
|
+
function buildAuditContextBlock(state: AuditState | null): string {
|
|
74
|
+
return `
|
|
75
|
+
<argus-context>
|
|
76
|
+
## Solidity Audit Context
|
|
77
|
+
|
|
78
|
+
### Severity Classification
|
|
79
|
+
- **Critical**: Direct theft/freezing of funds, unauthorized admin access, contract destruction
|
|
80
|
+
- **High**: Indirect fund loss, business logic manipulation, DoS on critical functions
|
|
81
|
+
- **Medium**: Degraded functionality, edge-case bugs, partial DoS, poor validation
|
|
82
|
+
- **Low**: Code quality issues, suboptimal patterns, missing events, minor logic issues
|
|
83
|
+
- **Informational**: Gas optimizations, style suggestions, best practices, non-security notes
|
|
84
|
+
|
|
85
|
+
### Available Argus Tools
|
|
86
|
+
- \`argus_slither_analyze\`: Run Slither static analysis on Solidity codebase
|
|
87
|
+
- \`argus_forge_test\`: Execute Foundry/Forge tests for vulnerability verification
|
|
88
|
+
- \`argus_forge_fuzz\`: Fuzz specific functions to discover edge cases
|
|
89
|
+
- \`argus_analyze_contract\`: Generate deep structural profile of a contract
|
|
90
|
+
- \`argus_check_patterns\`: Scan code against known vulnerability pattern library
|
|
91
|
+
- \`argus_solodit_search\`: Search real-world audit reports and known vulnerabilities
|
|
92
|
+
- \`argus_generate_report\`: Compile findings into structured audit report
|
|
93
|
+
- \`argus_sync_knowledge\`: Update local vulnerability database (SCVD)
|
|
94
|
+
|
|
95
|
+
### Audit State
|
|
96
|
+
${buildAuditStateSummary(state)}
|
|
97
|
+
|
|
98
|
+
### Quick Reference
|
|
99
|
+
Use @argus for full audits, @sentinel for testing, @pythia for research, @scribe for reports.
|
|
100
|
+
Severity must follow classification above. Do not inflate severity.
|
|
101
|
+
</argus-context>`.trim();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Factory function that creates a system prompt transform hook.
|
|
106
|
+
* The hook injects Solidity audit context when working in a Solidity project.
|
|
107
|
+
*
|
|
108
|
+
* @param getAuditState - Accessor function for current audit state (may return null)
|
|
109
|
+
* @returns Async transform function compatible with OpenCode's experimental.chat.system.transform
|
|
110
|
+
*/
|
|
111
|
+
export function createSystemPromptHook(
|
|
112
|
+
getAuditState: () => AuditState | null
|
|
113
|
+
): (input: SystemPromptInput) => Promise<string | null> {
|
|
114
|
+
return async (input: SystemPromptInput): Promise<string | null> => {
|
|
115
|
+
const isSolidity = await isSolidityProject(input.cwd);
|
|
116
|
+
|
|
117
|
+
if (!isSolidity) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const auditState = getAuditState();
|
|
122
|
+
return buildAuditContextBlock(auditState);
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export default createSystemPromptHook;
|