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,85 @@
|
|
|
1
|
+
import type { ScvdClient } from "./scvd-client";
|
|
2
|
+
import { buildIndex, loadIndex, saveIndex } from "./scvd-index";
|
|
3
|
+
|
|
4
|
+
export interface SyncResult {
|
|
5
|
+
success: boolean;
|
|
6
|
+
newFindings: number;
|
|
7
|
+
totalIndexed: number;
|
|
8
|
+
lastSync: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildErrorResult(error: unknown): SyncResult {
|
|
13
|
+
const message = error instanceof Error ? error.message : "Unknown sync error";
|
|
14
|
+
return {
|
|
15
|
+
success: false,
|
|
16
|
+
newFindings: 0,
|
|
17
|
+
totalIndexed: 0,
|
|
18
|
+
lastSync: new Date().toISOString(),
|
|
19
|
+
error: message,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function syncAll(client: ScvdClient, indexPath: string): Promise<SyncResult> {
|
|
24
|
+
try {
|
|
25
|
+
const findings = await client.fetchAllFindings();
|
|
26
|
+
const index = buildIndex(findings);
|
|
27
|
+
await saveIndex(index, indexPath);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
newFindings: findings.length,
|
|
32
|
+
totalIndexed: index.totalFindings,
|
|
33
|
+
lastSync: index.lastSync,
|
|
34
|
+
};
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return buildErrorResult(error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function syncIncremental(
|
|
41
|
+
client: ScvdClient,
|
|
42
|
+
indexPath: string
|
|
43
|
+
): Promise<SyncResult> {
|
|
44
|
+
try {
|
|
45
|
+
const [stats, existingIndex] = await Promise.all([
|
|
46
|
+
client.fetchStats(),
|
|
47
|
+
loadIndex(indexPath),
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
if (existingIndex && existingIndex.totalFindings === stats.total) {
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
newFindings: 0,
|
|
54
|
+
totalIndexed: existingIndex.totalFindings,
|
|
55
|
+
lastSync: existingIndex.lastSync,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return await syncAll(client, indexPath);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return buildErrorResult(error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function getSyncStatus(indexPath: string): Promise<{
|
|
66
|
+
lastSync: string | null;
|
|
67
|
+
totalFindings: number;
|
|
68
|
+
healthy: boolean;
|
|
69
|
+
}> {
|
|
70
|
+
const index = await loadIndex(indexPath);
|
|
71
|
+
|
|
72
|
+
if (!index) {
|
|
73
|
+
return {
|
|
74
|
+
lastSync: null,
|
|
75
|
+
totalFindings: 0,
|
|
76
|
+
healthy: false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
lastSync: index.lastSync,
|
|
82
|
+
totalFindings: index.totalFindings,
|
|
83
|
+
healthy: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { BackgroundManager, AuditStateManager, Managers } from "./types";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { AuditState } from "../state/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BackgroundManager interface
|
|
5
|
+
* Handles dispatching and managing background agent tasks
|
|
6
|
+
*/
|
|
7
|
+
export interface BackgroundManager {
|
|
8
|
+
/**
|
|
9
|
+
* Dispatch an agent task to run in the background
|
|
10
|
+
* @param agentName - Name of the agent to dispatch (e.g., "sentinel", "pythia")
|
|
11
|
+
* @param prompt - The prompt/instruction for the agent
|
|
12
|
+
* @param options - Optional configuration (priority, timeout, etc.)
|
|
13
|
+
* @returns taskId - Unique identifier for tracking this task
|
|
14
|
+
*/
|
|
15
|
+
dispatch(agentName: string, prompt: string, options?: { priority?: number }): string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cancel a running background task
|
|
19
|
+
* @param taskId - The task ID to cancel
|
|
20
|
+
*/
|
|
21
|
+
cancel(taskId: string): void;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the result of a completed background task
|
|
25
|
+
* @param taskId - The task ID to retrieve results for
|
|
26
|
+
* @returns Promise resolving to the task result
|
|
27
|
+
*/
|
|
28
|
+
getResult(taskId: string): Promise<unknown>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register a callback to be invoked when a task completes
|
|
32
|
+
* @param callback - Function called with (taskId, result) when task finishes
|
|
33
|
+
*/
|
|
34
|
+
onComplete(callback: (taskId: string, result: unknown) => void): void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the number of currently active/running tasks
|
|
38
|
+
* @returns Number of active tasks
|
|
39
|
+
*/
|
|
40
|
+
getActiveCount(): number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* AuditStateManager interface
|
|
45
|
+
* Handles persistence and retrieval of audit state
|
|
46
|
+
*/
|
|
47
|
+
export interface AuditStateManager {
|
|
48
|
+
/**
|
|
49
|
+
* Load audit state from persistent storage
|
|
50
|
+
* @returns Promise resolving to AuditState or null if not found
|
|
51
|
+
*/
|
|
52
|
+
load(): Promise<AuditState | null>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Save audit state to persistent storage
|
|
56
|
+
* @param state - The AuditState to persist
|
|
57
|
+
*/
|
|
58
|
+
save(state: AuditState): Promise<void>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the current in-memory audit state
|
|
62
|
+
* @returns The current AuditState or null if not loaded
|
|
63
|
+
*/
|
|
64
|
+
get(): AuditState | null;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Update the audit state with a partial patch
|
|
68
|
+
* @param patch - Partial AuditState object with fields to update
|
|
69
|
+
*/
|
|
70
|
+
update(patch: Partial<AuditState>): Promise<void>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Reset the audit state (clear all data)
|
|
74
|
+
*/
|
|
75
|
+
reset(): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Managers type
|
|
80
|
+
* Container for all manager instances
|
|
81
|
+
*/
|
|
82
|
+
export type Managers = {
|
|
83
|
+
backgroundManager: BackgroundManager;
|
|
84
|
+
auditStateManager: AuditStateManager;
|
|
85
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Hooks as PluginHooks, ToolDefinition } from "@opencode-ai/plugin"
|
|
2
|
+
import type { Hooks } from "./create-hooks"
|
|
3
|
+
|
|
4
|
+
export type PluginReturn = {
|
|
5
|
+
tool: Record<string, ToolDefinition>
|
|
6
|
+
} & Partial<Omit<PluginHooks, "tool">>
|
|
7
|
+
|
|
8
|
+
export function createPluginInterface(args: {
|
|
9
|
+
tools: Record<string, ToolDefinition>
|
|
10
|
+
hooks: Hooks
|
|
11
|
+
}): PluginReturn {
|
|
12
|
+
const { tools, hooks } = args
|
|
13
|
+
|
|
14
|
+
const result: PluginReturn = {
|
|
15
|
+
tool: tools,
|
|
16
|
+
config: hooks.config,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (hooks["experimental.chat.system.transform"]) {
|
|
20
|
+
result["experimental.chat.system.transform"] =
|
|
21
|
+
hooks["experimental.chat.system.transform"]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (hooks["experimental.session.compacting"]) {
|
|
25
|
+
result["experimental.session.compacting"] =
|
|
26
|
+
hooks["experimental.session.compacting"]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (hooks["tool.execute.after"]) {
|
|
30
|
+
result["tool.execute.after"] = hooks["tool.execute.after"]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (hooks.event) {
|
|
34
|
+
result.event = hooks.event
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return result
|
|
38
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
export function hasBinary(name: string): boolean {
|
|
6
|
+
try {
|
|
7
|
+
execSync(`which ${name}`, { stdio: "ignore", timeout: 3_000 });
|
|
8
|
+
return true;
|
|
9
|
+
} catch (_e) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseSolcVersion(target: string): string | undefined {
|
|
15
|
+
const foundryToml = join(target, "foundry.toml");
|
|
16
|
+
if (existsSync(foundryToml)) {
|
|
17
|
+
const content = readFileSync(foundryToml, "utf-8");
|
|
18
|
+
const match = content.match(/solc\s*=\s*["']([^"']+)["']/);
|
|
19
|
+
if (match?.[1]) return match[1];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const solFiles = [target];
|
|
23
|
+
if (existsSync(target) && target.endsWith(".sol")) {
|
|
24
|
+
solFiles.push(target);
|
|
25
|
+
} else {
|
|
26
|
+
const srcDir = join(target, "src");
|
|
27
|
+
if (existsSync(srcDir)) {
|
|
28
|
+
try {
|
|
29
|
+
const files = execSync(`find "${srcDir}" -name "*.sol" -maxdepth 3`, {
|
|
30
|
+
encoding: "utf-8",
|
|
31
|
+
timeout: 5_000,
|
|
32
|
+
})
|
|
33
|
+
.trim()
|
|
34
|
+
.split("\n")
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
solFiles.push(...files);
|
|
37
|
+
} catch (_findErr) {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const file of solFiles) {
|
|
43
|
+
if (!existsSync(file) || !file.endsWith(".sol")) continue;
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(file, "utf-8");
|
|
46
|
+
const pragma = content.match(/pragma\s+solidity\s+[\^~>=<]*\s*([\d.]+)/);
|
|
47
|
+
if (pragma?.[1]) return pragma[1];
|
|
48
|
+
} catch (_readErr) {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function extractContractNames(filePath: string): string[] {
|
|
55
|
+
if (!existsSync(filePath)) return [];
|
|
56
|
+
try {
|
|
57
|
+
const content = readFileSync(filePath, "utf-8");
|
|
58
|
+
const matches = content.matchAll(/\b(?:contract|library|interface)\s+(\w+)/g);
|
|
59
|
+
return Array.from(matches, (m) => m[1]).filter(Boolean) as string[];
|
|
60
|
+
} catch (_e) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export function deepMerge(target: any, source: any): any {
|
|
2
|
+
// If source is undefined, return target as-is
|
|
3
|
+
if (source === undefined) {
|
|
4
|
+
return target;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// If either is not an object, return source (override)
|
|
8
|
+
if (
|
|
9
|
+
typeof target !== "object" ||
|
|
10
|
+
target === null ||
|
|
11
|
+
typeof source !== "object" ||
|
|
12
|
+
source === null
|
|
13
|
+
) {
|
|
14
|
+
return source;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// If both are arrays, concatenate and deduplicate
|
|
18
|
+
if (Array.isArray(target) && Array.isArray(source)) {
|
|
19
|
+
const merged = [...target, ...source];
|
|
20
|
+
// Deduplicate by filtering unique values
|
|
21
|
+
return Array.from(new Set(merged));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// If target is array but source is not, return source
|
|
25
|
+
if (Array.isArray(target) && !Array.isArray(source)) {
|
|
26
|
+
return source;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// If source is array but target is not, return source
|
|
30
|
+
if (!Array.isArray(target) && Array.isArray(source)) {
|
|
31
|
+
return source;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Both are plain objects, merge recursively
|
|
35
|
+
const result = { ...target };
|
|
36
|
+
|
|
37
|
+
for (const key in source) {
|
|
38
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
39
|
+
const sourceValue = source[key];
|
|
40
|
+
|
|
41
|
+
// Skip undefined values from source
|
|
42
|
+
if (sourceValue === undefined) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// If both are objects (and not arrays), recurse
|
|
47
|
+
if (
|
|
48
|
+
typeof result[key] === "object" &&
|
|
49
|
+
result[key] !== null &&
|
|
50
|
+
!Array.isArray(result[key]) &&
|
|
51
|
+
typeof sourceValue === "object" &&
|
|
52
|
+
sourceValue !== null &&
|
|
53
|
+
!Array.isArray(sourceValue)
|
|
54
|
+
) {
|
|
55
|
+
result[key] = deepMerge(result[key], sourceValue);
|
|
56
|
+
} else if (
|
|
57
|
+
Array.isArray(result[key]) &&
|
|
58
|
+
Array.isArray(sourceValue)
|
|
59
|
+
) {
|
|
60
|
+
// Both are arrays, concatenate and deduplicate
|
|
61
|
+
const merged = [...result[key], ...sourceValue];
|
|
62
|
+
result[key] = Array.from(new Set(merged));
|
|
63
|
+
} else {
|
|
64
|
+
// Override with source value
|
|
65
|
+
result[key] = sourceValue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { stripJsoncComments } from "./jsonc-parser";
|
|
4
|
+
|
|
5
|
+
export type ConfigFormat = "json" | "jsonc" | "none";
|
|
6
|
+
|
|
7
|
+
export interface ConfigFileInfo {
|
|
8
|
+
path: string | null;
|
|
9
|
+
format: ConfigFormat;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function detectConfigFile(basePath: string): ConfigFileInfo {
|
|
13
|
+
const candidates = [
|
|
14
|
+
{ path: join(basePath, ".opencode", "solidity-argus.jsonc"), format: "jsonc" as const },
|
|
15
|
+
{ path: join(basePath, ".opencode", "solidity-argus.json"), format: "json" as const },
|
|
16
|
+
{ path: join(basePath, "solidity-argus.jsonc"), format: "jsonc" as const },
|
|
17
|
+
{ path: join(basePath, "solidity-argus.json"), format: "json" as const },
|
|
18
|
+
{ path: join(basePath, "config.jsonc"), format: "jsonc" as const },
|
|
19
|
+
{ path: join(basePath, "config.json"), format: "json" as const },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
for (const candidate of candidates) {
|
|
23
|
+
if (existsSync(candidate.path)) {
|
|
24
|
+
return {
|
|
25
|
+
path: candidate.path,
|
|
26
|
+
format: candidate.format,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
path: null,
|
|
33
|
+
format: "none",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readJsoncFile(filePath: string): Record<string, any> | null {
|
|
38
|
+
try {
|
|
39
|
+
if (!existsSync(filePath)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const content = readFileSync(filePath, "utf-8");
|
|
44
|
+
|
|
45
|
+
if (!content.trim()) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const stripped = stripJsoncComments(content);
|
|
50
|
+
const parsed = JSON.parse(stripped);
|
|
51
|
+
|
|
52
|
+
return parsed;
|
|
53
|
+
} catch (_error) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createLogger, type Logger, type LoggerConfig } from "./logger";
|
|
2
|
+
export { deepMerge } from "./deep-merge";
|
|
3
|
+
export { stripJsoncComments } from "./jsonc-parser";
|
|
4
|
+
export { detectConfigFile, readJsoncFile, type ConfigFormat, type ConfigFileInfo } from "./file-utils";
|
|
5
|
+
export { hasBinary, parseSolcVersion, extractContractNames } from "./binary-utils";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function stripJsoncComments(jsonc: string): string {
|
|
2
|
+
let result = jsonc;
|
|
3
|
+
|
|
4
|
+
result = result.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
5
|
+
|
|
6
|
+
const lines = result.split("\n");
|
|
7
|
+
result = lines
|
|
8
|
+
.map((line) => {
|
|
9
|
+
let inString = false;
|
|
10
|
+
let escaped = false;
|
|
11
|
+
let lastCommentIndex = -1;
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < line.length; i++) {
|
|
14
|
+
if (escaped) {
|
|
15
|
+
escaped = false;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (line[i] === "\\") {
|
|
19
|
+
escaped = true;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (line[i] === '"') {
|
|
23
|
+
inString = !inString;
|
|
24
|
+
}
|
|
25
|
+
if (!inString && line[i] === "/" && line[i + 1] === "/") {
|
|
26
|
+
lastCommentIndex = i;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (lastCommentIndex === -1) return line;
|
|
32
|
+
return line.substring(0, lastCommentIndex);
|
|
33
|
+
})
|
|
34
|
+
.join("\n");
|
|
35
|
+
|
|
36
|
+
result = result.replace(/,(\s*[}\]])/g, "$1");
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface LoggerConfig {
|
|
2
|
+
debug?: boolean;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface Logger {
|
|
6
|
+
info(...args: any[]): void;
|
|
7
|
+
debug(...args: any[]): void;
|
|
8
|
+
error(...args: any[]): void;
|
|
9
|
+
warn(...args: any[]): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createLogger(config: LoggerConfig = {}): Logger {
|
|
13
|
+
const { debug = false } = config;
|
|
14
|
+
|
|
15
|
+
const prefix = "[argus]";
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
info(...args: any[]): void {
|
|
19
|
+
console.error(prefix, ...args);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
debug(...args: any[]): void {
|
|
23
|
+
if (debug) {
|
|
24
|
+
console.error(prefix, ...args);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
error(...args: any[]): void {
|
|
29
|
+
console.error(prefix, ...args);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
warn(...args: any[]): void {
|
|
33
|
+
console.error(prefix, ...args);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import type { AuditState } from "./types";
|
|
3
|
+
import { createFindingStore, type FindingStore } from "./finding-store";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Factory function to create a new audit state instance (NOT singleton)
|
|
7
|
+
* Each call creates a fresh state with a unique session ID
|
|
8
|
+
*/
|
|
9
|
+
export function createAuditState(projectDir: string): {
|
|
10
|
+
state: AuditState;
|
|
11
|
+
store: FindingStore;
|
|
12
|
+
} {
|
|
13
|
+
const state: AuditState = {
|
|
14
|
+
sessionId: randomUUID(),
|
|
15
|
+
projectDir,
|
|
16
|
+
contractsReviewed: [],
|
|
17
|
+
findings: [],
|
|
18
|
+
toolsExecuted: [],
|
|
19
|
+
currentPhase: "reconnaissance",
|
|
20
|
+
scope: [],
|
|
21
|
+
startTime: Date.now(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const store = createFindingStore(state);
|
|
25
|
+
|
|
26
|
+
return { state, store };
|
|
27
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Finding, FindingSeverity, AuditState } from "./types";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
|
|
4
|
+
export interface FindingStore {
|
|
5
|
+
addFinding(finding: Omit<Finding, "id">): Finding;
|
|
6
|
+
getFindings(filter?: {
|
|
7
|
+
severity?: FindingSeverity;
|
|
8
|
+
source?: Finding["source"];
|
|
9
|
+
}): Finding[];
|
|
10
|
+
hasFinding(check: string, file: string, lines: [number, number]): boolean;
|
|
11
|
+
serialize(): string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a finding store with deduplication by check+file+lines
|
|
16
|
+
* Deduplication key: `${check}:${file}:${lines[0]}-${lines[1]}`
|
|
17
|
+
*/
|
|
18
|
+
export function createFindingStore(state: AuditState): FindingStore {
|
|
19
|
+
const findingMap = new Map<string, Finding>();
|
|
20
|
+
|
|
21
|
+
function generateId(
|
|
22
|
+
check: string,
|
|
23
|
+
file: string,
|
|
24
|
+
lines: [number, number]
|
|
25
|
+
): string {
|
|
26
|
+
const key = `${check}:${file}:${lines[0]}-${lines[1]}`;
|
|
27
|
+
// Use deterministic hash for stable IDs
|
|
28
|
+
return createHash("sha256").update(key).digest("hex").substring(0, 16);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function addFinding(finding: Omit<Finding, "id">): Finding {
|
|
32
|
+
const id = generateId(finding.check, finding.file, finding.lines);
|
|
33
|
+
|
|
34
|
+
// Check if finding already exists (deduplication)
|
|
35
|
+
if (findingMap.has(id)) {
|
|
36
|
+
return findingMap.get(id)!;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const newFinding: Finding = {
|
|
40
|
+
...finding,
|
|
41
|
+
id,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
findingMap.set(id, newFinding);
|
|
45
|
+
state.findings.push(newFinding);
|
|
46
|
+
|
|
47
|
+
return newFinding;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getFindings(filter?: {
|
|
51
|
+
severity?: FindingSeverity;
|
|
52
|
+
source?: Finding["source"];
|
|
53
|
+
}): Finding[] {
|
|
54
|
+
if (!filter) {
|
|
55
|
+
return Array.from(findingMap.values());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Array.from(findingMap.values()).filter((finding) => {
|
|
59
|
+
if (filter.severity && finding.severity !== filter.severity) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (filter.source && finding.source !== filter.source) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasFinding(
|
|
70
|
+
check: string,
|
|
71
|
+
file: string,
|
|
72
|
+
lines: [number, number]
|
|
73
|
+
): boolean {
|
|
74
|
+
const id = generateId(check, file, lines);
|
|
75
|
+
return findingMap.has(id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function serialize(): string {
|
|
79
|
+
const findings = Array.from(findingMap.values());
|
|
80
|
+
const contractCount = state.contractsReviewed.length;
|
|
81
|
+
const findingCount = findings.length;
|
|
82
|
+
|
|
83
|
+
// Count by severity
|
|
84
|
+
const severityCounts: Record<FindingSeverity, number> = {
|
|
85
|
+
Critical: 0,
|
|
86
|
+
High: 0,
|
|
87
|
+
Medium: 0,
|
|
88
|
+
Low: 0,
|
|
89
|
+
Informational: 0,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
findings.forEach((finding) => {
|
|
93
|
+
severityCounts[finding.severity]++;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Build severity string
|
|
97
|
+
const severityParts: string[] = [];
|
|
98
|
+
if (severityCounts.Critical > 0) {
|
|
99
|
+
severityParts.push(`${severityCounts.Critical} Critical`);
|
|
100
|
+
}
|
|
101
|
+
if (severityCounts.High > 0) {
|
|
102
|
+
severityParts.push(`${severityCounts.High} High`);
|
|
103
|
+
}
|
|
104
|
+
if (severityCounts.Medium > 0) {
|
|
105
|
+
severityParts.push(`${severityCounts.Medium} Medium`);
|
|
106
|
+
}
|
|
107
|
+
if (severityCounts.Low > 0) {
|
|
108
|
+
severityParts.push(`${severityCounts.Low} Low`);
|
|
109
|
+
}
|
|
110
|
+
if (severityCounts.Informational > 0) {
|
|
111
|
+
severityParts.push(`${severityCounts.Informational} Informational`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const severityStr =
|
|
115
|
+
severityParts.length > 0 ? ` (${severityParts.join(", ")})` : "";
|
|
116
|
+
|
|
117
|
+
return `Contracts: ${contractCount}, Findings: ${findingCount}${severityStr}, Phase: ${state.currentPhase}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
addFinding,
|
|
122
|
+
getFindings,
|
|
123
|
+
hasFinding,
|
|
124
|
+
serialize,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ArgusConfig } from "../config/types";
|
|
2
|
+
import type { Managers } from "../managers/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PluginState interface
|
|
6
|
+
* Represents the complete state of the Argus plugin instance
|
|
7
|
+
* Includes configuration, project context, and manager instances
|
|
8
|
+
*/
|
|
9
|
+
export interface PluginState {
|
|
10
|
+
config: ArgusConfig;
|
|
11
|
+
projectDir: string;
|
|
12
|
+
managers: Managers;
|
|
13
|
+
isHookEnabled: (name: string) => boolean;
|
|
14
|
+
}
|