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 CHANGED
@@ -1,16 +1,23 @@
1
1
  # chainlink-audit
2
2
 
3
- Security CLI for detecting potential Chainlink integration risks in Solidity repositories.
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
- Findings are heuristic leads for manual review, not confirmed vulnerabilities.
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
- throw new Error(`Unsupported format "${value}". Use text, json, markdown, or html.`);
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.1.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 scanner for potential Chainlink integration risks in Solidity repositories.")
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(() => {
@@ -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 potential Chainlink integration issues found by MVP rules. Manual review still required.</section>';
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">Potential Chainlink integration risks detected by heuristic MVP rules. Findings require manual review and are not confirmed vulnerabilities.</p>
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>Findings</span><strong>${result.findings.length}</strong></div>
336
- <div class="summary-card"><span>Minimum Severity</span><strong>${escapeHtml(result.config.minSeverity)}</strong></div>
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>Findings</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 severity: ${result.config.minSeverity}`,
11
+ `- Minimum potential impact: ${result.config.minSeverity}`,
12
12
  `- Excluded paths: ${result.config.exclude.length > 0 ? result.config.exclude.join(", ") : "none"}`,
13
- `- Findings: ${result.findings.length}`,
13
+ `- Unverified leads: ${result.findings.length}`,
14
+ `- Confirmed vulnerabilities: 0`,
14
15
  "",
15
- "This report contains potential issues produced by heuristic MVP rules. Findings require manual review before disclosure or remediation.",
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("## Findings", "", "No potential Chainlink integration issues found by MVP rules.");
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("## Findings", "");
23
+ lines.push("## Risk Leads", "");
23
24
  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
+ 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
+ }
@@ -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 severity: ${result.config.minSeverity}`,
8
+ `Minimum potential impact: ${result.config.minSeverity}`,
9
9
  `Excluded paths: ${result.config.exclude.length > 0 ? result.config.exclude.join(", ") : "none"}`,
10
- `Findings: ${result.findings.length}`,
10
+ `Unverified leads: ${result.findings.length}`,
11
11
  ].join("\n");
12
12
  if (result.findings.length === 0) {
13
- return `${header}\n\nNo potential Chainlink integration issues found by MVP rules. Manual review still required.`;
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
- ` Confidence: ${finding.confidence}`,
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
+ }
@@ -1,10 +1,69 @@
1
1
  import { countMatches, extractFunctionBody, firstLineMatching, makeFinding } from "./helpers.js";
2
2
  function ccipBody(content) {
3
- return extractFunctionBody(content, "_ccipReceive") || extractFunctionBody(content, "ccipReceive");
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 validates = /sourceChainSelector/.test(body) && /(require|revert|allowed|trusted|allowlist)/i.test(body);
22
- if (validates)
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 validates = /(message\.sender|sender)/.test(body) && /(require|revert|allowed|trusted|allowlist)/i.test(body);
52
- if (validates)
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
- const validatesRouter = /msg\.sender\s*(!=|==)\s*(router|s_router|i_router|address\(router\))|InvalidRouter|onlyRouter/i.test(context.content);
81
- if (validatesRouter || /is\s+CCIPReceiver/.test(context.content))
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.1.0",
4
- "description": "Security CLI for detecting potential Chainlink integration risks in Solidity repositories.",
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": [