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.
Files changed (131) hide show
  1. package/AGENTS.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +249 -0
  4. package/package.json +43 -0
  5. package/skills/INVENTORY.md +79 -0
  6. package/skills/README.md +56 -0
  7. package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +424 -0
  8. package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +157 -0
  9. package/skills/checklists/cyfrin-defi-core/SKILL.md +373 -0
  10. package/skills/checklists/cyfrin-defi-integrations/SKILL.md +412 -0
  11. package/skills/checklists/cyfrin-gas/SKILL.md +55 -0
  12. package/skills/checklists/general-audit/SKILL.md +433 -0
  13. package/skills/methodology/audit-workflow/SKILL.md +129 -0
  14. package/skills/methodology/report-template/SKILL.md +190 -0
  15. package/skills/methodology/severity-classification/SKILL.md +179 -0
  16. package/skills/protocol-patterns/amm-dex/SKILL.md +229 -0
  17. package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +317 -0
  18. package/skills/protocol-patterns/dao-governance/SKILL.md +281 -0
  19. package/skills/protocol-patterns/lending-borrowing/SKILL.md +221 -0
  20. package/skills/protocol-patterns/staking-vesting/SKILL.md +247 -0
  21. package/skills/references/exploit-reference/SKILL.md +259 -0
  22. package/skills/references/smartbugs-examples/SKILL.md +296 -0
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +298 -0
  24. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +59 -0
  25. package/skills/vulnerability-patterns/assert-violation/SKILL.md +59 -0
  26. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +61 -0
  27. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +55 -0
  28. package/skills/vulnerability-patterns/default-visibility/SKILL.md +62 -0
  29. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +60 -0
  30. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +59 -0
  31. package/skills/vulnerability-patterns/dos-revert/SKILL.md +72 -0
  32. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +249 -0
  33. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +51 -0
  34. package/skills/vulnerability-patterns/hash-collision/SKILL.md +52 -0
  35. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +61 -0
  36. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +60 -0
  37. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +59 -0
  38. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +61 -0
  39. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +61 -0
  40. package/skills/vulnerability-patterns/logic-errors/SKILL.md +333 -0
  41. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +60 -0
  42. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +66 -0
  43. package/skills/vulnerability-patterns/off-by-one/SKILL.md +67 -0
  44. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +252 -0
  45. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +65 -0
  46. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +61 -0
  47. package/skills/vulnerability-patterns/reentrancy/SKILL.md +266 -0
  48. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +72 -0
  49. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +59 -0
  50. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +63 -0
  51. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +52 -0
  52. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +65 -0
  53. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +61 -0
  54. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +63 -0
  55. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +56 -0
  56. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +80 -0
  57. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +69 -0
  58. package/skills/vulnerability-patterns/unused-variables/SKILL.md +70 -0
  59. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +81 -0
  60. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +77 -0
  61. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +294 -0
  62. package/src/agents/argus-prompt.ts +407 -0
  63. package/src/agents/pythia-prompt.ts +134 -0
  64. package/src/agents/scribe-prompt.ts +87 -0
  65. package/src/agents/sentinel-prompt.ts +133 -0
  66. package/src/cli/cli-program.ts +67 -0
  67. package/src/cli/commands/doctor.ts +83 -0
  68. package/src/cli/commands/init.ts +46 -0
  69. package/src/cli/commands/install.ts +55 -0
  70. package/src/cli/index.ts +13 -0
  71. package/src/cli/tui-prompts.ts +75 -0
  72. package/src/cli/types.ts +9 -0
  73. package/src/config/index.ts +3 -0
  74. package/src/config/loader.ts +36 -0
  75. package/src/config/schema.ts +82 -0
  76. package/src/config/types.ts +4 -0
  77. package/src/constants/defaults.ts +6 -0
  78. package/src/create-hooks.ts +84 -0
  79. package/src/create-managers.ts +26 -0
  80. package/src/create-tools.ts +30 -0
  81. package/src/features/audit-enforcer/audit-enforcer.ts +34 -0
  82. package/src/features/audit-enforcer/index.ts +1 -0
  83. package/src/features/background-agent/background-manager.ts +200 -0
  84. package/src/features/background-agent/index.ts +1 -0
  85. package/src/features/context-monitor/context-monitor.ts +48 -0
  86. package/src/features/context-monitor/index.ts +4 -0
  87. package/src/features/context-monitor/tool-output-truncator.ts +17 -0
  88. package/src/features/error-recovery/index.ts +2 -0
  89. package/src/features/error-recovery/session-recovery.ts +27 -0
  90. package/src/features/error-recovery/tool-error-recovery.ts +35 -0
  91. package/src/features/index.ts +5 -0
  92. package/src/features/persistent-state/audit-state-manager.ts +121 -0
  93. package/src/features/persistent-state/index.ts +1 -0
  94. package/src/hooks/compaction-hook.ts +50 -0
  95. package/src/hooks/config-handler.ts +116 -0
  96. package/src/hooks/event-hook-v2.ts +93 -0
  97. package/src/hooks/event-hook.ts +74 -0
  98. package/src/hooks/hook-system.ts +9 -0
  99. package/src/hooks/index.ts +5 -0
  100. package/src/hooks/knowledge-sync-hook.ts +57 -0
  101. package/src/hooks/safe-create-hook.ts +15 -0
  102. package/src/hooks/system-prompt-hook.ts +126 -0
  103. package/src/hooks/tool-tracking-hook.ts +234 -0
  104. package/src/hooks/types.ts +16 -0
  105. package/src/index.ts +36 -0
  106. package/src/knowledge/scvd-client.ts +242 -0
  107. package/src/knowledge/scvd-index.ts +183 -0
  108. package/src/knowledge/scvd-sync.ts +85 -0
  109. package/src/managers/index.ts +1 -0
  110. package/src/managers/types.ts +85 -0
  111. package/src/plugin-interface.ts +38 -0
  112. package/src/shared/binary-utils.ts +63 -0
  113. package/src/shared/deep-merge.ts +71 -0
  114. package/src/shared/file-utils.ts +56 -0
  115. package/src/shared/index.ts +5 -0
  116. package/src/shared/jsonc-parser.ts +39 -0
  117. package/src/shared/logger.ts +36 -0
  118. package/src/state/audit-state.ts +27 -0
  119. package/src/state/finding-store.ts +126 -0
  120. package/src/state/plugin-state.ts +14 -0
  121. package/src/state/types.ts +61 -0
  122. package/src/tools/contract-analyzer-tool.ts +184 -0
  123. package/src/tools/forge-fuzz-tool.ts +311 -0
  124. package/src/tools/forge-test-tool.ts +397 -0
  125. package/src/tools/pattern-checker-tool.ts +337 -0
  126. package/src/tools/report-generator-tool.ts +308 -0
  127. package/src/tools/slither-tool.ts +465 -0
  128. package/src/tools/solodit-search-tool.ts +131 -0
  129. package/src/tools/sync-knowledge-tool.ts +116 -0
  130. package/src/utils/project-detector.ts +133 -0
  131. 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
+ }