solidity-argus 0.1.8 → 0.3.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 +3 -3
- package/README.md +229 -13
- package/package.json +37 -8
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +72 -6
- package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
- package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
- package/skills/case-studies/cream-finance/SKILL.md +52 -0
- package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
- package/skills/case-studies/dao-hack/SKILL.md +51 -0
- package/skills/case-studies/euler-finance/SKILL.md +52 -0
- package/skills/case-studies/harvest-finance/SKILL.md +52 -0
- package/skills/case-studies/level-finance/SKILL.md +51 -0
- package/skills/case-studies/mango-markets/SKILL.md +53 -0
- package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
- package/skills/case-studies/parity-multisig/SKILL.md +55 -0
- package/skills/case-studies/poly-network/SKILL.md +51 -0
- package/skills/case-studies/rari-fuse/SKILL.md +51 -0
- package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
- package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
- package/skills/manifests/cyfrin.json +16 -0
- package/skills/manifests/defifofum.json +25 -0
- package/skills/manifests/kadenzipfel.json +48 -0
- package/skills/manifests/scvd.json +9 -0
- package/skills/manifests/smartbugs.json +9 -0
- package/skills/manifests/solodit.json +9 -0
- package/skills/manifests/sunweb3sec.json +9 -0
- package/skills/manifests/trailofbits.json +9 -0
- package/skills/methodology/audit-workflow/SKILL.md +3 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
- package/skills/references/exploit-reference/SKILL.md +3 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +27 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +8 -1
- package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +14 -1
- package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
- package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +13 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
- package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
- package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
- package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
- package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +22 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +11 -1
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +22 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +11 -1
- package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +13 -1
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
- package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
- package/src/agents/argus-prompt.ts +27 -10
- package/src/agents/pythia-prompt.ts +7 -8
- package/src/agents/scribe-prompt.ts +10 -5
- package/src/agents/sentinel-prompt.ts +36 -7
- package/src/cli/cli-output.ts +16 -0
- package/src/cli/cli-program.ts +29 -22
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +303 -23
- package/src/cli/commands/init.ts +8 -6
- package/src/cli/commands/install.ts +10 -8
- package/src/cli/commands/lint-skills.ts +118 -0
- package/src/cli/index.ts +5 -5
- package/src/cli/tui-prompts.ts +4 -2
- package/src/cli/types.ts +3 -3
- package/src/config/index.ts +1 -1
- package/src/config/loader.ts +4 -6
- package/src/config/schema.ts +6 -5
- package/src/config/types.ts +2 -2
- package/src/constants/defaults.ts +2 -0
- package/src/create-hooks.ts +225 -29
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +14 -8
- package/src/features/background-agent/background-manager.ts +93 -87
- package/src/features/background-agent/index.ts +1 -1
- package/src/features/context-monitor/context-monitor.ts +3 -3
- package/src/features/context-monitor/index.ts +2 -2
- package/src/features/error-recovery/session-recovery.ts +2 -4
- package/src/features/error-recovery/tool-error-recovery.ts +79 -19
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +158 -52
- package/src/features/persistent-state/global-run-index.ts +38 -0
- package/src/features/persistent-state/index.ts +1 -1
- package/src/features/persistent-state/run-journal.ts +86 -0
- package/src/hooks/agent-tracker.ts +53 -0
- package/src/hooks/compaction-hook.ts +46 -37
- package/src/hooks/config-handler.ts +31 -11
- package/src/hooks/context-budget.ts +42 -0
- package/src/hooks/event-hook.ts +48 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +19 -21
- package/src/hooks/recon-context-builder.ts +66 -0
- package/src/hooks/safe-create-hook.ts +9 -11
- package/src/hooks/system-prompt-hook.ts +128 -0
- package/src/hooks/tool-tracking-hook.ts +162 -29
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +23 -13
- package/src/knowledge/retry.ts +53 -0
- package/src/knowledge/scvd-client.ts +103 -83
- package/src/knowledge/scvd-errors.ts +89 -0
- package/src/knowledge/scvd-index.ts +110 -62
- package/src/knowledge/scvd-sync.ts +223 -47
- package/src/knowledge/source-manifest.ts +102 -0
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +19 -8
- package/src/shared/binary-utils.ts +44 -34
- package/src/shared/deep-merge.ts +55 -36
- package/src/shared/file-utils.ts +21 -19
- package/src/shared/index.ts +11 -5
- package/src/shared/jsonc-parser.ts +123 -28
- package/src/shared/logger.ts +91 -17
- package/src/shared/project-utils.ts +30 -0
- package/src/skills/analysis/cluster.ts +414 -0
- package/src/skills/analysis/gates.ts +227 -0
- package/src/skills/analysis/index.ts +33 -0
- package/src/skills/analysis/normalize.ts +217 -0
- package/src/skills/analysis/similarity.ts +224 -0
- package/src/skills/argus-skill-resolver.ts +237 -0
- package/src/skills/skill-schema.ts +99 -0
- package/src/solodit-lifecycle.ts +202 -0
- package/src/state/audit-state.ts +10 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +96 -44
- package/src/tools/argus-skill-load-tool.ts +78 -0
- package/src/tools/contract-analyzer-tool.ts +60 -77
- package/src/tools/forge-coverage-tool.ts +226 -0
- package/src/tools/forge-fuzz-tool.ts +127 -127
- package/src/tools/forge-test-tool.ts +153 -157
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +206 -167
- package/src/tools/pattern-loader.ts +77 -0
- package/src/tools/pattern-schema.ts +51 -0
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +333 -142
- package/src/tools/slither-tool.ts +300 -210
- package/src/tools/solodit-search-tool.ts +255 -80
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +118 -0
- package/src/utils/dependency-scanner.ts +93 -0
- package/src/utils/project-detector.ts +175 -86
- package/src/utils/solidity-parser.ts +112 -67
- package/src/utils/solodit-health.ts +29 -0
- package/src/hooks/event-hook-v2.ts +0 -99
- package/src/state/plugin-state.ts +0 -14
package/src/cli/cli-program.ts
CHANGED
|
@@ -1,45 +1,52 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { cliOutput } from "./cli-output"
|
|
2
|
+
import { checkSkillsCommand } from "./commands/check-skills"
|
|
3
|
+
import { doctorCommand } from "./commands/doctor"
|
|
4
|
+
import { initCommand } from "./commands/init"
|
|
5
|
+
import { installCommand } from "./commands/install"
|
|
6
|
+
import { lintSkillsCommand } from "./commands/lint-skills"
|
|
7
|
+
import type { CliCommand } from "./types"
|
|
5
8
|
|
|
6
9
|
const HELP_TEXT = `argus — Solidity Security Auditor for OpenCode
|
|
7
10
|
|
|
8
11
|
Commands:
|
|
9
|
-
doctor
|
|
10
|
-
init
|
|
11
|
-
install
|
|
12
|
-
|
|
12
|
+
doctor Check Slither/Foundry installation and config health
|
|
13
|
+
init Create solidity-argus config file
|
|
14
|
+
install Configure argus plugin in opencode config
|
|
15
|
+
lint-skills Validate SKILL.md files against schema
|
|
16
|
+
check-skills Analyze skills for duplicates, near-duplicates, and conflicts
|
|
17
|
+
`
|
|
13
18
|
|
|
14
19
|
export class CliProgram {
|
|
15
|
-
private commands: Map<string, CliCommand> = new Map()
|
|
20
|
+
private commands: Map<string, CliCommand> = new Map()
|
|
16
21
|
|
|
17
22
|
registerCommand(command: CliCommand): void {
|
|
18
|
-
this.commands.set(command.name, command)
|
|
23
|
+
this.commands.set(command.name, command)
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
async dispatch(args: string[]): Promise<number> {
|
|
22
|
-
const subcommand = args[0]
|
|
27
|
+
const subcommand = args[0]
|
|
23
28
|
|
|
24
29
|
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
25
|
-
|
|
26
|
-
return 0
|
|
30
|
+
cliOutput.log(HELP_TEXT)
|
|
31
|
+
return 0
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
const command = this.commands.get(subcommand)
|
|
34
|
+
const command = this.commands.get(subcommand)
|
|
30
35
|
if (!command) {
|
|
31
|
-
|
|
32
|
-
return 1
|
|
36
|
+
cliOutput.error(`Unknown command '${subcommand}'. Run 'argus' for help.`)
|
|
37
|
+
return 1
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
return command.execute(args.slice(1))
|
|
40
|
+
return command.execute(args.slice(1))
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export function createCliProgram(): CliProgram {
|
|
40
|
-
const program = new CliProgram()
|
|
41
|
-
program.registerCommand(doctorCommand)
|
|
42
|
-
program.registerCommand(initCommand)
|
|
43
|
-
program.registerCommand(installCommand)
|
|
44
|
-
|
|
45
|
+
const program = new CliProgram()
|
|
46
|
+
program.registerCommand(doctorCommand)
|
|
47
|
+
program.registerCommand(initCommand)
|
|
48
|
+
program.registerCommand(installCommand)
|
|
49
|
+
program.registerCommand(lintSkillsCommand)
|
|
50
|
+
program.registerCommand(checkSkillsCommand)
|
|
51
|
+
return program
|
|
45
52
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { loadArgusConfig } from "../../config/loader"
|
|
4
|
+
import { createLogger } from "../../shared/logger"
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_GATE_CONFIG,
|
|
7
|
+
formatReportJson,
|
|
8
|
+
formatReportText,
|
|
9
|
+
type GateConfig,
|
|
10
|
+
generateReport,
|
|
11
|
+
type SkillReport,
|
|
12
|
+
} from "../../skills/analysis/gates"
|
|
13
|
+
import { normalizeSkill, type SkillDoc } from "../../skills/analysis/normalize"
|
|
14
|
+
import { buildTfidfCorpus, computeAllPairs } from "../../skills/analysis/similarity"
|
|
15
|
+
import { resolveSkillRoots } from "../../skills/argus-skill-resolver"
|
|
16
|
+
import { cliOutput } from "../cli-output"
|
|
17
|
+
import type { CliCommand } from "../types"
|
|
18
|
+
|
|
19
|
+
const logger = createLogger()
|
|
20
|
+
|
|
21
|
+
function findSkillFiles(dir: string, maxDepth = 8): string[] {
|
|
22
|
+
const files: string[] = []
|
|
23
|
+
const stack: Array<{ path: string; depth: number }> = [{ path: dir, depth: 0 }]
|
|
24
|
+
|
|
25
|
+
while (stack.length > 0) {
|
|
26
|
+
const current = stack.pop()
|
|
27
|
+
if (!current || current.depth > maxDepth) continue
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const entries = readdirSync(current.path, { withFileTypes: true })
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const fullPath = join(current.path, entry.name)
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
stack.push({ path: fullPath, depth: current.depth + 1 })
|
|
35
|
+
} else if (entry.isFile() && entry.name.toUpperCase() === "SKILL.MD") {
|
|
36
|
+
files.push(fullPath)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return files
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseFormatArg(args: string[]): "text" | "json" {
|
|
46
|
+
const formatIdx = args.indexOf("--format")
|
|
47
|
+
if (formatIdx !== -1 && args[formatIdx + 1] === "json") {
|
|
48
|
+
return "json"
|
|
49
|
+
}
|
|
50
|
+
return "text"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseThresholdArg(args: string[], flag: string, fallback: number): number {
|
|
54
|
+
const idx = args.indexOf(flag)
|
|
55
|
+
if (idx === -1) return fallback
|
|
56
|
+
const raw = args[idx + 1]
|
|
57
|
+
if (!raw) return fallback
|
|
58
|
+
const parsed = Number.parseFloat(raw)
|
|
59
|
+
return Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : fallback
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function loadAndNormalizeSkills(cwd: string): SkillDoc[] {
|
|
63
|
+
let config: ReturnType<typeof loadArgusConfig> | undefined
|
|
64
|
+
try {
|
|
65
|
+
config = loadArgusConfig(cwd)
|
|
66
|
+
} catch {
|
|
67
|
+
logger.debug("Config load failed, using defaults")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const roots = resolveSkillRoots(cwd, config)
|
|
71
|
+
const docs: SkillDoc[] = []
|
|
72
|
+
|
|
73
|
+
for (const root of roots) {
|
|
74
|
+
const files = findSkillFiles(root.path)
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
try {
|
|
77
|
+
const content = readFileSync(file, "utf8")
|
|
78
|
+
const doc = normalizeSkill(content)
|
|
79
|
+
if (doc) {
|
|
80
|
+
docs.push(doc)
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
logger.debug("Skipping unreadable skill file")
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return docs
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function runAnalysis(docs: SkillDoc[], config: GateConfig): SkillReport {
|
|
92
|
+
const corpus = buildTfidfCorpus(docs)
|
|
93
|
+
const pairs = computeAllPairs(docs, corpus)
|
|
94
|
+
return generateReport(docs, pairs, config)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const checkSkillsCommand: CliCommand = {
|
|
98
|
+
name: "check-skills",
|
|
99
|
+
description:
|
|
100
|
+
"Analyze SKILL.md files for duplicates, near-duplicates, and detection rule conflicts",
|
|
101
|
+
async execute(args: string[]): Promise<number> {
|
|
102
|
+
const cwd = process.cwd()
|
|
103
|
+
const format = parseFormatArg(args)
|
|
104
|
+
|
|
105
|
+
const gateConfig: GateConfig = {
|
|
106
|
+
blockThreshold: parseThresholdArg(
|
|
107
|
+
args,
|
|
108
|
+
"--block-threshold",
|
|
109
|
+
DEFAULT_GATE_CONFIG.blockThreshold,
|
|
110
|
+
),
|
|
111
|
+
warnThreshold: parseThresholdArg(args, "--warn-threshold", DEFAULT_GATE_CONFIG.warnThreshold),
|
|
112
|
+
infoThreshold: parseThresholdArg(args, "--info-threshold", DEFAULT_GATE_CONFIG.infoThreshold),
|
|
113
|
+
blockExactRegexConflict: !args.includes("--no-regex-conflict"),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const docs = loadAndNormalizeSkills(cwd)
|
|
117
|
+
|
|
118
|
+
if (docs.length === 0) {
|
|
119
|
+
cliOutput.log("No SKILL.md files found.")
|
|
120
|
+
return 0
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cliOutput.log(`Analyzing ${docs.length} skills...`)
|
|
124
|
+
|
|
125
|
+
const report = runAnalysis(docs, gateConfig)
|
|
126
|
+
|
|
127
|
+
if (format === "json") {
|
|
128
|
+
cliOutput.log(formatReportJson(report))
|
|
129
|
+
} else {
|
|
130
|
+
cliOutput.log(formatReportText(report))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return report.summary.block > 0 ? 1 : 0
|
|
134
|
+
},
|
|
135
|
+
}
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { join } from "node:path"
|
|
4
|
-
import type { CliCommand } from "../types"
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
2
|
+
import { basename, dirname, extname, join } from "node:path"
|
|
5
3
|
import { loadArgusConfig } from "../../config/loader"
|
|
4
|
+
import type { ArgusConfig } from "../../config/types"
|
|
5
|
+
import { createLogger } from "../../shared/logger"
|
|
6
|
+
import {
|
|
7
|
+
getRequiredAuditSkills,
|
|
8
|
+
normalizeSkillName,
|
|
9
|
+
type ResolvedSkill,
|
|
10
|
+
resolveArgusSkills,
|
|
11
|
+
resolveSkillRoots,
|
|
12
|
+
} from "../../skills/argus-skill-resolver"
|
|
13
|
+
import { parseFrontmatter, validateSkillFrontmatter } from "../../skills/skill-schema"
|
|
14
|
+
import { detectViaIr } from "../../tools/slither-tool"
|
|
15
|
+
import { checkSoloditHealth } from "../../utils/solodit-health"
|
|
16
|
+
import { cliOutput } from "../cli-output"
|
|
17
|
+
import type { CliCommand } from "../types"
|
|
18
|
+
|
|
19
|
+
const logger = createLogger()
|
|
6
20
|
|
|
7
21
|
const GREEN = "\x1b[32m"
|
|
8
22
|
const RED = "\x1b[31m"
|
|
@@ -11,10 +25,15 @@ const RESET = "\x1b[0m"
|
|
|
11
25
|
|
|
12
26
|
function checkBinary(name: string): { found: boolean; version: string | null } {
|
|
13
27
|
try {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
const result = Bun.spawnSync([name, "--version"], {
|
|
29
|
+
stdout: "pipe",
|
|
30
|
+
stderr: "pipe",
|
|
31
|
+
timeout: 5000,
|
|
32
|
+
})
|
|
33
|
+
if (result.exitCode !== 0) {
|
|
34
|
+
return { found: false, version: null }
|
|
35
|
+
}
|
|
36
|
+
const version = new TextDecoder().decode(result.stdout).trim().split("\n")[0] ?? null
|
|
18
37
|
return { found: true, version }
|
|
19
38
|
} catch {
|
|
20
39
|
return { found: false, version: null }
|
|
@@ -28,54 +47,315 @@ function checkSolidityProject(dir: string): string | null {
|
|
|
28
47
|
return null
|
|
29
48
|
}
|
|
30
49
|
|
|
50
|
+
export const ALL_CATEGORIES = [
|
|
51
|
+
"vulnerability-pattern",
|
|
52
|
+
"methodology",
|
|
53
|
+
"protocol-pattern",
|
|
54
|
+
"checklist",
|
|
55
|
+
"reference",
|
|
56
|
+
] as const
|
|
57
|
+
|
|
58
|
+
export const REQUIRED_CATEGORIES: readonly string[] = ["vulnerability-pattern", "methodology"]
|
|
59
|
+
|
|
60
|
+
export type SkillHealthReport = {
|
|
61
|
+
categoryBreakdown: Record<string, number>
|
|
62
|
+
trustTierBreakdown: Record<string, number>
|
|
63
|
+
duplicates: Array<{ name: string; sources: string[] }>
|
|
64
|
+
schemaValid: number
|
|
65
|
+
schemaInvalid: number
|
|
66
|
+
schemaSkipped: number
|
|
67
|
+
invalidSkills: Array<{ name: string; error: string }>
|
|
68
|
+
missingCategories: string[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function findDuplicateSkills(
|
|
72
|
+
entries: Array<{ name: string; source: string }>,
|
|
73
|
+
): Array<{ name: string; sources: string[] }> {
|
|
74
|
+
const nameToSources = new Map<string, Set<string>>()
|
|
75
|
+
for (const { name, source } of entries) {
|
|
76
|
+
if (!nameToSources.has(name)) nameToSources.set(name, new Set())
|
|
77
|
+
const sources = nameToSources.get(name)
|
|
78
|
+
if (sources) sources.add(source)
|
|
79
|
+
}
|
|
80
|
+
return Array.from(nameToSources)
|
|
81
|
+
.filter(([, sources]) => sources.size > 1)
|
|
82
|
+
.map(([name, sources]) => ({ name, sources: Array.from(sources) }))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildSkillHealthReport(
|
|
86
|
+
resolvedSkills: Map<string, ResolvedSkill>,
|
|
87
|
+
duplicateEntries?: Array<{ name: string; source: string }>,
|
|
88
|
+
): SkillHealthReport {
|
|
89
|
+
const categoryBreakdown: Record<string, number> = {}
|
|
90
|
+
for (const cat of ALL_CATEGORIES) categoryBreakdown[cat] = 0
|
|
91
|
+
|
|
92
|
+
const trustTierBreakdown: Record<string, number> = {}
|
|
93
|
+
let schemaValid = 0
|
|
94
|
+
let schemaInvalid = 0
|
|
95
|
+
let schemaSkipped = 0
|
|
96
|
+
const invalidSkills: Array<{ name: string; error: string }> = []
|
|
97
|
+
|
|
98
|
+
for (const [name, skill] of resolvedSkills) {
|
|
99
|
+
trustTierBreakdown[skill.source] = (trustTierBreakdown[skill.source] ?? 0) + 1
|
|
100
|
+
|
|
101
|
+
const fm = parseFrontmatter(skill.content)
|
|
102
|
+
if (fm) {
|
|
103
|
+
const validation = validateSkillFrontmatter(fm)
|
|
104
|
+
if (validation.success) {
|
|
105
|
+
schemaValid++
|
|
106
|
+
if (validation.data.category) {
|
|
107
|
+
categoryBreakdown[validation.data.category] =
|
|
108
|
+
(categoryBreakdown[validation.data.category] ?? 0) + 1
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
schemaInvalid++
|
|
112
|
+
invalidSkills.push({ name, error: validation.errors[0] ?? "unknown error" })
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
schemaSkipped++
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const duplicates = duplicateEntries ? findDuplicateSkills(duplicateEntries) : []
|
|
120
|
+
const missingCategories = REQUIRED_CATEGORIES.filter((cat) => (categoryBreakdown[cat] ?? 0) === 0)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
categoryBreakdown,
|
|
124
|
+
trustTierBreakdown,
|
|
125
|
+
duplicates,
|
|
126
|
+
schemaValid,
|
|
127
|
+
schemaInvalid,
|
|
128
|
+
schemaSkipped,
|
|
129
|
+
invalidSkills,
|
|
130
|
+
missingCategories,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function scanMarkdownFiles(dir: string, maxDepth = 8): string[] {
|
|
135
|
+
if (!existsSync(dir)) return []
|
|
136
|
+
const files: string[] = []
|
|
137
|
+
const stack: Array<{ path: string; depth: number }> = [{ path: dir, depth: 0 }]
|
|
138
|
+
while (stack.length > 0) {
|
|
139
|
+
const current = stack.pop()
|
|
140
|
+
if (!current || current.depth > maxDepth) continue
|
|
141
|
+
try {
|
|
142
|
+
const entries = readdirSync(current.path, { withFileTypes: true })
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const fullPath = join(current.path, entry.name)
|
|
145
|
+
if (entry.isDirectory()) {
|
|
146
|
+
stack.push({ path: fullPath, depth: current.depth + 1 })
|
|
147
|
+
} else if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") {
|
|
148
|
+
files.push(fullPath)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
logger.debug("Failed to read directory during skill scan")
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return files
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function inferSkillName(filePath: string): string {
|
|
159
|
+
if (basename(filePath) === "SKILL.md") {
|
|
160
|
+
return basename(dirname(filePath))
|
|
161
|
+
}
|
|
162
|
+
return basename(filePath, extname(filePath))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function collectAllSkillNames(
|
|
166
|
+
projectDir: string,
|
|
167
|
+
argusConfig?: ArgusConfig,
|
|
168
|
+
): Array<{ name: string; source: string }> {
|
|
169
|
+
const roots = resolveSkillRoots(projectDir, argusConfig)
|
|
170
|
+
const entries: Array<{ name: string; source: string }> = []
|
|
171
|
+
for (const root of roots) {
|
|
172
|
+
const files = scanMarkdownFiles(root.path)
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
try {
|
|
175
|
+
const content = readFileSync(file, "utf8")
|
|
176
|
+
const fm = parseFrontmatter(content)
|
|
177
|
+
const nameFromFm = typeof fm?.name === "string" ? fm.name : null
|
|
178
|
+
const rawName = nameFromFm || inferSkillName(file)
|
|
179
|
+
const name = normalizeSkillName(rawName)
|
|
180
|
+
if (name) entries.push({ name, source: root.source })
|
|
181
|
+
} catch {
|
|
182
|
+
logger.debug("Failed to parse skill file frontmatter")
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return entries
|
|
187
|
+
}
|
|
188
|
+
|
|
31
189
|
export const doctorCommand: CliCommand = {
|
|
32
190
|
name: "doctor",
|
|
33
191
|
description: "Check tool dependencies and configuration",
|
|
34
|
-
async execute(
|
|
192
|
+
async execute(_args: string[]): Promise<number> {
|
|
35
193
|
const cwd = process.cwd()
|
|
36
194
|
let hasFailure = false
|
|
37
195
|
|
|
38
|
-
|
|
196
|
+
cliOutput.log("Argus Doctor\n")
|
|
39
197
|
|
|
40
198
|
const slither = checkBinary("slither")
|
|
41
199
|
if (slither.found) {
|
|
42
|
-
|
|
200
|
+
cliOutput.log(`${GREEN}✓${RESET} Slither: installed (${slither.version})`)
|
|
43
201
|
} else {
|
|
44
|
-
|
|
202
|
+
cliOutput.log(`${RED}✗${RESET} Slither: not found — pip install slither-analyzer`)
|
|
45
203
|
hasFailure = true
|
|
46
204
|
}
|
|
47
205
|
|
|
48
206
|
const forge = checkBinary("forge")
|
|
49
207
|
if (forge.found) {
|
|
50
|
-
|
|
208
|
+
cliOutput.log(`${GREEN}✓${RESET} Forge: installed (${forge.version})`)
|
|
51
209
|
} else {
|
|
52
|
-
|
|
210
|
+
cliOutput.log(
|
|
211
|
+
`${RED}✗${RESET} Forge: not found — curl -L https://foundry.paradigm.xyz | bash`,
|
|
212
|
+
)
|
|
53
213
|
hasFailure = true
|
|
54
214
|
}
|
|
55
215
|
|
|
216
|
+
const solcSelect = checkBinary("solc-select")
|
|
217
|
+
if (solcSelect.found) {
|
|
218
|
+
cliOutput.log(`${GREEN}✓${RESET} solc-select: installed (${solcSelect.version})`)
|
|
219
|
+
} else {
|
|
220
|
+
cliOutput.log(
|
|
221
|
+
`${YELLOW}⚠${RESET} solc-select: not found — pipx install solc-select (needed for via_ir flatten fallback)`,
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
56
225
|
const projectType = checkSolidityProject(cwd)
|
|
57
226
|
if (projectType) {
|
|
58
|
-
|
|
227
|
+
cliOutput.log(`${GREEN}✓${RESET} Project: ${projectType} detected`)
|
|
59
228
|
} else {
|
|
60
|
-
|
|
229
|
+
cliOutput.log(`${YELLOW}⚠${RESET} Project: no Solidity project detected`)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (projectType === "foundry" && detectViaIr(cwd)) {
|
|
233
|
+
cliOutput.log(
|
|
234
|
+
`${YELLOW}⚠${RESET} via_ir: enabled in foundry.toml — Slither will use flatten fallback`,
|
|
235
|
+
)
|
|
236
|
+
if (!forge.found) {
|
|
237
|
+
cliOutput.log(
|
|
238
|
+
`${RED}✗${RESET} forge is required for via_ir flatten fallback but is missing`,
|
|
239
|
+
)
|
|
240
|
+
hasFailure = true
|
|
241
|
+
}
|
|
242
|
+
if (!solcSelect.found) {
|
|
243
|
+
cliOutput.log(`${YELLOW}⚠${RESET} solc-select is recommended for via_ir flatten fallback`)
|
|
244
|
+
}
|
|
61
245
|
}
|
|
62
246
|
|
|
247
|
+
let config: ReturnType<typeof loadArgusConfig> | undefined
|
|
63
248
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
249
|
+
config = loadArgusConfig(cwd)
|
|
250
|
+
cliOutput.log(`${GREEN}✓${RESET} Config: valid`)
|
|
251
|
+
|
|
252
|
+
const requiredSkills = getRequiredAuditSkills()
|
|
253
|
+
const resolvedSkills = resolveArgusSkills(cwd, config)
|
|
254
|
+
const missingSkills = requiredSkills.filter((skillName) => !resolvedSkills.has(skillName))
|
|
255
|
+
|
|
256
|
+
if (missingSkills.length === 0) {
|
|
257
|
+
cliOutput.log(
|
|
258
|
+
`${GREEN}✓${RESET} Skills: required audit skills resolvable (${requiredSkills.join(", ")})`,
|
|
259
|
+
)
|
|
260
|
+
} else {
|
|
261
|
+
cliOutput.log(
|
|
262
|
+
`${RED}✗${RESET} Skills: missing required skills (${missingSkills.join(", ")})`,
|
|
263
|
+
)
|
|
264
|
+
hasFailure = true
|
|
265
|
+
}
|
|
66
266
|
} catch {
|
|
67
|
-
|
|
267
|
+
cliOutput.log(`${YELLOW}⚠${RESET} Config: using defaults`)
|
|
268
|
+
|
|
269
|
+
const requiredSkills = getRequiredAuditSkills()
|
|
270
|
+
const resolvedSkills = resolveArgusSkills(cwd)
|
|
271
|
+
const missingSkills = requiredSkills.filter((skillName) => !resolvedSkills.has(skillName))
|
|
272
|
+
|
|
273
|
+
if (missingSkills.length === 0) {
|
|
274
|
+
cliOutput.log(
|
|
275
|
+
`${GREEN}✓${RESET} Skills: required audit skills resolvable (${requiredSkills.join(", ")})`,
|
|
276
|
+
)
|
|
277
|
+
} else {
|
|
278
|
+
cliOutput.log(
|
|
279
|
+
`${RED}✗${RESET} Skills: missing required skills (${missingSkills.join(", ")})`,
|
|
280
|
+
)
|
|
281
|
+
hasFailure = true
|
|
282
|
+
}
|
|
68
283
|
}
|
|
69
284
|
|
|
70
285
|
try {
|
|
71
|
-
const response = await fetch("https://api.scvd.dev/stats", {
|
|
286
|
+
const response = await fetch("https://api.scvd.dev/stats", {
|
|
287
|
+
signal: AbortSignal.timeout(5000),
|
|
288
|
+
})
|
|
72
289
|
if (response.ok) {
|
|
73
|
-
|
|
290
|
+
cliOutput.log(`${GREEN}✓${RESET} SCVD API: reachable`)
|
|
74
291
|
} else {
|
|
75
|
-
|
|
292
|
+
cliOutput.log(`${YELLOW}⚠${RESET} SCVD API: returned ${response.status}`)
|
|
76
293
|
}
|
|
77
294
|
} catch {
|
|
78
|
-
|
|
295
|
+
cliOutput.log(`${YELLOW}⚠${RESET} SCVD API: unreachable`)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Solodit MCP check
|
|
299
|
+
const soloditConfig = config?.solodit ?? { enabled: true, port: 3000 }
|
|
300
|
+
const soloditEnabled = soloditConfig.enabled !== false
|
|
301
|
+
const soloditPort = soloditConfig.port ?? 3000
|
|
302
|
+
|
|
303
|
+
if (soloditEnabled) {
|
|
304
|
+
const health = await checkSoloditHealth(soloditPort, true)
|
|
305
|
+
if (health.reachable) {
|
|
306
|
+
cliOutput.log(`${GREEN}✓${RESET} Solodit MCP: reachable on port ${soloditPort}`)
|
|
307
|
+
} else {
|
|
308
|
+
cliOutput.log(
|
|
309
|
+
`${YELLOW}⚠${RESET} Solodit MCP: unreachable on port ${soloditPort} (start with: npx @lyuboslavlyubenov/solodit-mcp)`,
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
cliOutput.log(`${YELLOW}⚠${RESET} Solodit MCP: disabled in config`)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
cliOutput.log("\nSkill Health")
|
|
317
|
+
try {
|
|
318
|
+
const healthSkills = resolveArgusSkills(cwd, config)
|
|
319
|
+
const allEntries = collectAllSkillNames(cwd, config)
|
|
320
|
+
const report = buildSkillHealthReport(healthSkills, allEntries)
|
|
321
|
+
|
|
322
|
+
const catParts = ALL_CATEGORIES.map((cat) => `${cat}: ${report.categoryBreakdown[cat] ?? 0}`)
|
|
323
|
+
cliOutput.log(`${GREEN}✓${RESET} Categories: ${catParts.join(", ")}`)
|
|
324
|
+
|
|
325
|
+
const tierParts = Object.entries(report.trustTierBreakdown).map(
|
|
326
|
+
([tier, count]) => `${tier}: ${count}`,
|
|
327
|
+
)
|
|
328
|
+
cliOutput.log(`${GREEN}✓${RESET} Trust tiers: ${tierParts.join(", ")}`)
|
|
329
|
+
|
|
330
|
+
if (report.schemaInvalid === 0) {
|
|
331
|
+
cliOutput.log(
|
|
332
|
+
`${GREEN}✓${RESET} Schema: ${report.schemaValid} valid, 0 invalid, ${report.schemaSkipped} skipped (no frontmatter)`,
|
|
333
|
+
)
|
|
334
|
+
} else {
|
|
335
|
+
cliOutput.log(
|
|
336
|
+
`${YELLOW}⚠${RESET} Schema: ${report.schemaValid} valid, ${report.schemaInvalid} invalid, ${report.schemaSkipped} skipped (no frontmatter)`,
|
|
337
|
+
)
|
|
338
|
+
for (const inv of report.invalidSkills) {
|
|
339
|
+
cliOutput.log(` ${RED}✗${RESET} ${inv.name}: ${inv.error}`)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (report.duplicates.length > 0) {
|
|
344
|
+
for (const dup of report.duplicates) {
|
|
345
|
+
cliOutput.log(
|
|
346
|
+
`${YELLOW}⚠${RESET} Duplicate skill: "${dup.name}" found in ${dup.sources.join(" and ")}`,
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
cliOutput.log(`${GREEN}✓${RESET} No duplicate skills detected`)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const cat of report.missingCategories) {
|
|
354
|
+
cliOutput.log(`${YELLOW}⚠${RESET} Required category "${cat}" has 0 skills`)
|
|
355
|
+
}
|
|
356
|
+
} catch {
|
|
357
|
+
cliOutput.log(`${RED}✗${RESET} Could not analyze skill health`)
|
|
358
|
+
hasFailure = true
|
|
79
359
|
}
|
|
80
360
|
|
|
81
361
|
return hasFailure ? 1 : 0
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
2
2
|
import { join } from "node:path"
|
|
3
|
+
import { cliOutput } from "../cli-output"
|
|
3
4
|
import type { CliCommand } from "../types"
|
|
4
5
|
|
|
5
6
|
const GREEN = "\x1b[32m"
|
|
@@ -17,14 +18,15 @@ const DEFAULT_CONFIG = {
|
|
|
17
18
|
export const initCommand: CliCommand = {
|
|
18
19
|
name: "init",
|
|
19
20
|
description: "Initialize Argus configuration for this project",
|
|
20
|
-
async execute(
|
|
21
|
+
async execute(_args: string[]): Promise<number> {
|
|
21
22
|
const cwd = process.cwd()
|
|
22
23
|
const configDir = join(cwd, ".opencode")
|
|
23
24
|
const configPath = join(configDir, "solidity-argus.json")
|
|
24
25
|
|
|
25
26
|
if (existsSync(configPath)) {
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
cliOutput.error(
|
|
28
|
+
`${YELLOW}⚠${RESET} Config already exists: ${configPath} — remove it first if you want to reinitialize.`,
|
|
29
|
+
)
|
|
28
30
|
return 1
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -37,9 +39,9 @@ export const initCommand: CliCommand = {
|
|
|
37
39
|
? "Hardhat"
|
|
38
40
|
: "unknown"
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
cliOutput.log(`${GREEN}✓${RESET} Created ${configPath}`)
|
|
43
|
+
cliOutput.log(` Project type: ${projectType}`)
|
|
44
|
+
cliOutput.log(" Run 'argus doctor' to check dependencies.")
|
|
43
45
|
|
|
44
46
|
return 0
|
|
45
47
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
-
import { join } from "node:path"
|
|
3
2
|
import { homedir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { cliOutput } from "../cli-output"
|
|
4
5
|
import type { CliCommand } from "../types"
|
|
5
6
|
|
|
6
7
|
const GREEN = "\x1b[32m"
|
|
@@ -22,12 +23,13 @@ export function findOpencodeConfig(homeOverride?: string): string | null {
|
|
|
22
23
|
export const installCommand: CliCommand = {
|
|
23
24
|
name: "install",
|
|
24
25
|
description: "Register solidity-argus in your OpenCode config",
|
|
25
|
-
async execute(
|
|
26
|
+
async execute(_args: string[]): Promise<number> {
|
|
26
27
|
const configPath = findOpencodeConfig()
|
|
27
28
|
|
|
28
29
|
if (!configPath) {
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
cliOutput.error(
|
|
31
|
+
`${YELLOW}⚠${RESET} opencode.json not found — create one first, or run: opencode init`,
|
|
32
|
+
)
|
|
31
33
|
return 1
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -37,7 +39,7 @@ export const installCommand: CliCommand = {
|
|
|
37
39
|
const plugins: string[] = config.plugin ?? []
|
|
38
40
|
|
|
39
41
|
if (plugins.includes("solidity-argus")) {
|
|
40
|
-
|
|
42
|
+
cliOutput.log(`${GREEN}✓${RESET} solidity-argus already registered in ${configPath}`)
|
|
41
43
|
return 0
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -45,10 +47,10 @@ export const installCommand: CliCommand = {
|
|
|
45
47
|
config.plugin = plugins
|
|
46
48
|
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`)
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
cliOutput.log(`${GREEN}✓${RESET} Added solidity-argus to ${configPath}`)
|
|
49
51
|
return 0
|
|
50
|
-
} catch (
|
|
51
|
-
|
|
52
|
+
} catch (_error) {
|
|
53
|
+
cliOutput.error(`${YELLOW}⚠${RESET} Failed to update ${configPath}`)
|
|
52
54
|
return 1
|
|
53
55
|
}
|
|
54
56
|
},
|