@solana-epic/cli 0.1.0-beta.3 โ†’ 0.2.0-beta.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/dist/api.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { CompatibilityReport, DiffReport } from "@solana-epic/diff-engine";
2
+ import { config } from "@solana-epic/parser";
3
+ export interface UpgradeReport {
4
+ programName: string;
5
+ compatibility: CompatibilityReport;
6
+ report: DiffReport;
7
+ intelligence: any;
8
+ epicConfig: config.ResolvedEpicConfig;
9
+ }
10
+ export declare function runCheck(oldPath: string, newPath: string, epicConfig: config.ResolvedEpicConfig): Promise<UpgradeReport>;
11
+ export { formatMarkdown, formatSarif } from "./formatters.js";
12
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,mBAAmB,EACnB,UAAU,EACX,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE7C,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,mBAAmB,CAAC;IACnC,MAAM,EAAE,UAAU,CAAC;IACnB,YAAY,EAAE,GAAG,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC,kBAAkB,CAAC;CACvC;AAED,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAiB9H;AAED,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/api.js ADDED
@@ -0,0 +1,20 @@
1
+ import * as path from "path";
2
+ import { analyzePrograms, compareAccountLayouts, simulateCompatibility, createUpgradeIntelligence } from "@solana-epic/diff-engine";
3
+ export async function runCheck(oldPath, newPath, epicConfig) {
4
+ const resolvedOldPath = path.resolve(oldPath);
5
+ const resolvedNewPath = path.resolve(newPath);
6
+ const { oldProgram, newProgram } = await analyzePrograms(resolvedOldPath, resolvedNewPath, epicConfig);
7
+ const compatibility = simulateCompatibility(oldProgram, newProgram, epicConfig);
8
+ const report = compareAccountLayouts(oldProgram, newProgram, epicConfig);
9
+ const intelligence = createUpgradeIntelligence(report);
10
+ const programName = compatibility.accounts[0]?.account || report.findings[0]?.account || path.basename(resolvedNewPath);
11
+ return {
12
+ programName,
13
+ compatibility,
14
+ report,
15
+ intelligence,
16
+ epicConfig
17
+ };
18
+ }
19
+ export { formatMarkdown, formatSarif } from "./formatters.js";
20
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,yBAAyB,EAG1B,MAAM,0BAA0B,CAAC;AAWlC,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,OAAe,EAAE,OAAe,EAAE,UAAqC;IACpG,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9C,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,GAAG,MAAM,eAAe,CAAC,eAAe,EAAE,eAAe,EAAE,UAAU,CAAC,CAAC;IACvG,MAAM,aAAa,GAAG,qBAAqB,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;IAChF,MAAM,MAAM,GAAG,qBAAqB,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;IACzE,MAAM,YAAY,GAAG,yBAAyB,CAAC,MAAM,CAAC,CAAC;IACvD,MAAM,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;IAExH,OAAO;QACL,WAAW;QACX,aAAa;QACb,MAAM;QACN,YAAY;QACZ,UAAU;KACX,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { UpgradeReport } from "./api.js";
2
+ export declare function formatMarkdown(result: UpgradeReport, configChanged?: boolean): string;
3
+ export declare function formatSarif(result: UpgradeReport): any;
4
+ //# sourceMappingURL=formatters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatters.d.ts","sourceRoot":"","sources":["../src/formatters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,EAAE,aAAa,GAAE,OAAe,GAAG,MAAM,CAoF5F;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,GAAG,GAAG,CAyDtD"}
@@ -0,0 +1,134 @@
1
+ export function formatMarkdown(result, configChanged = false) {
2
+ const { compatibility, report, programName, epicConfig } = result;
3
+ const lines = [];
4
+ const blocked = compatibility.overall === "Blocked";
5
+ const migration = compatibility.overall === "Migration-Required";
6
+ const safe = compatibility.overall === "Compatible";
7
+ if (blocked) {
8
+ lines.push("## ๐Ÿ”ด EPIC Guard: UPGRADE BLOCKED");
9
+ }
10
+ else if (migration) {
11
+ lines.push("## ๐ŸŸก EPIC Guard: MIGRATION REQUIRED");
12
+ }
13
+ else {
14
+ lines.push("## ๐ŸŸข EPIC Guard: APPROVED");
15
+ }
16
+ lines.push("");
17
+ if (configChanged) {
18
+ lines.push("> [!WARNING]");
19
+ lines.push("> **UPGRADE CONFIGURATION GATE MODIFIED**");
20
+ lines.push("> This Pull Request contains changes to `epic.toml` configuration rules.");
21
+ lines.push("> Signers must audit the modifications below to ensure safety limits are not bypassed.");
22
+ lines.push("");
23
+ }
24
+ lines.push(`### Upgrade Compatibility: \`${programName}\``);
25
+ lines.push("");
26
+ if (compatibility.accounts.length === 0) {
27
+ lines.push("No state accounts found. Upgrade is safe.");
28
+ return lines.join("\n");
29
+ }
30
+ for (const acc of compatibility.accounts) {
31
+ const isBlocked = acc.status === "Blocked";
32
+ const isMigration = acc.status === "Migration-Required";
33
+ const icon = isBlocked ? "๐Ÿ”ด" : isMigration ? "๐ŸŸก" : "๐ŸŸข";
34
+ lines.push(`#### ${icon} Struct \`${acc.account}\` (${acc.status})`);
35
+ if (acc.reasons && acc.reasons.length > 0) {
36
+ lines.push("");
37
+ lines.push("**Reasoning:**");
38
+ for (const r of acc.reasons) {
39
+ lines.push(`* ${r}`);
40
+ }
41
+ }
42
+ if (acc.upgradePlan && acc.upgradePlan.length > 0) {
43
+ lines.push("");
44
+ lines.push("**Migration Plan:**");
45
+ if (acc.rentDeltaLamports !== null) {
46
+ lines.push(`* Rent Delta: \`${acc.rentDeltaLamports} lamports\` (\`${acc.sizeDelta} bytes\`)`);
47
+ }
48
+ for (const step of acc.upgradePlan) {
49
+ lines.push(`* ${step}`);
50
+ }
51
+ }
52
+ lines.push("");
53
+ }
54
+ // Overrides section
55
+ const appliedOverrides = report.findings.filter(f => f.severity !== (f.kind === "FIELD_ADDED" ? "MAJOR" : "CRITICAL"));
56
+ if (appliedOverrides.length > 0) {
57
+ lines.push("### ๐Ÿ”‘ Applied Layout Overrides");
58
+ lines.push("");
59
+ lines.push("| Struct | Finding | Field | Severity Shift | Note |");
60
+ lines.push("| :--- | :--- | :--- | :--- | :--- |");
61
+ for (const o of appliedOverrides) {
62
+ const original = o.kind === "FIELD_ADDED" ? "MAJOR" : "CRITICAL";
63
+ // Find note
64
+ let note = "No note provided.";
65
+ for (const [name, program] of epicConfig.programs.entries()) {
66
+ const match = program.overrides.find(override => override.account.toLowerCase() === o.account.toLowerCase() &&
67
+ override.finding.toUpperCase() === o.kind.toUpperCase());
68
+ if (match) {
69
+ note = match.note;
70
+ break;
71
+ }
72
+ }
73
+ lines.push(`| \`${o.account}\` | \`${o.kind}\` | \`${o.field?.name || "global"}\` | \`${original}\` โ”€โ”€โ–บ \`${o.severity}\` | ${note} |`);
74
+ }
75
+ lines.push("");
76
+ }
77
+ return lines.join("\n");
78
+ }
79
+ export function formatSarif(result) {
80
+ // Return a valid SARIF structure based on the findings
81
+ const sarif = {
82
+ version: "2.1.0",
83
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
84
+ runs: [
85
+ {
86
+ tool: {
87
+ driver: {
88
+ name: "EPIC Upgrade Intelligence",
89
+ version: "0.2.0-beta.0",
90
+ informationUri: "https://github.com/solana-epic/epic",
91
+ rules: []
92
+ }
93
+ },
94
+ results: []
95
+ }
96
+ ]
97
+ };
98
+ const run = sarif.runs[0];
99
+ const rules = new Map();
100
+ for (const finding of result.report.findings) {
101
+ const ruleId = `EPIC-LAYOUT-${finding.kind}`;
102
+ if (!rules.has(ruleId)) {
103
+ rules.set(ruleId, {
104
+ id: ruleId,
105
+ shortDescription: { text: `State Layout Drift: ${finding.kind}` },
106
+ helpUri: "https://github.com/solana-epic/epic"
107
+ });
108
+ }
109
+ // Map severity
110
+ let level = "warning";
111
+ if (finding.severity === "CRITICAL")
112
+ level = "error";
113
+ if (finding.severity === "SAFE")
114
+ level = "note";
115
+ run.results.push({
116
+ ruleId,
117
+ level,
118
+ message: {
119
+ text: `Account \`${finding.account}\` changed: ${finding.kind}. Field: ${finding.field?.name || 'N/A'}`
120
+ },
121
+ locations: [
122
+ {
123
+ physicalLocation: {
124
+ artifactLocation: { uri: "epic.toml" },
125
+ region: { startLine: 1 }
126
+ }
127
+ }
128
+ ]
129
+ });
130
+ }
131
+ run.tool.driver.rules = Array.from(rules.values());
132
+ return sarif;
133
+ }
134
+ //# sourceMappingURL=formatters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatters.js","sourceRoot":"","sources":["../src/formatters.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { compareAnchorPrograms, formatHumanReport } from "@solana-epic/diff-engine";
3
+ import { analyzePrograms, compareAccountLayouts, createUpgradeIntelligence, simulateCompatibility } from "@solana-epic/diff-engine";
4
4
  import { config } from "@solana-epic/parser";
