chainlink-audit 0.1.0 → 0.3.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/README.md +9 -2
- package/dist/config.js +3 -2
- package/dist/index.js +35 -4
- package/dist/reporters/html.js +10 -8
- package/dist/reporters/markdown.js +7 -6
- package/dist/reporters/sarif.js +105 -0
- package/dist/reporters/text.js +6 -5
- package/dist/reporters/triage.js +55 -0
- package/dist/rules/ccip.js +150 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
# chainlink-audit
|
|
2
2
|
|
|
3
|
-
Security CLI for
|
|
3
|
+
Security review CLI for flagging unverified Chainlink integration risk leads in Solidity repositories.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g chainlink-audit
|
|
7
|
+
chainlink-audit version
|
|
7
8
|
chainlink-audit init
|
|
8
9
|
chainlink-audit scan .
|
|
9
10
|
chainlink-audit scan . --format markdown --out chainlink-report.md
|
|
10
11
|
chainlink-audit scan . --format html --out chainlink-report.html
|
|
12
|
+
chainlink-audit scan . --format sarif --out chainlink-report.sarif
|
|
13
|
+
chainlink-audit triage chainlink-report.json --out triage.md
|
|
11
14
|
```
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
Results are heuristic risk leads for manual review, not confirmed vulnerabilities. Potential impact reflects what could happen if the lead is real; it does not prove exploitability.
|
|
17
|
+
|
|
18
|
+
Use `chainlink-audit triage <report.json>` to turn JSON scan output into a manual review checklist.
|
|
19
|
+
|
|
20
|
+
Published package: https://www.npmjs.com/package/chainlink-audit
|
|
14
21
|
|
|
15
22
|
See the repository README for full documentation:
|
|
16
23
|
https://github.com/alva-p/chainlink-integration-audit-kit
|
package/dist/config.js
CHANGED
|
@@ -15,9 +15,10 @@ export function severityRank(severity) {
|
|
|
15
15
|
}[severity];
|
|
16
16
|
}
|
|
17
17
|
export function parseOutputFormat(value) {
|
|
18
|
-
if (value === "text" || value === "json" || value === "markdown" || value === "html")
|
|
18
|
+
if (value === "text" || value === "json" || value === "markdown" || value === "html" || value === "sarif") {
|
|
19
19
|
return value;
|
|
20
|
-
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Unsupported format "${value}". Use text, json, markdown, html, or sarif.`);
|
|
21
22
|
}
|
|
22
23
|
export function parseSeverity(value) {
|
|
23
24
|
if (value === "info" || value === "low" || value === "medium" || value === "high")
|
package/dist/index.js
CHANGED
|
@@ -5,10 +5,12 @@ import { configFileName, loadConfig, parseOutputFormat, parseSeverity, writeDefa
|
|
|
5
5
|
import { renderJson } from "./reporters/json.js";
|
|
6
6
|
import { renderHtml } from "./reporters/html.js";
|
|
7
7
|
import { renderMarkdown } from "./reporters/markdown.js";
|
|
8
|
+
import { renderSarif } from "./reporters/sarif.js";
|
|
8
9
|
import { renderText } from "./reporters/text.js";
|
|
10
|
+
import { renderTriageMarkdown } from "./reporters/triage.js";
|
|
9
11
|
import { rules } from "./rules/index.js";
|
|
10
12
|
import { scanPath } from "./scanner.js";
|
|
11
|
-
const version = "0.
|
|
13
|
+
const version = "0.3.0";
|
|
12
14
|
function render(format, result) {
|
|
13
15
|
if (format === "json")
|
|
14
16
|
return renderJson(result);
|
|
@@ -16,17 +18,19 @@ function render(format, result) {
|
|
|
16
18
|
return renderMarkdown(result);
|
|
17
19
|
if (format === "html")
|
|
18
20
|
return renderHtml(result);
|
|
21
|
+
if (format === "sarif")
|
|
22
|
+
return renderSarif(result);
|
|
19
23
|
return renderText(result);
|
|
20
24
|
}
|
|
21
25
|
const program = new Command();
|
|
22
26
|
program
|
|
23
27
|
.name("chainlink-audit")
|
|
24
|
-
.description("CLI-first
|
|
28
|
+
.description("CLI-first audit assistant for unverified Chainlink integration risk leads.")
|
|
25
29
|
.version(version);
|
|
26
30
|
program
|
|
27
31
|
.command("scan")
|
|
28
32
|
.argument("<path>", "Solidity file or repository path to scan")
|
|
29
|
-
.option("--format <format>", "Output format: text, json, markdown, html")
|
|
33
|
+
.option("--format <format>", "Output format: text, json, markdown, html, sarif")
|
|
30
34
|
.option("--min-severity <severity>", "Minimum severity: info, low, medium, high")
|
|
31
35
|
.option("--out <file>", "Write report to a file instead of stdout")
|
|
32
36
|
.action(async (targetPath, options) => {
|
|
@@ -53,6 +57,33 @@ program
|
|
|
53
57
|
process.exitCode = 2;
|
|
54
58
|
}
|
|
55
59
|
});
|
|
60
|
+
program
|
|
61
|
+
.command("triage")
|
|
62
|
+
.argument("<report.json>", "JSON report produced by chainlink-audit scan --format json")
|
|
63
|
+
.option("--out <file>", "Write triage markdown to a file instead of stdout")
|
|
64
|
+
.description("Convert a JSON scan report into a manual triage checklist")
|
|
65
|
+
.action(async (reportPath, options) => {
|
|
66
|
+
try {
|
|
67
|
+
const raw = await fs.readFile(reportPath, "utf8");
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (!Array.isArray(parsed.findings) || typeof parsed.targetPath !== "string") {
|
|
70
|
+
throw new Error("Input does not look like a chainlink-audit JSON report");
|
|
71
|
+
}
|
|
72
|
+
const output = renderTriageMarkdown(parsed);
|
|
73
|
+
if (options.out) {
|
|
74
|
+
await fs.writeFile(options.out, `${output}\n`, "utf8");
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log(output);
|
|
78
|
+
}
|
|
79
|
+
process.exitCode = 0;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
83
|
+
console.error(`chainlink-audit: ${message}`);
|
|
84
|
+
process.exitCode = 2;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
56
87
|
program.command("init").description(`Create ${configFileName} with recommended defaults`).action(async () => {
|
|
57
88
|
try {
|
|
58
89
|
await writeDefaultConfig();
|
|
@@ -73,7 +104,7 @@ program.command("init").description(`Create ${configFileName} with recommended d
|
|
|
73
104
|
program.command("rules").description("List available MVP rules").action(() => {
|
|
74
105
|
for (const rule of rules) {
|
|
75
106
|
const metadata = rule.metadata;
|
|
76
|
-
console.log(`${metadata.ruleId} [${metadata.severity}] ${metadata.product} - ${metadata.title}`);
|
|
107
|
+
console.log(`${metadata.ruleId} [${metadata.severity} potential impact] ${metadata.product} - ${metadata.title}`);
|
|
77
108
|
}
|
|
78
109
|
});
|
|
79
110
|
program.command("version").description("Print CLI version").action(() => {
|
package/dist/reporters/html.js
CHANGED
|
@@ -18,13 +18,14 @@ function findingCard(finding) {
|
|
|
18
18
|
<h3>${escapeHtml(finding.title)}</h3>
|
|
19
19
|
</div>
|
|
20
20
|
<div class="badges">
|
|
21
|
-
<span class="badge severity">${escapeHtml(finding.severity)}</span>
|
|
22
|
-
<span class="badge confidence">${escapeHtml(finding.confidence)} confidence</span>
|
|
21
|
+
<span class="badge severity">${escapeHtml(finding.severity)} potential impact</span>
|
|
22
|
+
<span class="badge confidence">${escapeHtml(finding.confidence)} detection confidence</span>
|
|
23
23
|
</div>
|
|
24
24
|
</header>
|
|
25
25
|
<dl class="meta">
|
|
26
26
|
<div><dt>Location</dt><dd>${escapeHtml(finding.file)}:${finding.line}</dd></div>
|
|
27
27
|
<div><dt>Manual Review</dt><dd>${finding.manualReviewRequired ? "Required" : "Optional"}</dd></div>
|
|
28
|
+
<div><dt>Confirmed Vulnerability</dt><dd>No</dd></div>
|
|
28
29
|
</dl>
|
|
29
30
|
<section>
|
|
30
31
|
<h4>Description</h4>
|
|
@@ -46,7 +47,7 @@ export function renderHtml(result) {
|
|
|
46
47
|
const generatedAt = new Date().toISOString();
|
|
47
48
|
const findings = result.findings.length > 0
|
|
48
49
|
? result.findings.map(findingCard).join("\n")
|
|
49
|
-
: '<section class="empty">No
|
|
50
|
+
: '<section class="empty">No Chainlink integration risk leads found by MVP rules. Manual review still required.</section>';
|
|
50
51
|
return `<!doctype html>
|
|
51
52
|
<html lang="en">
|
|
52
53
|
<head>
|
|
@@ -325,22 +326,23 @@ export function renderHtml(result) {
|
|
|
325
326
|
<header class="hero">
|
|
326
327
|
<p class="eyebrow">Chainlink Audit Kit</p>
|
|
327
328
|
<h1>Chainlink Integration Audit Report</h1>
|
|
328
|
-
<p class="subtitle">
|
|
329
|
+
<p class="subtitle">Unverified Chainlink integration risk leads detected by heuristic MVP rules. Potential impact is not confirmed exploitability.</p>
|
|
329
330
|
</header>
|
|
330
331
|
|
|
331
332
|
<section class="summary" aria-label="Scan summary">
|
|
332
333
|
<div class="summary-card"><span>Target</span><strong>${escapeHtml(result.targetPath)}</strong></div>
|
|
333
334
|
<div class="summary-card"><span>Solidity Files</span><strong>${result.scannedFiles}</strong></div>
|
|
334
335
|
<div class="summary-card"><span>Products</span><strong>${escapeHtml(products)}</strong></div>
|
|
335
|
-
<div class="summary-card"><span>
|
|
336
|
-
<div class="summary-card"><span>
|
|
336
|
+
<div class="summary-card"><span>Unverified Leads</span><strong>${result.findings.length}</strong></div>
|
|
337
|
+
<div class="summary-card"><span>Confirmed Vulnerabilities</span><strong>0</strong></div>
|
|
338
|
+
<div class="summary-card"><span>Minimum Potential Impact</span><strong>${escapeHtml(result.config.minSeverity)}</strong></div>
|
|
337
339
|
<div class="summary-card"><span>Excluded Paths</span><strong>${escapeHtml(excludedPaths)}</strong></div>
|
|
338
340
|
</section>
|
|
339
341
|
|
|
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>
|
|
342
|
+
<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. High potential impact does not mean a confirmed or exploitable vulnerability.</p>
|
|
341
343
|
|
|
342
344
|
<section>
|
|
343
|
-
<h2>
|
|
345
|
+
<h2>Risk Leads</h2>
|
|
344
346
|
<div class="findings">
|
|
345
347
|
${findings}
|
|
346
348
|
</div>
|
|
@@ -8,20 +8,21 @@ export function renderMarkdown(result) {
|
|
|
8
8
|
`- Target: \`${result.targetPath}\``,
|
|
9
9
|
`- Solidity files scanned: ${result.scannedFiles}`,
|
|
10
10
|
`- Detected Chainlink products: ${products}`,
|
|
11
|
-
`- Minimum
|
|
11
|
+
`- Minimum potential impact: ${result.config.minSeverity}`,
|
|
12
12
|
`- Excluded paths: ${result.config.exclude.length > 0 ? result.config.exclude.join(", ") : "none"}`,
|
|
13
|
-
`-
|
|
13
|
+
`- Unverified leads: ${result.findings.length}`,
|
|
14
|
+
`- Confirmed vulnerabilities: 0`,
|
|
14
15
|
"",
|
|
15
|
-
"This report contains
|
|
16
|
+
"This report contains unverified risk leads produced by heuristic MVP rules. Potential impact is not confirmed exploitability. Validate each lead manually before disclosure or remediation.",
|
|
16
17
|
"",
|
|
17
18
|
];
|
|
18
19
|
if (result.findings.length === 0) {
|
|
19
|
-
lines.push("##
|
|
20
|
+
lines.push("## Risk Leads", "", "No Chainlink integration risk leads found by MVP rules.");
|
|
20
21
|
return lines.join("\n");
|
|
21
22
|
}
|
|
22
|
-
lines.push("##
|
|
23
|
+
lines.push("## Risk Leads", "");
|
|
23
24
|
for (const finding of result.findings) {
|
|
24
|
-
lines.push(`### ${finding.ruleId}: ${finding.title}`, "", `-
|
|
25
|
+
lines.push(`### ${finding.ruleId}: ${finding.title}`, "", `- Potential impact: ${finding.severity}`, `- Detection confidence: ${finding.confidence}`, `- Location: \`${finding.file}:${finding.line}\``, `- Manual review required: ${finding.manualReviewRequired ? "yes" : "no"}`, `- Confirmed vulnerability: no`, "", "#### Description", "", finding.description, "", "#### Risk", "", finding.risk, "", "#### Recommendation", "", finding.recommendation, "");
|
|
25
26
|
}
|
|
26
27
|
return lines.join("\n");
|
|
27
28
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { rules } from "../rules/index.js";
|
|
2
|
+
function normalizeUri(value) {
|
|
3
|
+
return value.split("\\").join("/");
|
|
4
|
+
}
|
|
5
|
+
function sarifLevel(severity) {
|
|
6
|
+
if (severity === "high")
|
|
7
|
+
return "error";
|
|
8
|
+
if (severity === "info")
|
|
9
|
+
return "note";
|
|
10
|
+
return "warning";
|
|
11
|
+
}
|
|
12
|
+
function resultMessage(finding) {
|
|
13
|
+
return [
|
|
14
|
+
`Unverified risk lead. Potential impact: ${finding.severity}. Detection confidence: ${finding.confidence}.`,
|
|
15
|
+
"",
|
|
16
|
+
finding.description,
|
|
17
|
+
"",
|
|
18
|
+
`Risk: ${finding.risk}`,
|
|
19
|
+
"",
|
|
20
|
+
`Recommendation: ${finding.recommendation}`,
|
|
21
|
+
"",
|
|
22
|
+
`Manual review required: ${finding.manualReviewRequired ? "yes" : "no"}. Confirmed vulnerability: no.`,
|
|
23
|
+
].join("\n");
|
|
24
|
+
}
|
|
25
|
+
export function renderSarif(result) {
|
|
26
|
+
const ruleMetadata = new Map(rules.map((rule) => [rule.metadata.ruleId, rule.metadata]));
|
|
27
|
+
const sarif = {
|
|
28
|
+
version: "2.1.0",
|
|
29
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
30
|
+
runs: [
|
|
31
|
+
{
|
|
32
|
+
tool: {
|
|
33
|
+
driver: {
|
|
34
|
+
name: "chainlink-audit",
|
|
35
|
+
informationUri: "https://github.com/alva-p/chainlink-integration-audit-kit",
|
|
36
|
+
rules: rules.map((rule) => ({
|
|
37
|
+
id: rule.metadata.ruleId,
|
|
38
|
+
name: rule.metadata.title,
|
|
39
|
+
shortDescription: {
|
|
40
|
+
text: rule.metadata.title,
|
|
41
|
+
},
|
|
42
|
+
fullDescription: {
|
|
43
|
+
text: rule.metadata.description,
|
|
44
|
+
},
|
|
45
|
+
help: {
|
|
46
|
+
text: `${rule.metadata.description}\n\nProduct: ${rule.metadata.product}. Potential impact: ${rule.metadata.severity}. Results are unverified risk leads and require manual review.`,
|
|
47
|
+
markdown: `${rule.metadata.description}\n\n**Product:** ${rule.metadata.product}\n\n**Potential impact:** ${rule.metadata.severity}\n\nResults are unverified risk leads and require manual review.`,
|
|
48
|
+
},
|
|
49
|
+
properties: {
|
|
50
|
+
product: rule.metadata.product,
|
|
51
|
+
potentialImpact: rule.metadata.severity,
|
|
52
|
+
},
|
|
53
|
+
})),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
automationDetails: {
|
|
57
|
+
id: "chainlink-audit/",
|
|
58
|
+
},
|
|
59
|
+
invocations: [
|
|
60
|
+
{
|
|
61
|
+
executionSuccessful: true,
|
|
62
|
+
properties: {
|
|
63
|
+
targetPath: result.targetPath,
|
|
64
|
+
scannedFiles: result.scannedFiles,
|
|
65
|
+
detectedProducts: result.products,
|
|
66
|
+
minimumPotentialImpact: result.config.minSeverity,
|
|
67
|
+
excludedPaths: result.config.exclude,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
results: result.findings.map((finding) => {
|
|
72
|
+
const metadata = ruleMetadata.get(finding.ruleId);
|
|
73
|
+
return {
|
|
74
|
+
ruleId: finding.ruleId,
|
|
75
|
+
ruleIndex: rules.findIndex((rule) => rule.metadata.ruleId === finding.ruleId),
|
|
76
|
+
level: sarifLevel(finding.severity),
|
|
77
|
+
message: {
|
|
78
|
+
text: resultMessage(finding),
|
|
79
|
+
},
|
|
80
|
+
locations: [
|
|
81
|
+
{
|
|
82
|
+
physicalLocation: {
|
|
83
|
+
artifactLocation: {
|
|
84
|
+
uri: normalizeUri(finding.file),
|
|
85
|
+
},
|
|
86
|
+
region: {
|
|
87
|
+
startLine: finding.line,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
properties: {
|
|
93
|
+
potentialImpact: finding.severity,
|
|
94
|
+
detectionConfidence: finding.confidence,
|
|
95
|
+
product: metadata?.product,
|
|
96
|
+
manualReviewRequired: finding.manualReviewRequired,
|
|
97
|
+
confirmedVulnerability: false,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
return JSON.stringify(sarif, null, 2);
|
|
105
|
+
}
|
package/dist/reporters/text.js
CHANGED
|
@@ -5,22 +5,23 @@ export function renderText(result) {
|
|
|
5
5
|
`Target: ${result.targetPath}`,
|
|
6
6
|
`Solidity files scanned: ${result.scannedFiles}`,
|
|
7
7
|
`Detected Chainlink products: ${products}`,
|
|
8
|
-
`Minimum
|
|
8
|
+
`Minimum potential impact: ${result.config.minSeverity}`,
|
|
9
9
|
`Excluded paths: ${result.config.exclude.length > 0 ? result.config.exclude.join(", ") : "none"}`,
|
|
10
|
-
`
|
|
10
|
+
`Unverified leads: ${result.findings.length}`,
|
|
11
11
|
].join("\n");
|
|
12
12
|
if (result.findings.length === 0) {
|
|
13
|
-
return `${header}\n\nNo
|
|
13
|
+
return `${header}\n\nNo Chainlink integration risk leads found by MVP rules. Manual review still required.`;
|
|
14
14
|
}
|
|
15
15
|
const findings = result.findings
|
|
16
16
|
.map((finding) => [
|
|
17
|
-
`[${finding.severity.toUpperCase()}] ${finding.ruleId} - ${finding.title}`,
|
|
18
|
-
`
|
|
17
|
+
`[${finding.severity.toUpperCase()} POTENTIAL IMPACT] ${finding.ruleId} - ${finding.title}`,
|
|
18
|
+
` Detection confidence: ${finding.confidence}`,
|
|
19
19
|
` Location: ${finding.file}:${finding.line}`,
|
|
20
20
|
` Description: ${finding.description}`,
|
|
21
21
|
` Risk: ${finding.risk}`,
|
|
22
22
|
` Recommendation: ${finding.recommendation}`,
|
|
23
23
|
` Manual review required: ${finding.manualReviewRequired ? "yes" : "no"}`,
|
|
24
|
+
" Confirmed vulnerability: no",
|
|
24
25
|
].join("\n"))
|
|
25
26
|
.join("\n\n");
|
|
26
27
|
return `${header}\n\n${findings}`;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function severityRank(finding) {
|
|
2
|
+
return {
|
|
3
|
+
info: 0,
|
|
4
|
+
low: 1,
|
|
5
|
+
medium: 2,
|
|
6
|
+
high: 3,
|
|
7
|
+
}[finding.severity];
|
|
8
|
+
}
|
|
9
|
+
function checkbox(label) {
|
|
10
|
+
return `- [ ] ${label}`;
|
|
11
|
+
}
|
|
12
|
+
export function renderTriageMarkdown(result) {
|
|
13
|
+
const products = result.products.length > 0 ? result.products.join(", ") : "none detected";
|
|
14
|
+
const findings = [...result.findings].sort((a, b) => {
|
|
15
|
+
const severityCompare = severityRank(b) - severityRank(a);
|
|
16
|
+
if (severityCompare !== 0)
|
|
17
|
+
return severityCompare;
|
|
18
|
+
const confidenceCompare = b.confidence.localeCompare(a.confidence);
|
|
19
|
+
if (confidenceCompare !== 0)
|
|
20
|
+
return confidenceCompare;
|
|
21
|
+
return a.file.localeCompare(b.file) || a.line - b.line || a.ruleId.localeCompare(b.ruleId);
|
|
22
|
+
});
|
|
23
|
+
const lines = [
|
|
24
|
+
"# Chainlink Audit Triage",
|
|
25
|
+
"",
|
|
26
|
+
"## Summary",
|
|
27
|
+
"",
|
|
28
|
+
`- Target: \`${result.targetPath}\``,
|
|
29
|
+
`- Solidity files scanned: ${result.scannedFiles}`,
|
|
30
|
+
`- Detected Chainlink products: ${products}`,
|
|
31
|
+
`- Unverified leads: ${findings.length}`,
|
|
32
|
+
"- Confirmed vulnerabilities: 0",
|
|
33
|
+
"",
|
|
34
|
+
"This triage file is for manual audit review. A high potential-impact lead is not a confirmed vulnerability.",
|
|
35
|
+
"",
|
|
36
|
+
"## Review Status Legend",
|
|
37
|
+
"",
|
|
38
|
+
checkbox("True positive"),
|
|
39
|
+
checkbox("False positive"),
|
|
40
|
+
checkbox("Accepted risk"),
|
|
41
|
+
checkbox("Needs more context"),
|
|
42
|
+
checkbox("Not reportable"),
|
|
43
|
+
"",
|
|
44
|
+
"## Leads",
|
|
45
|
+
"",
|
|
46
|
+
];
|
|
47
|
+
if (findings.length === 0) {
|
|
48
|
+
lines.push("No unverified risk leads were found by the current rule set.", "");
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
for (const finding of findings) {
|
|
52
|
+
lines.push(`### ${finding.ruleId}: ${finding.title}`, "", `- Potential impact: ${finding.severity}`, `- Detection confidence: ${finding.confidence}`, `- Location: \`${finding.file}:${finding.line}\``, "- Confirmed vulnerability: no", "", "#### Manual Status", "", checkbox("True positive"), checkbox("False positive"), checkbox("Accepted risk"), checkbox("Needs more context"), checkbox("Not reportable"), "", "#### Reviewer Notes", "", "- Evidence:", "- Exploitability:", "- Impact:", "- Recommendation:", "- Disclosure path:", "", "#### Scanner Context", "", `- Description: ${finding.description}`, `- Risk: ${finding.risk}`, `- Recommendation: ${finding.recommendation}`, "");
|
|
53
|
+
}
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
package/dist/rules/ccip.js
CHANGED
|
@@ -1,10 +1,69 @@
|
|
|
1
1
|
import { countMatches, extractFunctionBody, firstLineMatching, makeFinding } from "./helpers.js";
|
|
2
2
|
function ccipBody(content) {
|
|
3
|
-
return extractFunctionBody(content, "
|
|
3
|
+
return [extractFunctionBody(content, "ccipReceive"), extractFunctionBody(content, "_ccipReceive")].filter(Boolean).join("\n");
|
|
4
|
+
}
|
|
5
|
+
function ccipHeader(content) {
|
|
6
|
+
const match = /function\s+(_ccipReceive|ccipReceive)\b[^{;]*/.exec(content);
|
|
7
|
+
return match?.[0] ?? "";
|
|
4
8
|
}
|
|
5
9
|
function hasCcipReceiver(content) {
|
|
6
10
|
return /(function\s+_ccipReceive\b|function\s+ccipReceive\b|is\s+CCIPReceiver)/.test(content);
|
|
7
11
|
}
|
|
12
|
+
function isCcipBaseReceiver(content) {
|
|
13
|
+
return (/interface\s+\w*I?Any2EVMMessageReceiver\b/.test(content) ||
|
|
14
|
+
/interface\s+\w+\s*{[\s\S]*function\s+ccipReceive\b[^{]*;/.test(content) ||
|
|
15
|
+
/abstract\s+contract\s+\w*CCIPReceiver\b/.test(content) ||
|
|
16
|
+
/function\s+_ccipReceive\b[^{;]*internal[^{;]*virtual\s*;/.test(content));
|
|
17
|
+
}
|
|
18
|
+
function inheritsCcipReceiver(content) {
|
|
19
|
+
return /\bis\s+[\s\S{]*\bCCIPReceiver(Upgradeable)?\b/.test(content);
|
|
20
|
+
}
|
|
21
|
+
function escapeRegExp(value) {
|
|
22
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23
|
+
}
|
|
24
|
+
function delegatedCcipContent(body, repoSignals) {
|
|
25
|
+
const match = /\b([A-Z][A-Za-z0-9_]*)\s*\.\s*([A-Za-z0-9_]*ccipReceive|processCcipReceive)\s*\(/i.exec(body);
|
|
26
|
+
if (!match)
|
|
27
|
+
return body;
|
|
28
|
+
const [, libraryOrContractName, functionName] = match;
|
|
29
|
+
const typePattern = new RegExp(`\\b(library|contract)\\s+${escapeRegExp(libraryOrContractName)}\\b`);
|
|
30
|
+
const functionPattern = new RegExp(`\\bfunction\\s+${escapeRegExp(functionName)}\\b`);
|
|
31
|
+
const delegate = repoSignals.files.find((file) => typePattern.test(file.content) && functionPattern.test(file.content));
|
|
32
|
+
return delegate ? `${body}\n${delegate.content}` : body;
|
|
33
|
+
}
|
|
34
|
+
function validatesSourceChain(content, body) {
|
|
35
|
+
const header = ccipHeader(content);
|
|
36
|
+
const directValidation = /(sourceChainSelector|ccipSelectorToChainId)/.test(body) &&
|
|
37
|
+
/(require|revert|allowed|trusted|allowlist|supportedNetworks|UnsupportedNetwork)/i.test(body);
|
|
38
|
+
const modifierValidation = /(validChain|allowlisted|allowlist|trusted|onlyAllowlisted)[^{;]*(sourceChainSelector|\.sourceChainSelector)/i.test(header);
|
|
39
|
+
return directValidation || modifierValidation;
|
|
40
|
+
}
|
|
41
|
+
function validatesSourceSender(content, body) {
|
|
42
|
+
const header = ccipHeader(content);
|
|
43
|
+
const directValidation = /(message\.sender|data\.sender|sender)/.test(body) &&
|
|
44
|
+
/(require|revert|allowed|trusted|allowlist|Unauthorized)/i.test(body);
|
|
45
|
+
const modifierValidation = /(validSender|trustedSender|allowlistedSender|onlyAllowlisted|allowlisted|allowlist|trusted)[^{;]*(message\.sender|\.sender|sender)/i.test(header);
|
|
46
|
+
return directValidation || modifierValidation;
|
|
47
|
+
}
|
|
48
|
+
function hasBusinessMutation(body) {
|
|
49
|
+
return (countMatches(body, [
|
|
50
|
+
/transfer/i,
|
|
51
|
+
/mint/i,
|
|
52
|
+
/burn/i,
|
|
53
|
+
/swap/i,
|
|
54
|
+
/deposit/i,
|
|
55
|
+
/withdraw/i,
|
|
56
|
+
/credit/i,
|
|
57
|
+
/call\s*\{/,
|
|
58
|
+
/\[[^\]]+\]\s*(\+\+|--|\+=|-=|=)/,
|
|
59
|
+
/\b(push|pop)\s*\(/,
|
|
60
|
+
]) > 0);
|
|
61
|
+
}
|
|
62
|
+
function hasMessageIdTracking(content, body) {
|
|
63
|
+
if (!/(messageId|\.messageId)/.test(body))
|
|
64
|
+
return false;
|
|
65
|
+
return /(processed|received|executed|consumed|handled|seen|replay|duplicate|messageDetail|failedMessages|messageIdTo)/i.test(content);
|
|
66
|
+
}
|
|
8
67
|
export const ccipRules = [
|
|
9
68
|
{
|
|
10
69
|
metadata: {
|
|
@@ -17,9 +76,11 @@ export const ccipRules = [
|
|
|
17
76
|
scan(context) {
|
|
18
77
|
if (!hasCcipReceiver(context.content))
|
|
19
78
|
return [];
|
|
79
|
+
if (isCcipBaseReceiver(context.content))
|
|
80
|
+
return [];
|
|
20
81
|
const body = ccipBody(context.content) || context.content;
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
82
|
+
const validationContent = `${ccipHeader(context.content)}\n${delegatedCcipContent(body, context.repoSignals)}`;
|
|
83
|
+
if (validatesSourceChain(context.content, validationContent))
|
|
23
84
|
return [];
|
|
24
85
|
return [
|
|
25
86
|
makeFinding({
|
|
@@ -47,9 +108,11 @@ export const ccipRules = [
|
|
|
47
108
|
scan(context) {
|
|
48
109
|
if (!hasCcipReceiver(context.content))
|
|
49
110
|
return [];
|
|
111
|
+
if (isCcipBaseReceiver(context.content))
|
|
112
|
+
return [];
|
|
50
113
|
const body = ccipBody(context.content) || context.content;
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
114
|
+
const validationContent = `${ccipHeader(context.content)}\n${delegatedCcipContent(body, context.repoSignals)}`;
|
|
115
|
+
if (validatesSourceSender(context.content, validationContent))
|
|
53
116
|
return [];
|
|
54
117
|
return [
|
|
55
118
|
makeFinding({
|
|
@@ -77,8 +140,12 @@ export const ccipRules = [
|
|
|
77
140
|
scan(context) {
|
|
78
141
|
if (!hasCcipReceiver(context.content))
|
|
79
142
|
return [];
|
|
80
|
-
|
|
81
|
-
|
|
143
|
+
if (isCcipBaseReceiver(context.content))
|
|
144
|
+
return [];
|
|
145
|
+
const body = ccipBody(context.content) || context.content;
|
|
146
|
+
const validationContent = `${ccipHeader(context.content)}\n${delegatedCcipContent(body, context.repoSignals)}`;
|
|
147
|
+
const validatesRouter = /msg\.sender\s*(!=|==)\s*(router|s_router|i_router|address\(router\))|InvalidRouter|onlyRouter|NotCcipRouter|_msgSender\(\)\s*!=\s*address\([^)]*ccipRouter/i.test(validationContent);
|
|
148
|
+
if (validatesRouter || inheritsCcipReceiver(context.content))
|
|
82
149
|
return [];
|
|
83
150
|
return [
|
|
84
151
|
makeFinding({
|
|
@@ -106,6 +173,8 @@ export const ccipRules = [
|
|
|
106
173
|
scan(context) {
|
|
107
174
|
if (!hasCcipReceiver(context.content))
|
|
108
175
|
return [];
|
|
176
|
+
if (isCcipBaseReceiver(context.content))
|
|
177
|
+
return [];
|
|
109
178
|
const body = ccipBody(context.content) || context.content;
|
|
110
179
|
if (!/abi\.decode\s*\(\s*message\.data/.test(body))
|
|
111
180
|
return [];
|
|
@@ -138,6 +207,8 @@ export const ccipRules = [
|
|
|
138
207
|
scan(context) {
|
|
139
208
|
if (!hasCcipReceiver(context.content))
|
|
140
209
|
return [];
|
|
210
|
+
if (isCcipBaseReceiver(context.content))
|
|
211
|
+
return [];
|
|
141
212
|
const body = ccipBody(context.content);
|
|
142
213
|
if (!body)
|
|
143
214
|
return [];
|
|
@@ -160,4 +231,76 @@ export const ccipRules = [
|
|
|
160
231
|
];
|
|
161
232
|
},
|
|
162
233
|
},
|
|
234
|
+
{
|
|
235
|
+
metadata: {
|
|
236
|
+
ruleId: "CL-CCIP-006",
|
|
237
|
+
product: "ccip",
|
|
238
|
+
severity: "medium",
|
|
239
|
+
title: "Potential CCIP token amount indexing without length check",
|
|
240
|
+
description: "Receiver appears to index destTokenAmounts without an obvious length check.",
|
|
241
|
+
},
|
|
242
|
+
scan(context) {
|
|
243
|
+
if (!hasCcipReceiver(context.content))
|
|
244
|
+
return [];
|
|
245
|
+
if (isCcipBaseReceiver(context.content))
|
|
246
|
+
return [];
|
|
247
|
+
const body = ccipBody(context.content) || context.content;
|
|
248
|
+
const indexesTokenAmounts = /(?:message\.|any2EvmMessage\.)?destTokenAmounts\s*\[\s*\d+\s*\]/.test(body) ||
|
|
249
|
+
/(?:tokenAmounts|destTokenAmounts)\s*\[\s*\d+\s*\]/.test(body);
|
|
250
|
+
if (!indexesTokenAmounts)
|
|
251
|
+
return [];
|
|
252
|
+
const checksLength = /(?:destTokenAmounts|tokenAmounts)\.length/.test(body) ||
|
|
253
|
+
/(InvalidTokenAmounts|InvalidTokenAmount|NoToken|ExpectedToken|UnexpectedToken)/i.test(body);
|
|
254
|
+
if (checksLength)
|
|
255
|
+
return [];
|
|
256
|
+
return [
|
|
257
|
+
makeFinding({
|
|
258
|
+
context,
|
|
259
|
+
ruleId: this.metadata.ruleId,
|
|
260
|
+
severity: this.metadata.severity,
|
|
261
|
+
confidence: "medium",
|
|
262
|
+
line: firstLineMatching(context.lines, /(?:destTokenAmounts|tokenAmounts)\s*\[/),
|
|
263
|
+
title: this.metadata.title,
|
|
264
|
+
description: "Potential issue: malformed or unexpected CCIP token payloads may revert before business logic can handle them.",
|
|
265
|
+
risk: "Assuming at least one token amount can cause message processing failure or inconsistent recovery behavior.",
|
|
266
|
+
recommendation: "Validate destTokenAmounts.length and expected token addresses/amounts before indexing token amounts.",
|
|
267
|
+
}),
|
|
268
|
+
];
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
metadata: {
|
|
273
|
+
ruleId: "CL-CCIP-007",
|
|
274
|
+
product: "ccip",
|
|
275
|
+
severity: "medium",
|
|
276
|
+
title: "Potential CCIP receiver without messageId idempotency tracking",
|
|
277
|
+
description: "Mutating receiver logic does not show obvious messageId replay or idempotency tracking.",
|
|
278
|
+
},
|
|
279
|
+
scan(context) {
|
|
280
|
+
if (!hasCcipReceiver(context.content))
|
|
281
|
+
return [];
|
|
282
|
+
if (isCcipBaseReceiver(context.content))
|
|
283
|
+
return [];
|
|
284
|
+
const body = ccipBody(context.content);
|
|
285
|
+
if (!body)
|
|
286
|
+
return [];
|
|
287
|
+
if (!hasBusinessMutation(body))
|
|
288
|
+
return [];
|
|
289
|
+
if (hasMessageIdTracking(context.content, body))
|
|
290
|
+
return [];
|
|
291
|
+
return [
|
|
292
|
+
makeFinding({
|
|
293
|
+
context,
|
|
294
|
+
ruleId: this.metadata.ruleId,
|
|
295
|
+
severity: this.metadata.severity,
|
|
296
|
+
confidence: "low",
|
|
297
|
+
line: firstLineMatching(context.lines, /(_ccipReceive|ccipReceive)/),
|
|
298
|
+
title: this.metadata.title,
|
|
299
|
+
description: "Potential issue: repeated or retried CCIP messages may execute mutating business logic more than once.",
|
|
300
|
+
risk: "Without idempotency tracking, replay-like operational scenarios or manual execution flows can duplicate state changes.",
|
|
301
|
+
recommendation: "Track processed messageId values or design receiver logic to be explicitly idempotent before mutating state.",
|
|
302
|
+
}),
|
|
303
|
+
];
|
|
304
|
+
},
|
|
305
|
+
},
|
|
163
306
|
];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chainlink-audit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Security CLI for
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Security review CLI for flagging unverified Chainlink integration risk leads in Solidity repositories.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"keywords": [
|