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,123 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { execSync } from 'child_process';
5
+ import { runBenchmark, FIXTURES_DIR } from '../../benchmark/runner.js';
6
+ import { sectionHeader, dim } from '../ui/banner.js';
7
+
8
+ chalk.level = 3;
9
+
10
+ const SMARTBUGS_REPO = 'https://github.com/smartbugs/smartbugs-curated.git';
11
+
12
+ export async function benchmarkCommand(options) {
13
+ chalk.level = 3;
14
+
15
+ let datasetDir = FIXTURES_DIR;
16
+ let datasetName = 'built-in fixtures (7 contracts)';
17
+
18
+ // Use a custom dataset dir if provided
19
+ if (options.dataset) {
20
+ datasetDir = options.dataset;
21
+ datasetName = options.dataset;
22
+ if (!fs.existsSync(datasetDir)) {
23
+ console.log('\n' + chalk.hex('#ff4560')(`X Dataset directory not found: ${datasetDir}`) + '\n');
24
+ process.exit(2);
25
+ }
26
+ }
27
+
28
+ // Optionally fetch the full SmartBugs Curated dataset (143 contracts)
29
+ if (options.fetchSmartbugs) {
30
+ const target = path.join(process.cwd(), '.solguard-smartbugs');
31
+ if (!fs.existsSync(target)) {
32
+ console.log(dim(' Cloning SmartBugs Curated (143 labeled contracts)...'));
33
+ try {
34
+ execSync(`git clone --depth 1 ${SMARTBUGS_REPO} "${target}"`, { stdio: 'ignore' });
35
+ } catch {
36
+ console.log('\n' + chalk.hex('#ff4560')('X Failed to clone SmartBugs. Check git + network, then retry.') + '\n');
37
+ process.exit(2);
38
+ }
39
+ }
40
+ datasetDir = path.join(target, 'dataset');
41
+ datasetName = 'SmartBugs Curated (143 labeled contracts, DASP taxonomy)';
42
+ }
43
+
44
+ console.log(dim(`\n Dataset: ${datasetName}`));
45
+ console.log(dim(' Scoring: static detector layer only (deterministic). Contract-level, per DASP category.\n'));
46
+
47
+ const report = runBenchmark(datasetDir);
48
+
49
+ // ── Per-category table ──────────────────────────────────────────────────
50
+ sectionHeader('PER-CATEGORY RESULTS');
51
+ console.log(
52
+ ' ' +
53
+ chalk.hex('#7a90a8')('Category'.padEnd(26)) +
54
+ chalk.hex('#7a90a8')('Supp'.padStart(5)) +
55
+ chalk.hex('#7a90a8')('TP'.padStart(4)) +
56
+ chalk.hex('#7a90a8')('FN'.padStart(4)) +
57
+ chalk.hex('#7a90a8')('FP'.padStart(4)) +
58
+ chalk.hex('#7a90a8')('Recall'.padStart(9)) +
59
+ chalk.hex('#7a90a8')('Prec'.padStart(8)) +
60
+ chalk.hex('#7a90a8')('F1'.padStart(8))
61
+ );
62
+ console.log(' ' + chalk.hex('#162030')('-'.repeat(66)));
63
+
64
+ for (const [cat, m] of Object.entries(report.categories)) {
65
+ if (m.support === 0) continue;
66
+ const recall = pct(m.recall);
67
+ const recColor = m.recall >= 0.8 ? '#00e6b4' : m.recall >= 0.5 ? '#ffb740' : '#ff4560';
68
+ console.log(
69
+ ' ' +
70
+ chalk.hex('#c8d8e8')(m.label.slice(0, 25).padEnd(26)) +
71
+ chalk.hex('#c8d8e8')(String(m.support).padStart(5)) +
72
+ chalk.hex('#00e6b4')(String(m.tp).padStart(4)) +
73
+ chalk.hex('#ff4560')(String(m.fn).padStart(4)) +
74
+ chalk.hex('#ffb740')(String(m.fp).padStart(4)) +
75
+ chalk.hex(recColor)(recall.padStart(9)) +
76
+ chalk.hex('#c8d8e8')(pct(m.precision).padStart(8)) +
77
+ chalk.hex('#c8d8e8')(pct(m.f1).padStart(8))
78
+ );
79
+ }
80
+
81
+ // ── Overall ─────────────────────────────────────────────────────────────
82
+ sectionHeader('OVERALL (micro-averaged)');
83
+ const o = report.overall;
84
+ const recColor = o.microRecall >= 0.8 ? '#00e6b4' : o.microRecall >= 0.5 ? '#ffb740' : '#ff4560';
85
+ console.log(` ${chalk.white.bold('Contracts tested:')} ${o.contractsTested}`);
86
+ console.log(` ${chalk.white.bold('Vuln instances:')} ${o.totalVulnInstances}`);
87
+ console.log(` ${chalk.white.bold('True positives:')} ${chalk.hex('#00e6b4')(o.truePositives)}`);
88
+ console.log(` ${chalk.white.bold('False negatives:')} ${chalk.hex('#ff4560')(o.falseNegatives)} ${dim('(missed vulnerabilities)')}`);
89
+ console.log(` ${chalk.white.bold('False positives:')} ${chalk.hex('#ffb740')(o.falsePositives)}`);
90
+ console.log('');
91
+ console.log(` ${chalk.white.bold('Recall:')} ${chalk.hex(recColor).bold(pct(o.microRecall))} ${dim('(detection rate)')}`);
92
+ console.log(` ${chalk.white.bold('Precision:')} ${chalk.hex('#c8d8e8').bold(pct(o.microPrecision))}`);
93
+ console.log(` ${chalk.white.bold('F1 score:')} ${chalk.hex('#c8d8e8').bold(pct(o.microF1))}`);
94
+ console.log(` ${chalk.white.bold('False-negative rate:')} ${chalk.hex('#ff4560').bold(pct(o.falseNegativeRate))}`);
95
+
96
+ // ── Clean-contract false positives ──────────────────────────────────────
97
+ if (report.clean.cleanContracts > 0) {
98
+ sectionHeader('FALSE-POSITIVE CHECK (clean contracts)');
99
+ const fpColor = report.clean.falsePositiveRate <= 0.2 ? '#00e6b4' : '#ffb740';
100
+ console.log(` ${chalk.white.bold('Clean contracts:')} ${report.clean.cleanContracts}`);
101
+ console.log(` ${chalk.white.bold('Flagged anyway:')} ${chalk.hex(fpColor)(report.clean.cleanFalsePositives)}`);
102
+ console.log(` ${chalk.white.bold('False-positive rate:')} ${chalk.hex(fpColor).bold(pct(report.clean.falsePositiveRate))}`);
103
+ }
104
+
105
+ // ── Honesty footer ──────────────────────────────────────────────────────
106
+ console.log('\n' + chalk.hex('#162030')('-'.repeat(72)));
107
+ console.log(dim(' These numbers reflect the deterministic static layer only. The Claude AI'));
108
+ console.log(dim(' layer adds semantic findings not measured here, so production recall is'));
109
+ console.log(dim(' higher — but a non-zero false-negative rate means this tool MUST NOT be'));
110
+ console.log(dim(' the only gate before deploying high-value contracts.'));
111
+ console.log(chalk.hex('#162030')('-'.repeat(72)) + '\n');
112
+
113
+ // ── Machine-readable output ─────────────────────────────────────────────
114
+ if (options.output) {
115
+ fs.writeFileSync(options.output, JSON.stringify(report, null, 2));
116
+ console.log(chalk.hex('#00e6b4')('OK ') + `Full benchmark report written to ${chalk.hex('#4da6ff')(options.output)}\n`);
117
+ }
118
+ }
119
+
120
+ function pct(x) {
121
+ if (x === null || x === undefined) return 'n/a';
122
+ return (x * 100).toFixed(1) + '%';
123
+ }
@@ -0,0 +1,27 @@
1
+ import { input } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import boxen from 'boxen';
4
+ import { loadConfig, saveConfig, verifyAuditLog } from '../utils/secure-config.js';
5
+
6
+ export async function configCommand() {
7
+ chalk.level = 3;
8
+ console.log(boxen(
9
+ chalk.hex('#00e6b4').bold('SolGuard Configuration') + '\n' +
10
+ chalk.hex('#7a90a8')('API key is encrypted at rest (AES-256-GCM) in ~/.solguard/config.enc\n') +
11
+ chalk.hex('#7a90a8')('Enterprise: prefer setting ANTHROPIC_API_KEY via your secrets manager,\nor use --offline to never transmit source code.'),
12
+ { padding: 1, borderColor: '#162030', borderStyle: 'round' }
13
+ ));
14
+
15
+ const current = loadConfig();
16
+ const apiKey = await input({
17
+ message: chalk.white('Anthropic API key') + chalk.hex('#4a5a6a')(' (console.anthropic.com):'),
18
+ default: current.apiKey ? '***' + current.apiKey.slice(-6) : undefined,
19
+ });
20
+ const finalKey = apiKey.startsWith('***') ? current.apiKey : apiKey.trim();
21
+ saveConfig({ ...current, apiKey: finalKey });
22
+
23
+ console.log('\n' + chalk.hex('#00e6b4')('OK ') + chalk.white('Config encrypted and saved.'));
24
+
25
+ const audit = verifyAuditLog();
26
+ console.log(chalk.hex('#7a90a8')(` Audit log: ${audit.entries} entries, integrity ${audit.valid ? chalk.hex('#00e6b4')('VALID') : chalk.hex('#ff4560')('BROKEN at #' + audit.brokenAt)}`) + '\n');
27
+ }
@@ -0,0 +1,97 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // SolGuard knowledge base
3
+ // Maps every detector to authoritative framework identifiers so findings are
4
+ // traceable to OWASP SC Top 10 (2026), MITRE ATT&CK, and CWE.
5
+ //
6
+ // Sources:
7
+ // - OWASP Smart Contract Top 10 : 2026 (owasp.org/www-project-smart-contract-top-10)
8
+ // - MITRE ATT&CK v18
9
+ // - MITRE CWE
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ // OWASP Smart Contract Top 10 — 2026 (forward-looking, built on 2025 incident data)
13
+ export const OWASP_SC_2026 = {
14
+ SC01: { id: 'SC01:2026', title: 'Access Control Vulnerabilities',
15
+ desc: 'Unauthorized users or roles invoke privileged functions or modify critical state, often leading to full protocol compromise when admin, governance, or upgrade paths are exposed.',
16
+ loss2024: '$953.2M' },
17
+ SC02: { id: 'SC02:2026', title: 'Business Logic Vulnerabilities',
18
+ desc: 'Design-level flaws in lending, AMM, reward, or governance logic that break intended economic rules, letting attackers extract value even when low-level checks pass.',
19
+ loss2024: '$63.8M' },
20
+ SC03: { id: 'SC03:2026', title: 'Price Oracle Manipulation',
21
+ desc: 'Weak oracles and unsafe price integrations let attackers skew reference prices, enabling under-collateralized borrowing, unfair liquidations, and mispriced swaps.',
22
+ loss2024: '$8.8M' },
23
+ SC04: { id: 'SC04:2026', title: 'Flash Loan-Facilitated Attacks',
24
+ desc: 'Large uncollateralized flash loans magnify small bugs into large drains via complex multi-step sequences in a single transaction.',
25
+ loss2024: '$33.8M' },
26
+ SC05: { id: 'SC05:2026', title: 'Lack of Input Validation',
27
+ desc: 'Missing or weak validation of user, admin, or cross-chain inputs allows unsafe parameters to reach core logic, corrupting state or enabling direct fund loss.',
28
+ loss2024: '$14.6M' },
29
+ SC06: { id: 'SC06:2026', title: 'Unchecked External Calls',
30
+ desc: 'Unsafe interactions with external contracts where failures, reverts, or callbacks are not handled, often enabling reentrancy or inconsistent state.',
31
+ loss2024: '$550.7K' },
32
+ SC07: { id: 'SC07:2026', title: 'Arithmetic Errors',
33
+ desc: 'Subtle bugs in integer math, scaling, and rounding — especially in share, interest, and AMM calculations — exploitable for precision loss or value siphoning.',
34
+ loss2024: 'n/a' },
35
+ SC08: { id: 'SC08:2026', title: 'Reentrancy Attacks',
36
+ desc: 'External calls re-enter vulnerable functions before state is updated, allowing repeated withdrawals from an outdated view of state.',
37
+ loss2024: '$35.7M' },
38
+ SC09: { id: 'SC09:2026', title: 'Integer Overflow and Underflow',
39
+ desc: 'Arithmetic on code paths without overflow checks leads to wrapped values, broken invariants, and potential drains.',
40
+ loss2024: 'n/a' },
41
+ SC10: { id: 'SC10:2026', title: 'Proxy & Upgradeability Vulnerabilities',
42
+ desc: 'Misconfigured proxy, initialization, and upgrade mechanisms let attackers seize implementations or reinitialize critical state.',
43
+ loss2024: 'n/a' },
44
+ };
45
+
46
+ // MITRE ATT&CK techniques relevant to smart-contract / Web3 threat modeling.
47
+ // On-chain exploitation maps imperfectly to ATT&CK (built for traditional IT),
48
+ // so we use the closest applicable techniques plus supply-chain ones that APTs
49
+ // (e.g. Lazarus/BlueNoroff G0032) actually use against Web3 teams.
50
+ export const MITRE = {
51
+ T1195: { id: 'T1195', name: 'Supply Chain Compromise' },
52
+ T1195_001:{ id: 'T1195.001',name: 'Compromise Software Dependencies and Development Tools' },
53
+ T1190: { id: 'T1190', name: 'Exploit Public-Facing Application' },
54
+ T1059: { id: 'T1059', name: 'Command and Scripting Interpreter' },
55
+ T1499: { id: 'T1499', name: 'Endpoint Denial of Service' },
56
+ T1078: { id: 'T1078', name: 'Valid Accounts (privileged key abuse)' },
57
+ T1556: { id: 'T1556', name: 'Modify Authentication Process' },
58
+ T1565: { id: 'T1565', name: 'Data Manipulation (on-chain state/price)' },
59
+ T1583: { id: 'T1583', name: 'Acquire Infrastructure (attacker contracts)' },
60
+ };
61
+
62
+ // CWE identifiers for deterministic, tool-agnostic classification.
63
+ export const CWE = {
64
+ CWE_284: { id: 'CWE-284', name: 'Improper Access Control' },
65
+ CWE_285: { id: 'CWE-285', name: 'Improper Authorization' },
66
+ CWE_841: { id: 'CWE-841', name: 'Improper Enforcement of Behavioral Workflow' },
67
+ CWE_682: { id: 'CWE-682', name: 'Incorrect Calculation' },
68
+ CWE_190: { id: 'CWE-190', name: 'Integer Overflow or Wraparound' },
69
+ CWE_191: { id: 'CWE-191', name: 'Integer Underflow' },
70
+ CWE_252: { id: 'CWE-252', name: 'Unchecked Return Value' },
71
+ CWE_20: { id: 'CWE-20', name: 'Improper Input Validation' },
72
+ CWE_362: { id: 'CWE-362', name: 'Race Condition' },
73
+ CWE_841b: { id: 'CWE-841', name: 'Reentrancy' },
74
+ CWE_330: { id: 'CWE-330', name: 'Use of Insufficiently Random Values' },
75
+ CWE_400: { id: 'CWE-400', name: 'Uncontrolled Resource Consumption' },
76
+ CWE_829: { id: 'CWE-829', name: 'Inclusion of Functionality from Untrusted Control Sphere' },
77
+ CWE_665: { id: 'CWE-665', name: 'Improper Initialization' },
78
+ CWE_345: { id: 'CWE-345', name: 'Insufficient Verification of Data Authenticity' },
79
+ };
80
+
81
+ // NIST SSDF (SP 800-218) practices this tool helps satisfy, for the
82
+ // compliance appendix in enterprise reports.
83
+ export const NIST_SSDF = {
84
+ 'PW.7': 'Review and/or analyze human-readable code to identify vulnerabilities (this scan).',
85
+ 'PW.8': 'Test executable code to identify vulnerabilities and verify compliance.',
86
+ 'PS.3': 'Archive and protect each software release (SBOM generation).',
87
+ 'PS.2': 'Provide a mechanism for verifying software release integrity (signed reports).',
88
+ 'RV.1': 'Identify and confirm vulnerabilities on an ongoing basis.',
89
+ 'RV.2': 'Assess, prioritize, and remediate vulnerabilities.',
90
+ 'RV.3': 'Analyze vulnerabilities to identify their root causes.',
91
+ };
92
+
93
+ export const DISCLAIMER = `SolGuard is an AI-assisted automated scanner. It is NOT a substitute for a
94
+ professional manual audit, formal verification, or economic/game-theoretic review.
95
+ Automated analysis produces both false positives and false negatives. For
96
+ high-value or production deployments, commission an independent human audit and,
97
+ where applicable, formal verification of critical invariants.`;
@@ -0,0 +1,125 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // Enterprise output formats
3
+ // - SBOM (CycloneDX 1.5) — NIST SSDF PS.3, EO 14028 software supply chain
4
+ // - SARIF 2.1.0 — standard for GitHub Code Scanning / CI ingestion
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+
7
+ import crypto from 'crypto';
8
+
9
+ // Generate a CycloneDX SBOM from detected imports / dependencies in the source.
10
+ export function generateSBOM(contractInfo) {
11
+ const source = contractInfo.source;
12
+
13
+ // Extract Solidity imports (the contract's "dependencies")
14
+ const importRe = /import\s+(?:\{[^}]*\}\s+from\s+)?["']([^"']+)["']/g;
15
+ const deps = new Set();
16
+ let m;
17
+ while ((m = importRe.exec(source)) !== null) deps.add(m[1]);
18
+
19
+ // Detect well-known library families
20
+ const libraries = [];
21
+ if (/@openzeppelin/.test(source)) libraries.push({ name: '@openzeppelin/contracts', purl: 'pkg:npm/@openzeppelin/contracts' });
22
+ if (/@chainlink/.test(source)) libraries.push({ name: '@chainlink/contracts', purl: 'pkg:npm/@chainlink/contracts' });
23
+ if (/@uniswap/.test(source)) libraries.push({ name: '@uniswap/v3-core', purl: 'pkg:npm/@uniswap/v3-core' });
24
+ if (/solmate/.test(source)) libraries.push({ name: 'solmate', purl: 'pkg:github/transmissions11/solmate' });
25
+
26
+ const components = [
27
+ ...[...deps].map(d => ({
28
+ type: 'library',
29
+ name: d,
30
+ scope: 'required',
31
+ hashes: [{ alg: 'SHA-256', content: sha256(d) }],
32
+ })),
33
+ ...libraries.map(l => ({
34
+ type: 'library',
35
+ name: l.name,
36
+ purl: l.purl,
37
+ scope: 'required',
38
+ })),
39
+ ];
40
+
41
+ return {
42
+ bomFormat: 'CycloneDX',
43
+ specVersion: '1.5',
44
+ serialNumber: `urn:uuid:${crypto.randomUUID()}`,
45
+ version: 1,
46
+ metadata: {
47
+ timestamp: new Date().toISOString(),
48
+ tools: [{ vendor: 'SolGuard', name: 'solguard', version: '2.0.0' }],
49
+ component: {
50
+ type: 'application',
51
+ name: contractInfo.name,
52
+ ...(contractInfo.address && { 'bom-ref': contractInfo.address }),
53
+ hashes: [{ alg: 'SHA-256', content: sha256(source) }],
54
+ },
55
+ },
56
+ components,
57
+ };
58
+ }
59
+
60
+ // Convert findings to SARIF 2.1.0 for CI ingestion (GitHub Code Scanning, etc.)
61
+ export function generateSARIF(contractInfo, findings) {
62
+ const rules = [];
63
+ const seenRules = new Set();
64
+ const results = [];
65
+
66
+ const sarifLevel = { CRITICAL: 'error', HIGH: 'error', MEDIUM: 'warning', LOW: 'note' };
67
+
68
+ for (const f of findings) {
69
+ const ruleId = f.detectorId || f.owasp || f.title.slice(0, 24).replace(/\s+/g, '-');
70
+ if (!seenRules.has(ruleId)) {
71
+ seenRules.add(ruleId);
72
+ rules.push({
73
+ id: ruleId,
74
+ name: f.title,
75
+ shortDescription: { text: f.title },
76
+ fullDescription: { text: f.description },
77
+ helpUri: f.owasp ? `https://owasp.org/www-project-smart-contract-top-10/` : undefined,
78
+ properties: {
79
+ tags: [f.owasp, f.cwe, f.mitre].filter(Boolean),
80
+ 'security-severity': severityScore(f.severity),
81
+ },
82
+ defaultConfiguration: { level: sarifLevel[f.severity] || 'warning' },
83
+ });
84
+ }
85
+
86
+ const lineMatch = /Line (\d+)/.exec(f.location || '');
87
+ results.push({
88
+ ruleId,
89
+ level: sarifLevel[f.severity] || 'warning',
90
+ message: { text: `${f.description}\n\nFix: ${f.fix}` },
91
+ locations: [{
92
+ physicalLocation: {
93
+ artifactLocation: { uri: contractInfo.files?.[0]?.name || contractInfo.name },
94
+ region: lineMatch ? { startLine: parseInt(lineMatch[1], 10) } : { startLine: 1 },
95
+ },
96
+ }],
97
+ properties: {
98
+ owasp: f.owasp, cwe: f.cwe, mitre: f.mitre,
99
+ exploitLikelihood: f.exploitLikelihood, attackerCost: f.attackerCost,
100
+ },
101
+ });
102
+ }
103
+
104
+ return {
105
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
106
+ version: '2.1.0',
107
+ runs: [{
108
+ tool: { driver: {
109
+ name: 'SolGuard',
110
+ version: '2.0.0',
111
+ informationUri: 'https://github.com/rsh1k/solguard',
112
+ rules,
113
+ }},
114
+ results,
115
+ }],
116
+ };
117
+ }
118
+
119
+ function severityScore(sev) {
120
+ return ({ CRITICAL: '9.5', HIGH: '7.5', MEDIUM: '5.0', LOW: '2.5' })[sev] || '5.0';
121
+ }
122
+
123
+ export function sha256(s) {
124
+ return crypto.createHash('sha256').update(s).digest('hex');
125
+ }
@@ -0,0 +1,77 @@
1
+ import chalk from 'chalk';
2
+ import boxen from 'boxen';
3
+
4
+ export async function banner() {
5
+ // Force color on
6
+ chalk.level = 3;
7
+
8
+ const title = [
9
+ ' ███████╗ ██████╗ ██╗ ██████╗ ██╗ ██╗ █████╗ ██████╗ ██████╗ ',
10
+ ' ██╔════╝██╔═══██╗██║ ██╔════╝ ██║ ██║██╔══██╗██╔══██╗██╔══██╗',
11
+ ' ███████╗██║ ██║██║ ██║ ███╗██║ ██║███████║██████╔╝██║ ██║',
12
+ ' ╚════██║██║ ██║██║ ██║ ██║██║ ██║██╔══██║██╔══██╗██║ ██║',
13
+ ' ███████║╚██████╔╝███████╗╚██████╔╝╚██████╔╝██║ ██║██║ ██║██████╔╝',
14
+ ' ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ',
15
+ ].map(l => chalk.hex('#00e6b4')(l)).join('\n');
16
+
17
+ const subtitle = chalk.hex('#4a5a6a')(' Aegis · AI-powered smart contract security auditor');
18
+ const version = chalk.hex('#4a5a6a')(' v2.1.0 · OWASP SC Top 10 2026 · MITRE · NIST SSDF');
19
+
20
+ console.log('\n' + title);
21
+ console.log(subtitle);
22
+ console.log(version + '\n');
23
+ }
24
+
25
+ export function sectionHeader(title) {
26
+ chalk.level = 3;
27
+ const line = chalk.hex('#162030')('─'.repeat(60));
28
+ const label = chalk.hex('#00e6b4').bold(` ${title} `);
29
+ console.log('\n' + chalk.hex('#00e6b4')('┌') + line);
30
+ console.log(chalk.hex('#00e6b4')('│') + label);
31
+ console.log(chalk.hex('#00e6b4')('└') + line);
32
+ }
33
+
34
+ export function severityBadge(sev) {
35
+ chalk.level = 3;
36
+ const map = {
37
+ CRITICAL: chalk.bgHex('#ff4560').white.bold(' CRITICAL '),
38
+ HIGH: chalk.bgHex('#ffb740').black.bold(' HIGH '),
39
+ MEDIUM: chalk.bgHex('#4da6ff').black.bold(' MEDIUM '),
40
+ LOW: chalk.bgHex('#4a5a6a').white.bold(' LOW '),
41
+ };
42
+ return map[sev] || chalk.gray(` ${sev} `);
43
+ }
44
+
45
+ export function scoreBar(score, width = 30) {
46
+ chalk.level = 3;
47
+ const filled = Math.round((score / 100) * width);
48
+ const empty = width - filled;
49
+ const color = score >= 75 ? '#00e6b4' : score >= 50 ? '#ffb740' : '#ff4560';
50
+ const bar = chalk.hex(color)('█'.repeat(filled)) + chalk.hex('#162030')('░'.repeat(empty));
51
+ return `[${bar}] ${chalk.hex(color).bold(score + '/100')}`;
52
+ }
53
+
54
+ export function dim(text) {
55
+ chalk.level = 3;
56
+ return chalk.hex('#4a5a6a')(text);
57
+ }
58
+
59
+ export function success(text) {
60
+ chalk.level = 3;
61
+ return chalk.hex('#00e6b4')('✔ ') + text;
62
+ }
63
+
64
+ export function warn(text) {
65
+ chalk.level = 3;
66
+ return chalk.hex('#ffb740')('⚠ ') + text;
67
+ }
68
+
69
+ export function error(text) {
70
+ chalk.level = 3;
71
+ return chalk.hex('#ff4560')('✖ ') + text;
72
+ }
73
+
74
+ export function info(text) {
75
+ chalk.level = 3;
76
+ return chalk.hex('#4da6ff')('ℹ ') + text;
77
+ }
@@ -0,0 +1,178 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import { severityBadge, scoreBar, sectionHeader, dim, success, warn } from './banner.js';
4
+ import { DISCLAIMER, NIST_SSDF } from '../knowledge/frameworks.js';
5
+
6
+ chalk.level = 3;
7
+ const SEV_ORDER = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
8
+
9
+ function wrap(text, indent = ' ', width = 76) {
10
+ const words = (text || '').split(' ');
11
+ let line = indent; const out = [];
12
+ for (const w of words) {
13
+ if ((line + w).length > width) { out.push(line); line = indent + w + ' '; }
14
+ else line += w + ' ';
15
+ }
16
+ if (line.trim()) out.push(line);
17
+ return out.join('\n');
18
+ }
19
+
20
+ export function renderReport(contractInfo, findings, meta, options) {
21
+ chalk.level = 3;
22
+
23
+ if (options.json) {
24
+ console.log(JSON.stringify({ contractInfo: { name: contractInfo.name, address: contractInfo.address }, score: meta.score, verdict: meta.verdict, findings, attackPaths: meta.attackPaths }, null, 2));
25
+ return;
26
+ }
27
+
28
+ const verdictColor = meta.score >= 80 ? '#00e6b4' : meta.score >= 60 ? '#ffb740' : '#ff4560';
29
+
30
+ sectionHeader('CONTRACT');
31
+ console.log(` ${chalk.white.bold('Name:')} ${chalk.hex('#c8d8e8')(contractInfo.name)}`);
32
+ if (contractInfo.address) console.log(` ${chalk.white.bold('Address:')} ${chalk.hex('#4da6ff')(contractInfo.address)} (${contractInfo.network})`);
33
+ console.log(` ${chalk.white.bold('Solidity:')} ${chalk.hex('#c8d8e8')(meta.metrics.solidityVersion)}`);
34
+ console.log(` ${chalk.white.bold('Files:')} ${chalk.hex('#c8d8e8')(contractInfo.files?.length ?? 1)}`);
35
+
36
+ sectionHeader('RISK SCORE');
37
+ console.log(`\n ${scoreBar(meta.score)}`);
38
+ console.log(`\n ${chalk.hex(verdictColor).bold('> ' + meta.verdict)}`);
39
+ if (meta.aiSummary) console.log(`\n${wrap(meta.aiSummary, ' ')}`);
40
+
41
+ console.log('');
42
+ for (const [label, val] of Object.entries(meta.breakdown)) {
43
+ const col = val >= 75 ? '#00e6b4' : val >= 50 ? '#ffb740' : '#ff4560';
44
+ const filled = Math.round((val / 100) * 18);
45
+ const bar = chalk.hex(col)('#'.repeat(filled)) + chalk.hex('#162030')('-'.repeat(18 - filled));
46
+ console.log(` ${chalk.hex('#4a5a6a')(label.padEnd(18))} [${bar}] ${chalk.hex(col).bold(String(val).padStart(3))}`);
47
+ }
48
+
49
+ const counts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
50
+ for (const f of findings) counts[f.severity]++;
51
+ sectionHeader('FINDINGS SUMMARY');
52
+ console.log(
53
+ ` ${chalk.hex('#ff4560').bold(counts.CRITICAL + ' Critical')} ` +
54
+ `${chalk.hex('#ffb740').bold(counts.HIGH + ' High')} ` +
55
+ `${chalk.hex('#4da6ff').bold(counts.MEDIUM + ' Medium')} ` +
56
+ `${chalk.hex('#4a5a6a').bold(counts.LOW + ' Low')}`
57
+ );
58
+
59
+ // Attack paths — the red-team lens
60
+ if (meta.attackPaths && meta.attackPaths.length) {
61
+ sectionHeader('ATTACK PATHS (red-team analysis)');
62
+ for (const p of meta.attackPaths) {
63
+ console.log(`\n ${severityBadge(p.severity)} ${chalk.white.bold(p.name)}`);
64
+ console.log(` ${dim('OWASP:')} ${chalk.hex('#4da6ff')(p.owasp.join(', '))} ${dim('MITRE:')} ${chalk.hex('#4da6ff')(p.mitre.join(', '))}`);
65
+ p.steps.forEach((s, i) => console.log(chalk.hex('#7a90a8')(` ${i + 1}. ${s}`)));
66
+ }
67
+ }
68
+
69
+ if (findings.length) {
70
+ sectionHeader(`FINDINGS (${findings.length})`);
71
+ const sorted = [...findings].sort((a, b) => (SEV_ORDER[a.severity] ?? 9) - (SEV_ORDER[b.severity] ?? 9));
72
+ for (let i = 0; i < sorted.length; i++) {
73
+ const f = sorted[i];
74
+ console.log(`\n ${severityBadge(f.severity)} ${chalk.white.bold(f.title)}`);
75
+ const tags = [f.owasp, f.cwe, f.mitre].filter(Boolean).join(' ');
76
+ if (tags) console.log(` ${chalk.hex('#4da6ff')(tags)}`);
77
+ console.log(` ${dim('Location:')} ${f.location || 'n/a'} ${dim('Detector:')} ${f.source === 'static' ? 'static' : 'Claude AI'}` +
78
+ (f.exploitLikelihood ? ` ${dim('Exploitability:')} ${f.exploitLikelihood}/5 ${dim('Attacker cost:')} ${f.attackerCost}` : ''));
79
+ console.log('\n' + wrap(f.description, ' '));
80
+ if (f.fix) console.log(`\n ${chalk.hex('#00e6b4').bold('Fix:')} ${wrap(f.fix, '').trim()}`);
81
+ if (i < sorted.length - 1) console.log('\n ' + chalk.hex('#162030')('-'.repeat(60)));
82
+ }
83
+ } else {
84
+ console.log('\n ' + success('No findings from automated detectors.'));
85
+ }
86
+
87
+ // NIST SSDF compliance appendix
88
+ sectionHeader('NIST SSDF (SP 800-218) COVERAGE');
89
+ for (const [k, v] of Object.entries(NIST_SSDF)) {
90
+ console.log(` ${chalk.hex('#00e6b4')(k.padEnd(6))} ${chalk.hex('#7a90a8')(v)}`);
91
+ }
92
+
93
+ console.log('\n' + chalk.hex('#162030')('-'.repeat(78)));
94
+ console.log(wrap(DISCLAIMER, ' '));
95
+ console.log(chalk.hex('#162030')('-'.repeat(78)) + '\n');
96
+
97
+ if (options.output) {
98
+ saveMarkdown(options.output, contractInfo, findings, meta);
99
+ console.log(success(`Markdown report saved to ${chalk.hex('#4da6ff')(options.output)}\n`));
100
+ }
101
+ }
102
+
103
+ function saveMarkdown(outputPath, contractInfo, findings, meta) {
104
+ const sorted = [...findings].sort((a, b) => (SEV_ORDER[a.severity] ?? 9) - (SEV_ORDER[b.severity] ?? 9));
105
+ const counts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
106
+ for (const f of findings) counts[f.severity]++;
107
+
108
+ const md = `# SolGuard Security Report
109
+
110
+ **Contract:** ${contractInfo.name}
111
+ ${contractInfo.address ? `**Address:** \`${contractInfo.address}\` (${contractInfo.network})` : ''}
112
+ **Solidity:** ${meta.metrics.solidityVersion}
113
+ **Date:** ${new Date().toISOString().slice(0, 19)}Z
114
+ **Scanner:** SolGuard v2.0.0 ${meta.offline ? '(offline mode)' : '(static + Claude AI)'}
115
+
116
+ ---
117
+
118
+ ## Risk Score: ${meta.score}/100 - ${meta.verdict}
119
+
120
+ ${meta.aiSummary || ''}
121
+
122
+ ### Category Breakdown
123
+ | Category | Score |
124
+ |---|---|
125
+ ${Object.entries(meta.breakdown).map(([k, v]) => `| ${k} | ${v}/100 |`).join('\n')}
126
+
127
+ ### Findings Summary
128
+ | Severity | Count |
129
+ |---|---|
130
+ | Critical | ${counts.CRITICAL} |
131
+ | High | ${counts.HIGH} |
132
+ | Medium | ${counts.MEDIUM} |
133
+ | Low | ${counts.LOW} |
134
+
135
+ ---
136
+
137
+ ## Attack Paths (Red-Team Analysis)
138
+
139
+ ${(meta.attackPaths || []).map(p => `### ${p.name} (${p.severity})
140
+ **OWASP:** ${p.owasp.join(', ')} | **MITRE ATT&CK:** ${p.mitre.join(', ')}
141
+
142
+ ${p.steps.map((s, i) => `${i + 1}. ${s}`).join('\n')}`).join('\n\n') || '_No multi-step attack chains identified._'}
143
+
144
+ ---
145
+
146
+ ## Findings
147
+
148
+ ${sorted.map((f, i) => `### ${i + 1}. [${f.severity}] ${f.title}
149
+
150
+ | Field | Value |
151
+ |---|---|
152
+ | OWASP | ${f.owasp || 'n/a'} ${f.owaspTitle ? '- ' + f.owaspTitle : ''} |
153
+ | CWE | ${f.cwe || 'n/a'} ${f.cweName ? '- ' + f.cweName : ''} |
154
+ | MITRE ATT&CK | ${f.mitre || 'n/a'} ${f.mitreName ? '- ' + f.mitreName : ''} |
155
+ | Location | ${f.location || 'n/a'} |
156
+ | Exploitability | ${f.exploitLikelihood ? f.exploitLikelihood + '/5' : 'n/a'} |
157
+ | Attacker cost | ${f.attackerCost || 'n/a'} |
158
+ | Detector | ${f.source === 'static' ? 'Static analysis' : 'Claude AI'} |
159
+
160
+ ${f.description}
161
+
162
+ **Recommended fix:** ${f.fix}
163
+ `).join('\n---\n\n')}
164
+
165
+ ---
166
+
167
+ ## NIST SSDF (SP 800-218) Coverage
168
+
169
+ ${Object.entries(NIST_SSDF).map(([k, v]) => `- **${k}**: ${v}`).join('\n')}
170
+
171
+ ---
172
+
173
+ ## Disclaimer
174
+
175
+ ${DISCLAIMER}
176
+ `;
177
+ fs.writeFileSync(outputPath, md, 'utf8');
178
+ }