aegis-audit 2.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.
@@ -0,0 +1,115 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import axios from 'axios';
4
+
5
+ const ETHERSCAN_APIS = {
6
+ ethereum: 'https://api.etherscan.io/api',
7
+ base: 'https://api.basescan.org/api',
8
+ arbitrum: 'https://api.arbiscan.io/api',
9
+ polygon: 'https://api.polygonscan.com/api',
10
+ optimism: 'https://api-optimistic.etherscan.io/api',
11
+ bsc: 'https://api.bscscan.com/api',
12
+ };
13
+
14
+ export async function fetchSource(target, network = 'ethereum') {
15
+ const lc = target.toLowerCase();
16
+
17
+ // ── Local .sol file ──────────────────────────────────────────────
18
+ if (lc.endsWith('.sol') || fs.existsSync(target)) {
19
+ const stat = fs.statSync(target);
20
+ if (stat.isDirectory()) {
21
+ return fetchFolder(target);
22
+ }
23
+ const code = fs.readFileSync(target, 'utf8');
24
+ return {
25
+ type: 'file',
26
+ name: path.basename(target),
27
+ source: code,
28
+ files: [{ name: path.basename(target), code }],
29
+ };
30
+ }
31
+
32
+ // ── Folder of .sol files ─────────────────────────────────────────
33
+ if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
34
+ return fetchFolder(target);
35
+ }
36
+
37
+ // ── On-chain address ─────────────────────────────────────────────
38
+ if (/^0x[0-9a-fA-F]{40}$/.test(target)) {
39
+ return fetchFromChain(target, network);
40
+ }
41
+
42
+ throw new Error(`Cannot resolve target: "${target}"\nExpected: 0x address, .sol file, or folder path`);
43
+ }
44
+
45
+ function fetchFolder(dir) {
46
+ const files = [];
47
+ function walk(d) {
48
+ for (const f of fs.readdirSync(d)) {
49
+ const full = path.join(d, f);
50
+ if (fs.statSync(full).isDirectory()) { walk(full); continue; }
51
+ if (f.endsWith('.sol')) {
52
+ files.push({ name: path.relative(dir, full), code: fs.readFileSync(full, 'utf8') });
53
+ }
54
+ }
55
+ }
56
+ walk(dir);
57
+ if (files.length === 0) throw new Error(`No .sol files found in ${dir}`);
58
+ return {
59
+ type: 'folder',
60
+ name: path.basename(dir),
61
+ source: files.map(f => `// === ${f.name} ===\n${f.code}`).join('\n\n'),
62
+ files,
63
+ };
64
+ }
65
+
66
+ async function fetchFromChain(address, network) {
67
+ const baseUrl = ETHERSCAN_APIS[network.toLowerCase()];
68
+ if (!baseUrl) throw new Error(`Unknown network: ${network}`);
69
+
70
+ // Try without API key first (rate-limited but works for demos)
71
+ const url = `${baseUrl}?module=contract&action=getsourcecode&address=${address}`;
72
+
73
+ const resp = await axios.get(url, { timeout: 15000 });
74
+ const data = resp.data;
75
+
76
+ if (data.status !== '1' || !data.result?.[0]) {
77
+ throw new Error(`Contract not found on ${network}. Is it verified on Etherscan?`);
78
+ }
79
+
80
+ const result = data.result[0];
81
+ if (!result.SourceCode || result.SourceCode === '') {
82
+ throw new Error(`Contract source not verified on ${network}. Upload your .sol file directly instead.`);
83
+ }
84
+
85
+ let files = [];
86
+ let source = result.SourceCode;
87
+
88
+ // Handle multi-file JSON format
89
+ if (source.startsWith('{{') || source.startsWith('{')) {
90
+ try {
91
+ const cleaned = source.startsWith('{{') ? source.slice(1, -1) : source;
92
+ const parsed = JSON.parse(cleaned);
93
+ const sources = parsed.sources || parsed;
94
+ files = Object.entries(sources).map(([name, obj]) => ({
95
+ name,
96
+ code: typeof obj === 'string' ? obj : obj.content,
97
+ }));
98
+ source = files.map(f => `// === ${f.name} ===\n${f.code}`).join('\n\n');
99
+ } catch {
100
+ files = [{ name: `${address}.sol`, code: source }];
101
+ }
102
+ } else {
103
+ files = [{ name: `${address}.sol`, code: source }];
104
+ }
105
+
106
+ return {
107
+ type: 'address',
108
+ name: result.ContractName || address,
109
+ address,
110
+ network,
111
+ compiler: result.CompilerVersion,
112
+ source,
113
+ files,
114
+ };
115
+ }
@@ -0,0 +1,110 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // Enterprise config + audit trail
3
+ // - API key encrypted at rest (AES-256-GCM) with a machine-derived key
4
+ // - Append-only, hash-chained audit log (tamper-evident) — NIST RV / AU
5
+ //
6
+ // Note: machine-derived encryption protects against casual disk inspection and
7
+ // accidental key commits. It is NOT a substitute for a real secrets manager
8
+ // (Vault, AWS Secrets Manager). Enterprise deployments should set
9
+ // ANTHROPIC_API_KEY via their secrets manager and skip stored keys entirely.
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ import crypto from 'crypto';
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import os from 'os';
16
+
17
+ export const CONFIG_DIR = path.join(os.homedir(), '.solguard');
18
+ export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.enc');
19
+ export const AUDIT_LOG = path.join(CONFIG_DIR, 'audit.log');
20
+
21
+ // Derive a key from stable machine + user attributes. Best-effort obfuscation.
22
+ function machineKey() {
23
+ const material = [os.hostname(), os.userInfo().username, os.platform(), os.arch()].join('|');
24
+ return crypto.createHash('sha256').update(material + '::solguard-v2').digest();
25
+ }
26
+
27
+ export function saveConfig(obj) {
28
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
29
+ const iv = crypto.randomBytes(12);
30
+ const cipher = crypto.createCipheriv('aes-256-gcm', machineKey(), iv);
31
+ const enc = Buffer.concat([cipher.update(JSON.stringify(obj), 'utf8'), cipher.final()]);
32
+ const tag = cipher.getAuthTag();
33
+ const blob = Buffer.concat([iv, tag, enc]).toString('base64');
34
+ fs.writeFileSync(CONFIG_FILE, blob, { mode: 0o600 });
35
+ }
36
+
37
+ export function loadConfig() {
38
+ try {
39
+ if (!fs.existsSync(CONFIG_FILE)) return {};
40
+ const blob = Buffer.from(fs.readFileSync(CONFIG_FILE, 'utf8'), 'base64');
41
+ const iv = blob.subarray(0, 12);
42
+ const tag = blob.subarray(12, 28);
43
+ const enc = blob.subarray(28);
44
+ const decipher = crypto.createDecipheriv('aes-256-gcm', machineKey(), iv);
45
+ decipher.setAuthTag(tag);
46
+ const dec = Buffer.concat([decipher.update(enc), decipher.final()]);
47
+ return JSON.parse(dec.toString('utf8'));
48
+ } catch {
49
+ return {};
50
+ }
51
+ }
52
+
53
+ // ── Tamper-evident audit log ────────────────────────────────────────────────
54
+ // Each entry includes a hash of the previous entry, forming a chain. Any edit
55
+ // to a past entry breaks every subsequent hash — detectable via verifyAuditLog.
56
+ export function auditAppend(event) {
57
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
58
+
59
+ let prevHash = 'GENESIS';
60
+ if (fs.existsSync(AUDIT_LOG)) {
61
+ const lines = fs.readFileSync(AUDIT_LOG, 'utf8').trim().split('\n').filter(Boolean);
62
+ if (lines.length) {
63
+ try { prevHash = JSON.parse(lines[lines.length - 1]).hash; } catch {}
64
+ }
65
+ }
66
+
67
+ const entry = {
68
+ ts: new Date().toISOString(),
69
+ event: event.type,
70
+ target: event.target ? redactTarget(event.target) : undefined,
71
+ findings: event.findings,
72
+ score: event.score,
73
+ actor: os.userInfo().username,
74
+ prevHash,
75
+ };
76
+ entry.hash = crypto.createHash('sha256')
77
+ .update(JSON.stringify({ ...entry, hash: undefined }))
78
+ .digest('hex');
79
+
80
+ fs.appendFileSync(AUDIT_LOG, JSON.stringify(entry) + '\n', { mode: 0o600 });
81
+ return entry.hash;
82
+ }
83
+
84
+ export function verifyAuditLog() {
85
+ if (!fs.existsSync(AUDIT_LOG)) return { valid: true, entries: 0 };
86
+ const lines = fs.readFileSync(AUDIT_LOG, 'utf8').trim().split('\n').filter(Boolean);
87
+ let prevHash = 'GENESIS';
88
+ for (let i = 0; i < lines.length; i++) {
89
+ let entry;
90
+ try {
91
+ entry = JSON.parse(lines[i]);
92
+ } catch {
93
+ return { valid: false, entries: lines.length, brokenAt: i + 1, reason: 'malformed entry' };
94
+ }
95
+ const recomputed = crypto.createHash('sha256')
96
+ .update(JSON.stringify({ ...entry, hash: undefined }))
97
+ .digest('hex');
98
+ if (entry.prevHash !== prevHash || entry.hash !== recomputed) {
99
+ return { valid: false, entries: lines.length, brokenAt: i + 1, reason: 'hash chain broken' };
100
+ }
101
+ prevHash = entry.hash;
102
+ }
103
+ return { valid: true, entries: lines.length };
104
+ }
105
+
106
+ // Don't write full local paths or full addresses to the audit log unnecessarily.
107
+ function redactTarget(t) {
108
+ if (/^0x[0-9a-fA-F]{40}$/.test(t)) return t.slice(0, 10) + '…' + t.slice(-4);
109
+ return path.basename(t);
110
+ }