chainlink-audit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chainlink-integration-audit-kit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # chainlink-audit
2
+
3
+ Security CLI for detecting potential Chainlink integration risks in Solidity repositories.
4
+
5
+ ```bash
6
+ npm install -g chainlink-audit
7
+ chainlink-audit init
8
+ chainlink-audit scan .
9
+ chainlink-audit scan . --format markdown --out chainlink-report.md
10
+ chainlink-audit scan . --format html --out chainlink-report.html
11
+ ```
12
+
13
+ Findings are heuristic leads for manual review, not confirmed vulnerabilities.
14
+
15
+ See the repository README for full documentation:
16
+ https://github.com/alva-p/chainlink-integration-audit-kit
package/dist/config.js ADDED
@@ -0,0 +1,77 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ export const configFileName = ".chainlink-audit.json";
4
+ export const defaultConfig = {
5
+ exclude: ["test/", "tests/", "mock/", "mocks/", "script/", "lib/"],
6
+ format: "text",
7
+ minSeverity: "low",
8
+ };
9
+ export function severityRank(severity) {
10
+ return {
11
+ info: 0,
12
+ low: 1,
13
+ medium: 2,
14
+ high: 3,
15
+ }[severity];
16
+ }
17
+ export function parseOutputFormat(value) {
18
+ if (value === "text" || value === "json" || value === "markdown" || value === "html")
19
+ return value;
20
+ throw new Error(`Unsupported format "${value}". Use text, json, markdown, or html.`);
21
+ }
22
+ export function parseSeverity(value) {
23
+ if (value === "info" || value === "low" || value === "medium" || value === "high")
24
+ return value;
25
+ throw new Error(`Unsupported severity "${value}". Use info, low, medium, or high.`);
26
+ }
27
+ function isRecord(value) {
28
+ return typeof value === "object" && value !== null && !Array.isArray(value);
29
+ }
30
+ export function normalizeConfig(raw) {
31
+ if (!isRecord(raw))
32
+ return { ...defaultConfig };
33
+ const exclude = Array.isArray(raw.exclude)
34
+ ? raw.exclude.filter((entry) => typeof entry === "string")
35
+ : defaultConfig.exclude;
36
+ const format = typeof raw.format === "string" ? parseOutputFormat(raw.format) : defaultConfig.format;
37
+ const minSeverity = typeof raw.minSeverity === "string" ? parseSeverity(raw.minSeverity) : defaultConfig.minSeverity;
38
+ return {
39
+ exclude,
40
+ format,
41
+ minSeverity,
42
+ };
43
+ }
44
+ export async function loadConfig(startPath = process.cwd()) {
45
+ const absoluteStart = path.resolve(startPath);
46
+ let current = absoluteStart;
47
+ try {
48
+ const stat = await fs.stat(current);
49
+ if (stat.isFile())
50
+ current = path.dirname(current);
51
+ }
52
+ catch {
53
+ current = process.cwd();
54
+ }
55
+ while (true) {
56
+ const candidate = path.join(current, configFileName);
57
+ try {
58
+ const raw = await fs.readFile(candidate, "utf8");
59
+ return normalizeConfig(JSON.parse(raw));
60
+ }
61
+ catch (error) {
62
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
63
+ if (code !== "ENOENT") {
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ throw new Error(`Failed to read ${candidate}: ${message}`);
66
+ }
67
+ }
68
+ const parent = path.dirname(current);
69
+ if (parent === current)
70
+ return { ...defaultConfig };
71
+ current = parent;
72
+ }
73
+ }
74
+ export async function writeDefaultConfig(targetPath = configFileName) {
75
+ const output = `${JSON.stringify(defaultConfig, null, 2)}\n`;
76
+ await fs.writeFile(targetPath, output, { encoding: "utf8", flag: "wx" });
77
+ }
package/dist/index.js ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import { promises as fs } from "node:fs";
3
+ import { Command } from "commander";
4
+ import { configFileName, loadConfig, parseOutputFormat, parseSeverity, writeDefaultConfig, } from "./config.js";
5
+ import { renderJson } from "./reporters/json.js";
6
+ import { renderHtml } from "./reporters/html.js";
7
+ import { renderMarkdown } from "./reporters/markdown.js";
8
+ import { renderText } from "./reporters/text.js";
9
+ import { rules } from "./rules/index.js";
10
+ import { scanPath } from "./scanner.js";
11
+ const version = "0.1.0";
12
+ function render(format, result) {
13
+ if (format === "json")
14
+ return renderJson(result);
15
+ if (format === "markdown")
16
+ return renderMarkdown(result);
17
+ if (format === "html")
18
+ return renderHtml(result);
19
+ return renderText(result);
20
+ }
21
+ const program = new Command();
22
+ program
23
+ .name("chainlink-audit")
24
+ .description("CLI-first scanner for potential Chainlink integration risks in Solidity repositories.")
25
+ .version(version);
26
+ program
27
+ .command("scan")
28
+ .argument("<path>", "Solidity file or repository path to scan")
29
+ .option("--format <format>", "Output format: text, json, markdown, html")
30
+ .option("--min-severity <severity>", "Minimum severity: info, low, medium, high")
31
+ .option("--out <file>", "Write report to a file instead of stdout")
32
+ .action(async (targetPath, options) => {
33
+ try {
34
+ const config = await loadConfig(targetPath);
35
+ if (options.format)
36
+ config.format = parseOutputFormat(options.format);
37
+ if (options.minSeverity)
38
+ config.minSeverity = parseSeverity(options.minSeverity);
39
+ const format = config.format;
40
+ const result = await scanPath(targetPath, { config });
41
+ const output = render(format, result);
42
+ if (options.out) {
43
+ await fs.writeFile(options.out, `${output}\n`, "utf8");
44
+ }
45
+ else {
46
+ console.log(output);
47
+ }
48
+ process.exitCode = 0;
49
+ }
50
+ catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ console.error(`chainlink-audit: ${message}`);
53
+ process.exitCode = 2;
54
+ }
55
+ });
56
+ program.command("init").description(`Create ${configFileName} with recommended defaults`).action(async () => {
57
+ try {
58
+ await writeDefaultConfig();
59
+ console.log(`Created ${configFileName}`);
60
+ }
61
+ catch (error) {
62
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
63
+ if (code === "EEXIST") {
64
+ console.error(`chainlink-audit: ${configFileName} already exists`);
65
+ process.exitCode = 1;
66
+ return;
67
+ }
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ console.error(`chainlink-audit: ${message}`);
70
+ process.exitCode = 2;
71
+ }
72
+ });
73
+ program.command("rules").description("List available MVP rules").action(() => {
74
+ for (const rule of rules) {
75
+ const metadata = rule.metadata;
76
+ console.log(`${metadata.ruleId} [${metadata.severity}] ${metadata.product} - ${metadata.title}`);
77
+ }
78
+ });
79
+ program.command("version").description("Print CLI version").action(() => {
80
+ console.log(version);
81
+ });
82
+ program.parseAsync();
@@ -0,0 +1,372 @@
1
+ function escapeHtml(value) {
2
+ return value
3
+ .replaceAll("&", "&amp;")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;")
6
+ .replaceAll('"', "&quot;")
7
+ .replaceAll("'", "&#39;");
8
+ }
9
+ function severityClass(severity) {
10
+ return `severity-${severity}`;
11
+ }
12
+ function findingCard(finding) {
13
+ return `
14
+ <article class="finding ${severityClass(finding.severity)}">
15
+ <header class="finding-header">
16
+ <div>
17
+ <p class="rule-id">${escapeHtml(finding.ruleId)}</p>
18
+ <h3>${escapeHtml(finding.title)}</h3>
19
+ </div>
20
+ <div class="badges">
21
+ <span class="badge severity">${escapeHtml(finding.severity)}</span>
22
+ <span class="badge confidence">${escapeHtml(finding.confidence)} confidence</span>
23
+ </div>
24
+ </header>
25
+ <dl class="meta">
26
+ <div><dt>Location</dt><dd>${escapeHtml(finding.file)}:${finding.line}</dd></div>
27
+ <div><dt>Manual Review</dt><dd>${finding.manualReviewRequired ? "Required" : "Optional"}</dd></div>
28
+ </dl>
29
+ <section>
30
+ <h4>Description</h4>
31
+ <p>${escapeHtml(finding.description)}</p>
32
+ </section>
33
+ <section>
34
+ <h4>Risk</h4>
35
+ <p>${escapeHtml(finding.risk)}</p>
36
+ </section>
37
+ <section>
38
+ <h4>Recommendation</h4>
39
+ <p>${escapeHtml(finding.recommendation)}</p>
40
+ </section>
41
+ </article>`;
42
+ }
43
+ export function renderHtml(result) {
44
+ const products = result.products.length > 0 ? result.products.join(", ") : "none detected";
45
+ const excludedPaths = result.config.exclude.length > 0 ? result.config.exclude.join(", ") : "none";
46
+ const generatedAt = new Date().toISOString();
47
+ const findings = result.findings.length > 0
48
+ ? result.findings.map(findingCard).join("\n")
49
+ : '<section class="empty">No potential Chainlink integration issues found by MVP rules. Manual review still required.</section>';
50
+ return `<!doctype html>
51
+ <html lang="en">
52
+ <head>
53
+ <meta charset="utf-8">
54
+ <meta name="viewport" content="width=device-width, initial-scale=1">
55
+ <title>Chainlink Integration Audit Report</title>
56
+ <script>
57
+ (function () {
58
+ var storedTheme = localStorage.getItem("chainlink-audit-theme");
59
+ var systemDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
60
+ document.documentElement.dataset.theme = storedTheme || (systemDark ? "dark" : "light");
61
+ })();
62
+ </script>
63
+ <style>
64
+ :root {
65
+ color-scheme: light dark;
66
+ --bg: #f7f9fc;
67
+ --panel: #ffffff;
68
+ --panel-strong: #f8fafc;
69
+ --text: #111827;
70
+ --muted: #5b6472;
71
+ --border: #d8dee9;
72
+ --high: #b42318;
73
+ --medium: #b54708;
74
+ --low: #175cd3;
75
+ --info: #475467;
76
+ --accent: #0f5fff;
77
+ --accent-soft: #eef4ff;
78
+ --accent-border: #c7d7fe;
79
+ --accent-text: #253b74;
80
+ --shadow: 0 1px 2px rgb(16 24 40 / 4%);
81
+ }
82
+ :root[data-theme="dark"] {
83
+ --bg: #0f1115;
84
+ --panel: #171a21;
85
+ --panel-strong: #20242d;
86
+ --text: #eef2f7;
87
+ --muted: #a7b0bf;
88
+ --border: #303642;
89
+ --high: #ff8a80;
90
+ --medium: #ffbf66;
91
+ --low: #70b8ff;
92
+ --info: #c4cad4;
93
+ --accent: #72a7ff;
94
+ --accent-soft: #172033;
95
+ --accent-border: #2f4975;
96
+ --accent-text: #c9d8ff;
97
+ --shadow: 0 14px 32px rgb(0 0 0 / 22%);
98
+ }
99
+ @media (prefers-color-scheme: dark) {
100
+ :root:not([data-theme="light"]) {
101
+ --bg: #0f1115;
102
+ --panel: #171a21;
103
+ --panel-strong: #20242d;
104
+ --text: #eef2f7;
105
+ --muted: #a7b0bf;
106
+ --border: #303642;
107
+ --high: #ff8a80;
108
+ --medium: #ffbf66;
109
+ --low: #70b8ff;
110
+ --info: #c4cad4;
111
+ --accent: #72a7ff;
112
+ --accent-soft: #172033;
113
+ --accent-border: #2f4975;
114
+ --accent-text: #c9d8ff;
115
+ --shadow: 0 14px 32px rgb(0 0 0 / 22%);
116
+ }
117
+ }
118
+ * { box-sizing: border-box; }
119
+ body {
120
+ margin: 0;
121
+ background: var(--bg);
122
+ color: var(--text);
123
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
124
+ line-height: 1.5;
125
+ }
126
+ .topbar {
127
+ align-items: center;
128
+ display: flex;
129
+ gap: 16px;
130
+ justify-content: space-between;
131
+ margin-bottom: 22px;
132
+ }
133
+ .brand {
134
+ color: var(--muted);
135
+ font-size: 13px;
136
+ font-weight: 700;
137
+ text-transform: uppercase;
138
+ }
139
+ .theme-toggle {
140
+ appearance: none;
141
+ background: var(--panel);
142
+ border: 1px solid var(--border);
143
+ border-radius: 999px;
144
+ color: var(--text);
145
+ cursor: pointer;
146
+ font: inherit;
147
+ font-size: 13px;
148
+ font-weight: 700;
149
+ padding: 8px 12px;
150
+ transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
151
+ }
152
+ .theme-toggle:hover {
153
+ border-color: var(--accent);
154
+ color: var(--accent);
155
+ }
156
+ .theme-toggle:focus-visible {
157
+ outline: 3px solid var(--accent-border);
158
+ outline-offset: 2px;
159
+ }
160
+ main {
161
+ width: min(1120px, calc(100% - 32px));
162
+ margin: 0 auto;
163
+ padding: 40px 0 56px;
164
+ }
165
+ .hero {
166
+ padding-bottom: 24px;
167
+ border-bottom: 1px solid var(--border);
168
+ margin-bottom: 24px;
169
+ }
170
+ .eyebrow {
171
+ color: var(--accent);
172
+ font-size: 13px;
173
+ font-weight: 700;
174
+ letter-spacing: 0;
175
+ margin: 0 0 8px;
176
+ text-transform: uppercase;
177
+ }
178
+ h1 {
179
+ font-size: 34px;
180
+ line-height: 1.15;
181
+ margin: 0 0 12px;
182
+ }
183
+ .subtitle {
184
+ max-width: 800px;
185
+ color: var(--muted);
186
+ margin: 0;
187
+ font-size: 16px;
188
+ }
189
+ .summary {
190
+ display: grid;
191
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
192
+ gap: 12px;
193
+ margin: 24px 0;
194
+ }
195
+ .summary-card, .finding, .empty {
196
+ background: var(--panel);
197
+ border: 1px solid var(--border);
198
+ border-radius: 8px;
199
+ box-shadow: var(--shadow);
200
+ }
201
+ .summary-card {
202
+ padding: 16px;
203
+ min-height: 96px;
204
+ }
205
+ .summary-card span {
206
+ color: var(--muted);
207
+ display: block;
208
+ font-size: 13px;
209
+ margin-bottom: 6px;
210
+ }
211
+ .summary-card strong {
212
+ display: block;
213
+ font-size: 18px;
214
+ overflow-wrap: anywhere;
215
+ }
216
+ .note {
217
+ background: var(--accent-soft);
218
+ border: 1px solid var(--accent-border);
219
+ border-radius: 8px;
220
+ color: var(--accent-text);
221
+ margin: 0 0 24px;
222
+ padding: 14px 16px;
223
+ }
224
+ .findings {
225
+ display: grid;
226
+ gap: 16px;
227
+ }
228
+ .finding {
229
+ border-left: 5px solid var(--info);
230
+ padding: 18px;
231
+ }
232
+ .finding.severity-high { border-left-color: var(--high); }
233
+ .finding.severity-medium { border-left-color: var(--medium); }
234
+ .finding.severity-low { border-left-color: var(--low); }
235
+ .finding-header {
236
+ align-items: flex-start;
237
+ display: flex;
238
+ gap: 16px;
239
+ justify-content: space-between;
240
+ margin-bottom: 12px;
241
+ }
242
+ .rule-id {
243
+ color: var(--muted);
244
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
245
+ font-size: 13px;
246
+ font-weight: 700;
247
+ margin: 0 0 4px;
248
+ }
249
+ h2 { font-size: 22px; margin: 28px 0 14px; }
250
+ h3 { font-size: 18px; line-height: 1.3; margin: 0; }
251
+ h4 { font-size: 13px; margin: 16px 0 4px; text-transform: uppercase; }
252
+ p { margin: 0; }
253
+ .badges {
254
+ display: flex;
255
+ flex-wrap: wrap;
256
+ gap: 8px;
257
+ justify-content: flex-end;
258
+ }
259
+ .badge {
260
+ border-radius: 999px;
261
+ border: 1px solid var(--border);
262
+ color: var(--muted);
263
+ font-size: 12px;
264
+ font-weight: 700;
265
+ padding: 4px 9px;
266
+ text-transform: uppercase;
267
+ white-space: nowrap;
268
+ }
269
+ .severity-high .badge.severity { border-color: #fecdca; color: var(--high); }
270
+ .severity-medium .badge.severity { border-color: #fedf89; color: var(--medium); }
271
+ .severity-low .badge.severity { border-color: #b2ddff; color: var(--low); }
272
+ :root[data-theme="dark"] .severity-high .badge.severity { border-color: #6f2b2b; }
273
+ :root[data-theme="dark"] .severity-medium .badge.severity { border-color: #694614; }
274
+ :root[data-theme="dark"] .severity-low .badge.severity { border-color: #254c7a; }
275
+ .meta {
276
+ display: grid;
277
+ gap: 10px;
278
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
279
+ margin: 12px 0 4px;
280
+ }
281
+ .meta div {
282
+ background: var(--panel-strong);
283
+ border-radius: 6px;
284
+ padding: 10px;
285
+ }
286
+ dt {
287
+ color: var(--muted);
288
+ font-size: 12px;
289
+ font-weight: 700;
290
+ margin-bottom: 3px;
291
+ text-transform: uppercase;
292
+ }
293
+ dd {
294
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
295
+ font-size: 13px;
296
+ margin: 0;
297
+ overflow-wrap: anywhere;
298
+ }
299
+ .empty { color: var(--muted); padding: 18px; }
300
+ footer {
301
+ color: var(--muted);
302
+ font-size: 12px;
303
+ margin-top: 28px;
304
+ }
305
+ @media (max-width: 640px) {
306
+ main { width: min(100% - 20px, 1120px); padding-top: 24px; }
307
+ h1 { font-size: 28px; }
308
+ .topbar { align-items: flex-start; }
309
+ .finding-header { display: block; }
310
+ .badges { justify-content: flex-start; margin-top: 12px; }
311
+ }
312
+ @media print {
313
+ .theme-toggle { display: none; }
314
+ body { background: #ffffff; color: #111827; }
315
+ .summary-card, .finding, .empty { box-shadow: none; }
316
+ }
317
+ </style>
318
+ </head>
319
+ <body>
320
+ <main>
321
+ <div class="topbar">
322
+ <div class="brand">chainlink-audit</div>
323
+ <button class="theme-toggle" type="button" aria-label="Toggle dark mode">Theme: <span id="theme-label">System</span></button>
324
+ </div>
325
+ <header class="hero">
326
+ <p class="eyebrow">Chainlink Audit Kit</p>
327
+ <h1>Chainlink Integration Audit Report</h1>
328
+ <p class="subtitle">Potential Chainlink integration risks detected by heuristic MVP rules. Findings require manual review and are not confirmed vulnerabilities.</p>
329
+ </header>
330
+
331
+ <section class="summary" aria-label="Scan summary">
332
+ <div class="summary-card"><span>Target</span><strong>${escapeHtml(result.targetPath)}</strong></div>
333
+ <div class="summary-card"><span>Solidity Files</span><strong>${result.scannedFiles}</strong></div>
334
+ <div class="summary-card"><span>Products</span><strong>${escapeHtml(products)}</strong></div>
335
+ <div class="summary-card"><span>Findings</span><strong>${result.findings.length}</strong></div>
336
+ <div class="summary-card"><span>Minimum Severity</span><strong>${escapeHtml(result.config.minSeverity)}</strong></div>
337
+ <div class="summary-card"><span>Excluded Paths</span><strong>${escapeHtml(excludedPaths)}</strong></div>
338
+ </section>
339
+
340
+ <p class="note">This report is designed for audit triage. Validate each lead against source code, deployment configuration, and protocol assumptions before disclosure or remediation.</p>
341
+
342
+ <section>
343
+ <h2>Findings</h2>
344
+ <div class="findings">
345
+ ${findings}
346
+ </div>
347
+ </section>
348
+
349
+ <footer>Generated at ${escapeHtml(generatedAt)} by chainlink-audit.</footer>
350
+ </main>
351
+ <script>
352
+ (function () {
353
+ var button = document.querySelector(".theme-toggle");
354
+ var label = document.getElementById("theme-label");
355
+ function applyLabel() {
356
+ var theme = document.documentElement.dataset.theme || "light";
357
+ if (label) label.textContent = theme === "dark" ? "Dark" : "Light";
358
+ }
359
+ if (button) {
360
+ button.addEventListener("click", function () {
361
+ var nextTheme = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
362
+ document.documentElement.dataset.theme = nextTheme;
363
+ localStorage.setItem("chainlink-audit-theme", nextTheme);
364
+ applyLabel();
365
+ });
366
+ }
367
+ applyLabel();
368
+ })();
369
+ </script>
370
+ </body>
371
+ </html>`;
372
+ }
@@ -0,0 +1,3 @@
1
+ export function renderJson(result) {
2
+ return JSON.stringify(result, null, 2);
3
+ }
@@ -0,0 +1,27 @@
1
+ export function renderMarkdown(result) {
2
+ const products = result.products.length > 0 ? result.products.join(", ") : "none detected";
3
+ const lines = [
4
+ "# Chainlink Integration Audit Report",
5
+ "",
6
+ "## Summary",
7
+ "",
8
+ `- Target: \`${result.targetPath}\``,
9
+ `- Solidity files scanned: ${result.scannedFiles}`,
10
+ `- Detected Chainlink products: ${products}`,
11
+ `- Minimum severity: ${result.config.minSeverity}`,
12
+ `- Excluded paths: ${result.config.exclude.length > 0 ? result.config.exclude.join(", ") : "none"}`,
13
+ `- Findings: ${result.findings.length}`,
14
+ "",
15
+ "This report contains potential issues produced by heuristic MVP rules. Findings require manual review before disclosure or remediation.",
16
+ "",
17
+ ];
18
+ if (result.findings.length === 0) {
19
+ lines.push("## Findings", "", "No potential Chainlink integration issues found by MVP rules.");
20
+ return lines.join("\n");
21
+ }
22
+ lines.push("## Findings", "");
23
+ for (const finding of result.findings) {
24
+ lines.push(`### ${finding.ruleId}: ${finding.title}`, "", `- Severity: ${finding.severity}`, `- Confidence: ${finding.confidence}`, `- Location: \`${finding.file}:${finding.line}\``, `- Manual review required: ${finding.manualReviewRequired ? "yes" : "no"}`, "", "#### Description", "", finding.description, "", "#### Risk", "", finding.risk, "", "#### Recommendation", "", finding.recommendation, "");
25
+ }
26
+ return lines.join("\n");
27
+ }
@@ -0,0 +1,27 @@
1
+ export function renderText(result) {
2
+ const products = result.products.length > 0 ? result.products.join(", ") : "none detected";
3
+ const header = [
4
+ "Chainlink Integration Audit Kit",
5
+ `Target: ${result.targetPath}`,
6
+ `Solidity files scanned: ${result.scannedFiles}`,
7
+ `Detected Chainlink products: ${products}`,
8
+ `Minimum severity: ${result.config.minSeverity}`,
9
+ `Excluded paths: ${result.config.exclude.length > 0 ? result.config.exclude.join(", ") : "none"}`,
10
+ `Findings: ${result.findings.length}`,
11
+ ].join("\n");
12
+ if (result.findings.length === 0) {
13
+ return `${header}\n\nNo potential Chainlink integration issues found by MVP rules. Manual review still required.`;
14
+ }
15
+ const findings = result.findings
16
+ .map((finding) => [
17
+ `[${finding.severity.toUpperCase()}] ${finding.ruleId} - ${finding.title}`,
18
+ ` Confidence: ${finding.confidence}`,
19
+ ` Location: ${finding.file}:${finding.line}`,
20
+ ` Description: ${finding.description}`,
21
+ ` Risk: ${finding.risk}`,
22
+ ` Recommendation: ${finding.recommendation}`,
23
+ ` Manual review required: ${finding.manualReviewRequired ? "yes" : "no"}`,
24
+ ].join("\n"))
25
+ .join("\n\n");
26
+ return `${header}\n\n${findings}`;
27
+ }