5
5
  import { spawnSync, execSync } from "node:child_process";
6
6
  import path from "node:path";
@@ -13,7 +13,42 @@ program
13
13
  .version(CLI_VERSION)
14
14
  .option("--no-banner", "Disable the startup banner");
15
15
  import { resolveParserBinary } from "./loader.js";
16
- import { printBanner, printInitSequence, printSection, printRuleFinding, colors, printEndSummary, DIVIDER, ruleKnowledge } from "./ui.js";
16
+ import { printStartup, getBannerString, printInitSequence, printSection, printRuleFinding, colors, printEndSummary, printUpgradeReport, printCompatibilityReport, severityBadge, scoreBar, bandForScore, DIVIDER, ruleKnowledge } from "./ui.js";
17
+ import { generateSarif, generateMarkdown } from "./reports.js";
18
+ // Count Rust source files under a path (real, not fabricated metrics).
19
+ function countRustFiles(target) {
20
+ let count = 0;
21
+ const skip = new Set([".git", "target", "node_modules", "vendor"]);
22
+ const walk = (dir) => {
23
+ let entries;
24
+ try {
25
+ entries = fs.readdirSync(dir, { withFileTypes: true });
26
+ }
27
+ catch {
28
+ return;
29
+ }
30
+ for (const entry of entries) {
31
+ if (entry.isDirectory()) {
32
+ if (skip.has(entry.name))
33
+ continue;
34
+ walk(path.join(dir, entry.name));
35
+ }
36
+ else if (entry.isFile() && entry.name.endsWith(".rs")) {
37
+ count++;
38
+ }
39
+ }
40
+ };
41
+ try {
42
+ const stat = fs.statSync(target);
43
+ if (stat.isFile())
44
+ return target.endsWith(".rs") ? 1 : 0;
45
+ }
46
+ catch {
47
+ return 0;
48
+ }
49
+ walk(target);
50
+ return count;
51
+ }
17
52
  function findRustBinary() {
18
53
  try {
19
54
  return resolveParserBinary();
@@ -31,7 +66,7 @@ program
31
66
  const startTime = Date.now();
32
67
  try {
33
68
  const opts = program.opts();
34
- printBanner(!opts.banner);
69
+ printStartup("Workspace Analysis", !opts.banner);
35
70
  printInitSequence([
36
71
  "Rust AST Loaded",
37
72
  "Parsing Anchor Workspace",
@@ -87,19 +122,23 @@ program
87
122
  .command("check")
88
123
  .description("Compare two Solana program workspace versions and report upgrade readiness.")
89
124
  .option("-c, --config <path>", "Path to epic.toml configuration file")
125
+ .option("-f, --format <format>", "Output format: text, json", "text")
90
126
  .argument("<old_path>", "Path to the old program version source directory")
91
127
  .argument("<new_path>", "Path to the new program version source directory")
92
128
  .action(async (oldPath, newPath, options) => {
93
129
  const startTime = Date.now();
130
+ const isJson = options.format === "json";
94
131
  try {
95
132
  const opts = program.opts();
96
- printBanner(!opts.banner);
97
- printInitSequence([
98
- "Rust AST Loaded",
99
- "Parsing Anchor Workspace",
100
- "Building Call Graph"
101
- ]);
102
- console.log("");
133
+ const startupShown = isJson ? false : printStartup("Upgrade Intelligence", !opts.banner);
134
+ if (!isJson) {
135
+ printInitSequence([
136
+ "Rust AST Loaded",
137
+ "Parsing Anchor Workspace",
138
+ "Building Call Graph"
139
+ ]);
140
+ console.log("");
141
+ }
103
142
  const resolvedOldPath = path.resolve(oldPath);
104
143
  const resolvedNewPath = path.resolve(newPath);
105
144
  let epicConfig;
@@ -110,28 +149,59 @@ program
110
149
  console.error(`epic.toml validation error: ${err.message}`);
111
150
  process.exit(1);
112
151
  }
113
- const report = await compareAnchorPrograms(resolvedOldPath, resolvedNewPath, epicConfig);
114
- console.log(formatHumanReport(report));
152
+ // Parse both versions once, then run BOTH the compatibility simulator
153
+ // (state survival) and the existing layout-diff findings off the same AST.
154
+ const { oldProgram, newProgram } = await analyzePrograms(resolvedOldPath, resolvedNewPath, epicConfig);
155
+ const compatibility = simulateCompatibility(oldProgram, newProgram, epicConfig);
156
+ const report = compareAccountLayouts(oldProgram, newProgram, epicConfig);
157
+ const intelligence = createUpgradeIntelligence(report);
158
+ const programName = compatibility.accounts[0]?.account || report.findings[0]?.account || path.basename(resolvedNewPath);
159
+ if (isJson) {
160
+ console.log(JSON.stringify({
161
+ program: programName,
162
+ compatibility,
163
+ findings: report.findings,
164
+ severity: report.severity
165
+ }, null, 2));
166
+ process.exit(compatibility.overall === "Blocked" ? 1 : 0);
167
+ }
168
+ // Lead with the compatibility verdict (the product), then keep the
169
+ // detailed layout findings below it as supporting evidence.
170
+ printCompatibilityReport(compatibility, { program: programName }, { skipTitle: startupShown });
171
+ if (report.findings.length) {
172
+ console.log(colors.gray(DIVIDER));
173
+ console.log(colors.white(colors.bold("LAYOUT FINDINGS (DETAIL)")));
174
+ console.log(colors.gray(DIVIDER));
175
+ console.log("");
176
+ printUpgradeReport(report, intelligence, { program: programName }, { skipTitle: true });
177
+ console.log("");
178
+ }
179
+ // Exit code. BLOCKED always fails CI (state corruption is non-negotiable),
180
+ // overriding fail_on_severity. Other outcomes respect the configured threshold.
115
181
  const severityOrder = ["SAFE", "MINOR", "WARNING", "MAJOR", "CRITICAL"];
116
182
  const thresholdIndex = severityOrder.indexOf(epicConfig.failOnSeverity);
117
183
  const reportSeverityIndex = severityOrder.indexOf(report.severity);
184
+ const severityFails = thresholdIndex !== -1 && reportSeverityIndex !== -1 && reportSeverityIndex >= thresholdIndex;
185
+ const blocked = compatibility.overall === "Blocked";
186
+ const fails = blocked || severityFails;
118
187
  console.log(colors.gray(DIVIDER));
119
188
  console.log("");
120
- if (thresholdIndex !== -1 && reportSeverityIndex !== -1 && reportSeverityIndex >= thresholdIndex) {
189
+ if (blocked) {
190
+ console.log(colors.critical(`โœ– EPIC Guard Blocked: deploying would corrupt existing on-chain accounts.`));
191
+ }
192
+ else if (severityFails) {
121
193
  console.log(colors.critical(`โœ– EPIC Guard Blocked: Upgrade severity is ${report.severity} (threshold: ${epicConfig.failOnSeverity}).`));
122
194
  }
195
+ else if (compatibility.overall === "Migration-Required") {
196
+ console.log(colors.warning(`โ–ฒ EPIC Guard: Upgrade is safe only after the migration above is performed.`));
197
+ }
123
198
  else {
124
199
  console.log(colors.success(`โœ“ EPIC Guard Approved Upgrade.`));
125
200
  }
126
201
  console.log("");
127
202
  console.log(colors.dim(`Time: ${(Date.now() - startTime) / 1000} s`));
128
203
  console.log("");
129
- if (thresholdIndex !== -1 && reportSeverityIndex !== -1 && reportSeverityIndex >= thresholdIndex) {
130
- process.exit(1);
131
- }
132
- else {
133
- process.exit(0);
134
- }
204
+ process.exit(fails ? 1 : 0);
135
205
  }
136
206
  catch (error) {
137
207
  const message = error instanceof Error ? error.message : String(error);
@@ -151,26 +221,18 @@ function getSeverityLevel(sev) {
151
221
  return 3;
152
222
  return 3;
153
223
  }
154
- function generateSarif(findings) {
155
- const results = findings.map((f) => ({
156
- ruleId: f.rule_id,
157
- level: "warning",
158
- message: { text: f.message },
159
- locations: [{ physicalLocation: { artifactLocation: { uri: f.location.file }, region: { startLine: f.location.line } } }]
160
- }));
161
- return {
162
- version: "2.1.0",
163
- runs: [{ tool: { driver: { name: "EPIC", rules: [] } }, results }]
164
- };
165
- }
166
224
  program
167
225
  .command("doctor")
168
226
  .description("Run diagnostics on the environment")
169
227
  .action(() => {
170
- console.log(colors.gray(DIVIDER));
171
- console.log(colors.bold(colors.white("Environment Diagnostics")));
172
- console.log(colors.gray(DIVIDER));
173
- console.log("");
228
+ const opts = program.opts();
229
+ const startupShown = printStartup("Environment Diagnostics", !opts.banner);
230
+ if (!startupShown) {
231
+ console.log(colors.gray(DIVIDER));
232
+ console.log(colors.bold(colors.white("Environment Diagnostics")));
233
+ console.log(colors.gray(DIVIDER));
234
+ console.log("");
235
+ }
174
236
  let hasErrors = false;
175
237
  const checkVersion = (cmd, name, required) => {
176
238
  try {
@@ -244,28 +306,35 @@ program
244
306
  .command("explain <rule_id>")
245
307
  .description("Explain a security rule in detail")
246
308
  .action((ruleId) => {
309
+ const opts = program.opts();
310
+ printStartup("Rule Explanation", !opts.banner);
247
311
  const knowledge = ruleKnowledge[ruleId];
248
312
  if (!knowledge) {
249
313
  console.log(colors.critical(`Rule ${ruleId} not found.`));
314
+ console.log(colors.dim("Run 'epic rules' to list all available rules."));
250
315
  process.exit(1);
251
316
  }
317
+ const band = bandForScore(knowledge.score);
252
318
  console.log(colors.gray(DIVIDER));
253
- console.log(colors.bold(colors.white("Rule")));
254
- console.log(colors.cyan(knowledge.desc));
255
319
  console.log("");
256
- console.log(colors.bold(colors.white("Severity")));
257
- console.log(colors.critical("Critical / High"));
258
- console.log(colors.gray(DIVIDER));
320
+ console.log(`${severityBadge(band)} ${colors.white(ruleId)} ${colors.gray("ยท")} ${colors.white(knowledge.desc)}`);
259
321
  console.log("");
260
- console.log(colors.bold(colors.white("Historical Exploits")));
261
- console.log(colors.dim(knowledge.historical));
322
+ console.log(`${colors.dim("Risk Score")} ${scoreBar(knowledge.score)} ${colors.white(`${knowledge.score} / 100`)}`);
262
323
  console.log("");
263
- console.log(colors.bold(colors.white("Suggested Fix")));
264
- console.log(colors.dim(knowledge.fix));
324
+ console.log(colors.gray(DIVIDER));
265
325
  console.log("");
266
- console.log(colors.bold(colors.white("Why this matters")));
326
+ console.log(colors.bold(colors.white("WHY IT'S DANGEROUS")));
267
327
  console.log(colors.dim(knowledge.why));
268
328
  console.log("");
329
+ console.log(colors.bold(colors.white("WHAT BREAKS")));
330
+ console.log(colors.warning(knowledge.impact));
331
+ console.log("");
332
+ console.log(colors.bold(colors.white("HOW TO FIX")));
333
+ console.log(colors.green(knowledge.fix));
334
+ console.log("");
335
+ console.log(colors.bold(colors.white("HISTORICAL EXPLOITS")));
336
+ console.log(colors.dim(knowledge.historical));
337
+ console.log("");
269
338
  console.log(colors.gray(DIVIDER));
270
339
  console.log("");
271
340
  });
@@ -285,10 +354,12 @@ program
285
354
  try {
286
355
  const opts = program.opts();
287
356
  if (options.format === "text")
288
- printBanner(!opts.banner);
357
+ printStartup("Security Audit", !opts.banner);
289
358
  const binary = findRustBinary();
290
359
  const resolvedPath = path.resolve(targetPath);
360
+ const auditStart = Date.now();
291
361
  const result = spawnSync(binary, ["audit", resolvedPath], { encoding: "utf-8" });
362
+ const ruleEngineMs = Date.now() - auditStart;
292
363
  if (result.status !== 0)
293
364
  throw new Error("Parser failed");
294
365
  const findings = JSON.parse(result.stdout.trim());
@@ -308,7 +379,24 @@ program
308
379
  return !builtinIgnore.some(p => relPath.includes(`/${p}/`) || relPath.startsWith(`${p}/`) || relPath === p);
309
380
  });
310
381
  if (options.format === "text") {
311
- const fileCount = activeFindings.length > 0 ? 182 : 45;
382
+ // Real repository structure from the parser's analyze pass + filesystem.
383
+ const fileCount = countRustFiles(resolvedPath);
384
+ let structsFound = 0, enumsFound = 0, accountsFound = 0;
385
+ let analyzeMs = 0;
386
+ try {
387
+ const analyzeStart = Date.now();
388
+ const analyzeResult = spawnSync(binary, [resolvedPath], { encoding: "utf-8" });
389
+ analyzeMs = Date.now() - analyzeStart;
390
+ if (analyzeResult.status === 0 && analyzeResult.stdout) {
391
+ const overview = JSON.parse(analyzeResult.stdout.trim());
392
+ structsFound = overview.structs_found ?? 0;
393
+ enumsFound = overview.enums_found ?? 0;
394
+ accountsFound = Array.isArray(overview.accounts) ? overview.accounts.length : 0;
395
+ }
396
+ }
397
+ catch {
398
+ // Analyze enrichment is best-effort; the audit result is authoritative.
399
+ }
312
400
  const totalTimeMs = Date.now() - startTime;
313
401
  printInitSequence([
314
402
  `Scanning Files\n${colors.cyan("โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ")} ${colors.dim(`${fileCount} / ${fileCount}`)}`,
@@ -319,28 +407,22 @@ program
319
407
  const projName = path.basename(resolvedPath) || ".";
320
408
  printSection("Workspace", {
321
409
  "Project": projName,
322
- "Rust Version": "1.88.0",
323
- "Anchor": "0.31",
324
- "Rules Loaded": 5,
410
+ "Rules Loaded": Object.keys(ruleKnowledge).length,
325
411
  "Configuration": options.config || "epic.toml"
326
412
  });
327
413
  printSection("Repository Overview", {
328
414
  "Rust Files": fileCount,
329
- "Instructions": Math.round(fileCount * 0.35),
330
- "Accounts": Math.round(fileCount * 0.95),
331
- "CPIs": Math.round(fileCount * 0.28),
332
- "PDAs": Math.round(fileCount * 0.22),
333
- "Anchor Programs": 1
415
+ "Structs": structsFound,
416
+ "Enums": enumsFound,
417
+ "State Accounts": accountsFound
334
418
  });
335
419
  const criticalCount = activeFindings.filter((f) => getSeverityLevel(f.severity) === 3).length;
336
420
  const warningCount = activeFindings.filter((f) => getSeverityLevel(f.severity) < 3).length;
337
421
  const rulesTriggered = new Set(activeFindings.map((f) => f.rule_id)).size;
338
422
  printSection("Execution Metrics", {
339
423
  "Indexed Files": fileCount,
340
- "AST Build": `${Math.max(1, Math.round(totalTimeMs * 0.45))} ms`,
341
- "Call Graph": `${Math.max(1, Math.round(totalTimeMs * 0.15))} ms`,
342
- "Rule Engine": `${Math.max(1, Math.round(totalTimeMs * 0.35))} ms`,
343
- "Rendering": `${Math.max(1, Math.round(totalTimeMs * 0.05))} ms`,
424
+ "Parse + AST": `${Math.max(1, analyzeMs)} ms`,
425
+ "Rule Engine": `${Math.max(1, ruleEngineMs)} ms`,
344
426
  "Total": `${(totalTimeMs / 1000).toFixed(2)} s`
345
427
  });
346
428
  printSection("Security Summary", {
@@ -387,13 +469,10 @@ program
387
469
  const knowledge = ruleKnowledge[mostCommonRule];
388
470
  console.log(colors.bold(colors.white("Most Common Issue")));
389
471
  console.log(colors.dim(`${knowledge.desc}`));
390
- console.log(colors.cyan(`${highestOccurrences} occurrences`));
391
- console.log("");
392
- console.log(colors.dim("Estimated Fix Time"));
393
- console.log(colors.white("~25-40 minutes"));
472
+ console.log(colors.cyan(`${highestOccurrences} occurrence${highestOccurrences === 1 ? "" : "s"}`));
394
473
  console.log("");
395
474
  console.log(colors.dim("Priority"));
396
- console.log(colors.white(`Resolve this rule before investigating other issues.`));
475
+ console.log(colors.white(`Resolve this rule first โ€” it accounts for the most findings.`));
397
476
  console.log("");
398
477
  }
399
478
  printEndSummary(projName, 5, criticalCount, warningCount, Date.now() - startTime);
@@ -402,18 +481,13 @@ program
402
481
  console.log(JSON.stringify(activeFindings, null, 2));
403
482
  }
404
483
  else if (options.format === "sarif") {
405
- // Implement SARIF if needed
484
+ console.log(JSON.stringify(generateSarif(activeFindings), null, 2));
406
485
  }
407
486
  else if (options.format === "markdown") {
408
- console.log("# EPIC Security Report");
409
- console.log(`Critical: ${activeFindings.filter((f) => getSeverityLevel(f.severity) === 3).length}`);
410
- console.log(`High: ${activeFindings.filter((f) => getSeverityLevel(f.severity) < 3).length}`);
411
- console.log("\n## Findings\n");
412
- for (const finding of activeFindings) {
413
- console.log(`### ${finding.rule_id}: ${finding.rule_name || finding.rule_id}`);
414
- console.log(`**Location:** \`${finding.location.file}:${finding.location.line}\``);
415
- console.log(`**Message:** ${finding.message}\n`);
416
- }
487
+ console.log(generateMarkdown(activeFindings, {
488
+ project: path.basename(resolvedPath) || ".",
489
+ scanMs: Date.now() - startTime
490
+ }));
417
491
  }
418
492
  if (options.strict) {
419
493
  const threshold = epicConfig.failOnSeverity || "CRITICAL";
@@ -447,34 +521,42 @@ program
447
521
  .command("rules")
448
522
  .description("List all available security rules.")
449
523
  .action(() => {
450
- console.log("EPIC-SEC-001");
451
- console.log("Owner Validation");
452
- console.log("Critical");
453
- console.log("Implemented\n");
454
- console.log("EPIC-SEC-002");
455
- console.log("Missing Signer Validation");
456
- console.log("Critical");
457
- console.log("Implemented\n");
458
- console.log("EPIC-SEC-003");
459
- console.log("Missing Post-CPI Account Reload");
460
- console.log("Critical");
461
- console.log("Implemented\n");
462
- console.log("EPIC-SEC-004");
463
- console.log("PDA Cryptographic Seed Collision Risk");
464
- console.log("High");
465
- console.log("Implemented\n");
466
- console.log("EPIC-SEC-005");
467
- console.log("Arbitrary CPI Target Program Spoofing");
468
- console.log("Critical");
469
- console.log("Implemented");
524
+ const opts = program.opts();
525
+ const startupShown = printStartup("Security Rules", !opts.banner);
526
+ const rules = [
527
+ ["EPIC-SEC-001", "Owner Validation"],
528
+ ["EPIC-SEC-002", "Missing Signer Validation"],
529
+ ["EPIC-SEC-003", "Missing Post-CPI Account Reload"],
530
+ ["EPIC-SEC-004", "PDA Cryptographic Seed Collision Risk"],
531
+ ["EPIC-SEC-005", "Arbitrary CPI Target Program Spoofing"]
532
+ ];
533
+ if (!startupShown) {
534
+ console.log(colors.gray(DIVIDER));
535
+ console.log(colors.white(colors.bold("EPIC Security Rules")));
536
+ console.log(colors.gray(DIVIDER));
537
+ console.log("");
538
+ }
539
+ for (const [id, name] of rules) {
540
+ const kb = ruleKnowledge[id];
541
+ const score = kb ? kb.score : 50;
542
+ const band = bandForScore(score);
543
+ console.log(`${severityBadge(band)} ${colors.white(id)} ${colors.gray("ยท")} ${colors.white(name)}`);
544
+ if (kb) {
545
+ console.log(` ${scoreBar(score, 16)} ${colors.dim(`${score} / 100`)} ${colors.gray("Implemented")}`);
546
+ }
547
+ else {
548
+ console.log(` ${colors.gray("Implemented")}`);
549
+ }
550
+ console.log("");
551
+ }
552
+ console.log(colors.dim("Run 'epic explain <RULE-ID>' for a full breakdown of any rule."));
553
+ console.log("");
470
554
  });
471
555
  program.configureHelp({
472
556
  formatHelp: (cmd, helper) => {
473
- return `
474
- ${colors.bold(colors.white("EPIC"))}
475
- ${colors.dim("Security-first upgrade intelligence for Solana")}
476
- ${colors.cyan("v" + CLI_VERSION)}
477
-
557
+ const noBannerFlag = !!cmd.opts().noBanner || process.argv.includes("--no-banner");
558
+ const header = getBannerString(noBannerFlag);
559
+ return `${header}
478
560
  ${colors.bold("Commands")}
479
561
  ${colors.white("audit".padEnd(14))} Run security rules against the repository.
480
562
  ${colors.white("doctor".padEnd(14))} Run diagnostics on the environment.