chainlink-audit 0.3.1 → 0.3.3
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 +11 -0
- package/dist/benchmark.js +58 -0
- package/dist/rules/automation.js +18 -4
- package/dist/rules/ccip.js +51 -11
- package/dist/rules/data-feeds.js +16 -3
- package/dist/rules/functions-cre.js +1 -1
- package/dist/rules/helpers.js +14 -2
- package/dist/rules/vrf.js +3 -1
- package/dist/scanner.js +10 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -17,6 +17,17 @@ Results are heuristic risk leads for manual review, not confirmed vulnerabilitie
|
|
|
17
17
|
|
|
18
18
|
Use `chainlink-audit triage <report.json>` to turn JSON scan output into a manual review checklist.
|
|
19
19
|
|
|
20
|
+
## Ecosystem Benchmark
|
|
21
|
+
|
|
22
|
+
The repository includes a pinned Chainlink Ecosystem benchmark manifest at `benchmarks/ecosystem-repos.json`.
|
|
23
|
+
It tracks public GitHub repositories for Ecosystem projects outside the original manual audit sample.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm run benchmark:ecosystem -- --out ../../cache/ecosystem-benchmark-report.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The runner clones pinned commits into `../../cache/ecosystem-benchmark`, scans each checkout, and writes an aggregate JSON report with per-project and per-rule lead counts.
|
|
30
|
+
|
|
20
31
|
Published package: https://www.npmjs.com/package/chainlink-audit
|
|
21
32
|
|
|
22
33
|
See the repository README for full documentation:
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function increment(map, key, value = 1) {
|
|
2
|
+
map[key] = (map[key] ?? 0) + value;
|
|
3
|
+
}
|
|
4
|
+
function sortCounts(counts) {
|
|
5
|
+
return Object.fromEntries(Object.entries(counts).sort(([leftKey, leftValue], [rightKey, rightValue]) => {
|
|
6
|
+
return rightValue - leftValue || leftKey.localeCompare(rightKey);
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
9
|
+
function productFromRuleId(ruleId) {
|
|
10
|
+
const productCode = ruleId.split("-")[1]?.toUpperCase();
|
|
11
|
+
return ({
|
|
12
|
+
AUTO: "automation",
|
|
13
|
+
CCIP: "ccip",
|
|
14
|
+
DF: "data-feeds",
|
|
15
|
+
FN: "functions-cre",
|
|
16
|
+
VRF: "vrf",
|
|
17
|
+
}[productCode ?? ""] ?? "unknown");
|
|
18
|
+
}
|
|
19
|
+
export function summarizeScanResults(manifestRepositories, scanResults) {
|
|
20
|
+
return scanResults.map((result, index) => {
|
|
21
|
+
const repository = manifestRepositories[index];
|
|
22
|
+
const findingsByRule = {};
|
|
23
|
+
for (const finding of result.findings) {
|
|
24
|
+
increment(findingsByRule, finding.ruleId);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
project: repository.project,
|
|
28
|
+
githubUrl: repository.githubUrl,
|
|
29
|
+
commit: repository.commit,
|
|
30
|
+
scannedFiles: result.scannedFiles,
|
|
31
|
+
scannerProducts: result.products,
|
|
32
|
+
totalFindings: result.findings.length,
|
|
33
|
+
findingsByRule: sortCounts(findingsByRule),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function summarizeBenchmarkResults(projectResults) {
|
|
38
|
+
const products = new Set();
|
|
39
|
+
const findingsByRule = {};
|
|
40
|
+
const findingsByProduct = {};
|
|
41
|
+
for (const result of projectResults) {
|
|
42
|
+
for (const product of result.scannerProducts) {
|
|
43
|
+
products.add(product);
|
|
44
|
+
}
|
|
45
|
+
for (const [ruleId, count] of Object.entries(result.findingsByRule)) {
|
|
46
|
+
increment(findingsByRule, ruleId, count);
|
|
47
|
+
increment(findingsByProduct, productFromRuleId(ruleId), count);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
repositoryCount: projectResults.length,
|
|
52
|
+
scannedFiles: projectResults.reduce((total, result) => total + result.scannedFiles, 0),
|
|
53
|
+
totalFindings: projectResults.reduce((total, result) => total + result.totalFindings, 0),
|
|
54
|
+
scannerProducts: [...products].sort(),
|
|
55
|
+
findingsByRule: sortCounts(findingsByRule),
|
|
56
|
+
findingsByProduct: sortCounts(findingsByProduct),
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/rules/automation.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import { extractFunctionBody, firstLineMatching, makeFinding } from "./helpers.js";
|
|
1
|
+
import { escapeRegExp, extractFunctionBody, firstLineMatching, isInterfaceOnlyFile, makeFinding } from "./helpers.js";
|
|
2
2
|
function hasAutomation(content) {
|
|
3
3
|
return /(AutomationCompatibleInterface|KeeperCompatibleInterface|function\s+checkUpkeep\b|function\s+performUpkeep\b)/.test(content);
|
|
4
4
|
}
|
|
5
|
+
function internalCallBodies(content, body) {
|
|
6
|
+
const calledNames = [...body.matchAll(/\b([a-zA-Z_]\w*)\s*\(/g)].map((match) => match[1]);
|
|
7
|
+
return [...new Set(calledNames)]
|
|
8
|
+
.map((name) => {
|
|
9
|
+
const declaration = new RegExp(`function\\s+${escapeRegExp(name)}\\b[^{;]*\\b(internal|private)\\b`);
|
|
10
|
+
return declaration.test(content) ? extractFunctionBody(content, name) : "";
|
|
11
|
+
})
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.join("\n");
|
|
14
|
+
}
|
|
5
15
|
export const automationRules = [
|
|
6
16
|
{
|
|
7
17
|
metadata: {
|
|
@@ -17,8 +27,9 @@ export const automationRules = [
|
|
|
17
27
|
const body = extractFunctionBody(context.content, "performUpkeep");
|
|
18
28
|
if (!body)
|
|
19
29
|
return [];
|
|
20
|
-
const
|
|
21
|
-
|
|
30
|
+
const extendedBody = `${body}\n${internalCallBodies(context.content, body)}`;
|
|
31
|
+
const revalidates = /(require|revert|UpkeepNotNeeded|checkUpkeep)/.test(extendedBody) &&
|
|
32
|
+
/(block\.timestamp|lastExecutedAt|interval|upkeepNeeded|balance|paused)/.test(extendedBody);
|
|
22
33
|
if (revalidates)
|
|
23
34
|
return [];
|
|
24
35
|
return [
|
|
@@ -81,7 +92,10 @@ export const automationRules = [
|
|
|
81
92
|
return [];
|
|
82
93
|
if (!/function\s+performUpkeep\b/.test(context.content))
|
|
83
94
|
return [];
|
|
84
|
-
|
|
95
|
+
if (isInterfaceOnlyFile(context.content))
|
|
96
|
+
return [];
|
|
97
|
+
const pausePattern = /(Pausable|whenNotPaused|paused|unpause|pause\w*\s*\(|emergency|guardian|abort\w*\s*\(|dismantle|kill\w*\s*\(|circuitBreak)/i;
|
|
98
|
+
const hasPause = pausePattern.test(context.content) || context.repoSignals.files.some((file) => pausePattern.test(file.content));
|
|
85
99
|
if (hasPause)
|
|
86
100
|
return [];
|
|
87
101
|
return [
|
package/dist/rules/ccip.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { countMatches, extractFunctionBody, firstLineMatching, makeFinding } from "./helpers.js";
|
|
1
|
+
import { countMatches, escapeRegExp, extractBlockBody, extractFunctionBody, firstLineMatching, makeFinding } from "./helpers.js";
|
|
2
2
|
function ccipBody(content) {
|
|
3
3
|
return [extractFunctionBody(content, "ccipReceive"), extractFunctionBody(content, "_ccipReceive")].filter(Boolean).join("\n");
|
|
4
4
|
}
|
|
5
|
+
const CCIP_RECEIVE_DECL = /function\s+(_ccipReceive|ccipReceive)\b/;
|
|
5
6
|
function ccipHeader(content) {
|
|
6
7
|
const match = /function\s+(_ccipReceive|ccipReceive)\b[^{;]*/.exec(content);
|
|
7
8
|
return match?.[0] ?? "";
|
|
@@ -31,9 +32,6 @@ function isCcipBaseReceiver(content) {
|
|
|
31
32
|
function inheritsCcipReceiver(content) {
|
|
32
33
|
return /\bis\s+[\s\S{]*\bCCIPReceiver(Upgradeable)?\b/.test(content);
|
|
33
34
|
}
|
|
34
|
-
function escapeRegExp(value) {
|
|
35
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36
|
-
}
|
|
37
35
|
function extractParentNames(content) {
|
|
38
36
|
const match = /\bcontract\s+\w+\s+is\s+([\w\s,]+?)(?:\{|$)/m.exec(content);
|
|
39
37
|
if (!match)
|
|
@@ -58,6 +56,44 @@ function delegatedCcipContent(body, repoSignals) {
|
|
|
58
56
|
const delegate = repoSignals.files.find((file) => typePattern.test(file.content) && functionPattern.test(file.content));
|
|
59
57
|
return delegate ? `${body}\n${delegate.content}` : body;
|
|
60
58
|
}
|
|
59
|
+
function extractModifierNames(header) {
|
|
60
|
+
const afterParams = header.replace(/^function\s+\w+\s*\([^)]*\)/s, "");
|
|
61
|
+
const names = afterParams.match(/\b[A-Za-z_]\w*\b/g) ?? [];
|
|
62
|
+
const keywords = new Set([
|
|
63
|
+
"external",
|
|
64
|
+
"public",
|
|
65
|
+
"internal",
|
|
66
|
+
"private",
|
|
67
|
+
"view",
|
|
68
|
+
"pure",
|
|
69
|
+
"payable",
|
|
70
|
+
"override",
|
|
71
|
+
"virtual",
|
|
72
|
+
"returns",
|
|
73
|
+
]);
|
|
74
|
+
return [...new Set(names)].filter((name) => !keywords.has(name));
|
|
75
|
+
}
|
|
76
|
+
function resolveModifierBodies(content, repoSignals) {
|
|
77
|
+
const header = ccipHeader(content);
|
|
78
|
+
const modifierNames = extractModifierNames(header);
|
|
79
|
+
if (modifierNames.length === 0)
|
|
80
|
+
return "";
|
|
81
|
+
const parents = extractParentNames(content);
|
|
82
|
+
const parentFiles = repoSignals.files.filter((file) => parents.some((parent) => new RegExp(`\\b(?:abstract\\s+)?contract\\s+${escapeRegExp(parent)}\\b`).test(file.content)));
|
|
83
|
+
const candidateFiles = [{ content }, ...parentFiles];
|
|
84
|
+
return modifierNames
|
|
85
|
+
.map((name) => {
|
|
86
|
+
const declaration = new RegExp(`modifier\\s+${escapeRegExp(name)}\\b`);
|
|
87
|
+
for (const file of candidateFiles) {
|
|
88
|
+
const body = extractBlockBody(file.content, declaration);
|
|
89
|
+
if (body)
|
|
90
|
+
return body;
|
|
91
|
+
}
|
|
92
|
+
return "";
|
|
93
|
+
})
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.join("\n");
|
|
96
|
+
}
|
|
61
97
|
function stripEmitLines(body) {
|
|
62
98
|
return body
|
|
63
99
|
.split("\n")
|
|
@@ -136,7 +172,7 @@ export const ccipRules = [
|
|
|
136
172
|
if (isCcipBaseReceiver(context.content))
|
|
137
173
|
return [];
|
|
138
174
|
const body = ccipBody(context.content) || context.content;
|
|
139
|
-
const validationContent = `${ccipHeader(context.content)}\n${delegatedCcipContent(body, context.repoSignals)}`;
|
|
175
|
+
const validationContent = `${ccipHeader(context.content)}\n${delegatedCcipContent(body, context.repoSignals)}\n${resolveModifierBodies(context.content, context.repoSignals)}`;
|
|
140
176
|
if (validatesSourceChain(context.content, validationContent))
|
|
141
177
|
return [];
|
|
142
178
|
return [
|
|
@@ -145,7 +181,7 @@ export const ccipRules = [
|
|
|
145
181
|
ruleId: this.metadata.ruleId,
|
|
146
182
|
severity: this.metadata.severity,
|
|
147
183
|
confidence: "medium",
|
|
148
|
-
line: firstLineMatching(context.lines,
|
|
184
|
+
line: firstLineMatching(context.lines, CCIP_RECEIVE_DECL),
|
|
149
185
|
title: this.metadata.title,
|
|
150
186
|
description: "Potential issue: messages from unexpected source chains may be accepted.",
|
|
151
187
|
risk: "Cross-chain spoofing or misrouted messages can trigger unauthorized state changes.",
|
|
@@ -168,7 +204,7 @@ export const ccipRules = [
|
|
|
168
204
|
if (isCcipBaseReceiver(context.content))
|
|
169
205
|
return [];
|
|
170
206
|
const body = ccipBody(context.content) || context.content;
|
|
171
|
-
const validationContent = `${ccipHeader(context.content)}\n${delegatedCcipContent(body, context.repoSignals)}`;
|
|
207
|
+
const validationContent = `${ccipHeader(context.content)}\n${delegatedCcipContent(body, context.repoSignals)}\n${resolveModifierBodies(context.content, context.repoSignals)}`;
|
|
172
208
|
if (validatesSourceSender(context.content, validationContent))
|
|
173
209
|
return [];
|
|
174
210
|
return [
|
|
@@ -177,7 +213,7 @@ export const ccipRules = [
|
|
|
177
213
|
ruleId: this.metadata.ruleId,
|
|
178
214
|
severity: this.metadata.severity,
|
|
179
215
|
confidence: "medium",
|
|
180
|
-
line: firstLineMatching(context.lines,
|
|
216
|
+
line: firstLineMatching(context.lines, CCIP_RECEIVE_DECL),
|
|
181
217
|
title: this.metadata.title,
|
|
182
218
|
description: "Potential issue: messages from untrusted source contracts may be accepted.",
|
|
183
219
|
risk: "An attacker may spoof business instructions if sender validation is missing or incomplete.",
|
|
@@ -210,7 +246,7 @@ export const ccipRules = [
|
|
|
210
246
|
ruleId: this.metadata.ruleId,
|
|
211
247
|
severity: this.metadata.severity,
|
|
212
248
|
confidence: "medium",
|
|
213
|
-
line: firstLineMatching(context.lines,
|
|
249
|
+
line: firstLineMatching(context.lines, CCIP_RECEIVE_DECL),
|
|
214
250
|
title: this.metadata.title,
|
|
215
251
|
description: "Potential issue: direct calls to the receiver may bypass CCIP router trust assumptions.",
|
|
216
252
|
risk: "If the entrypoint is public/external and router validation is absent, fabricated messages may be processed.",
|
|
@@ -238,6 +274,10 @@ export const ccipRules = [
|
|
|
238
274
|
const defensiveChecks = /(message\.data\.length|try\s+this|catch|InvalidPayload|Payload|version|schema)/i.test(body);
|
|
239
275
|
if (defensiveChecks)
|
|
240
276
|
return [];
|
|
277
|
+
// A trusted, validated sender already constrains the payload to the expected schema.
|
|
278
|
+
const validationContent = `${ccipHeader(context.content)}\n${delegatedCcipContent(body, context.repoSignals)}\n${resolveModifierBodies(context.content, context.repoSignals)}`;
|
|
279
|
+
if (validatesSourceSender(context.content, validationContent))
|
|
280
|
+
return [];
|
|
241
281
|
return [
|
|
242
282
|
makeFinding({
|
|
243
283
|
context,
|
|
@@ -279,7 +319,7 @@ export const ccipRules = [
|
|
|
279
319
|
ruleId: this.metadata.ruleId,
|
|
280
320
|
severity: this.metadata.severity,
|
|
281
321
|
confidence: "low",
|
|
282
|
-
line: firstLineMatching(context.lines,
|
|
322
|
+
line: firstLineMatching(context.lines, CCIP_RECEIVE_DECL),
|
|
283
323
|
title: this.metadata.title,
|
|
284
324
|
description: "Potential issue: receiver business logic appears coupled to CCIP execution without a visible retry/manual path.",
|
|
285
325
|
risk: "A revert in message handling may require manual execution or leave cross-chain state inconsistent.",
|
|
@@ -351,7 +391,7 @@ export const ccipRules = [
|
|
|
351
391
|
ruleId: this.metadata.ruleId,
|
|
352
392
|
severity: this.metadata.severity,
|
|
353
393
|
confidence: "low",
|
|
354
|
-
line: firstLineMatching(context.lines,
|
|
394
|
+
line: firstLineMatching(context.lines, CCIP_RECEIVE_DECL),
|
|
355
395
|
title: this.metadata.title,
|
|
356
396
|
description: "Potential issue: repeated or retried CCIP messages may execute mutating business logic more than once.",
|
|
357
397
|
risk: "Without idempotency tracking, replay-like operational scenarios or manual execution flows can duplicate state changes.",
|
package/dist/rules/data-feeds.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { firstLineMatching, hasAny, makeFinding } from "./helpers.js";
|
|
2
|
+
const AGGREGATOR_WRAPPER_PATTERN = /function\s+latestRoundData\s*\([^)]*\)\s*(?:external|public|view|override|virtual|\s)*returns\s*\(\s*uint80\s*,\s*int256\s*,\s*uint256\s*,\s*uint256\s*,\s*uint80\s*\)\s*\{/s;
|
|
3
|
+
function isAggregatorWrapper(content) {
|
|
4
|
+
return AGGREGATOR_WRAPPER_PATTERN.test(content);
|
|
5
|
+
}
|
|
2
6
|
export const dataFeedRules = [
|
|
3
7
|
{
|
|
4
8
|
metadata: {
|
|
@@ -11,6 +15,8 @@ export const dataFeedRules = [
|
|
|
11
15
|
scan(context) {
|
|
12
16
|
if (!/\.latestRoundData\s*\(/.test(context.content))
|
|
13
17
|
return [];
|
|
18
|
+
if (isAggregatorWrapper(context.content))
|
|
19
|
+
return [];
|
|
14
20
|
const hasFreshnessCheck = /updatedAt/.test(context.content) &&
|
|
15
21
|
/(block\.timestamp|maxStaleness|stale|heartbeat|updatedAt\s*!=\s*0)/i.test(context.content);
|
|
16
22
|
if (hasFreshnessCheck)
|
|
@@ -41,11 +47,18 @@ export const dataFeedRules = [
|
|
|
41
47
|
scan(context) {
|
|
42
48
|
if (!/\.latestRoundData\s*\(/.test(context.content))
|
|
43
49
|
return [];
|
|
50
|
+
if (isAggregatorWrapper(context.content))
|
|
51
|
+
return [];
|
|
44
52
|
const hasPositiveAnswerCheck = hasAny(context.content, [
|
|
45
|
-
/answer\s
|
|
46
|
-
/price\s
|
|
47
|
-
/oracleAnswer\s
|
|
53
|
+
/answer\s*>=?\s*0/,
|
|
54
|
+
/price\s*>=?\s*0/,
|
|
55
|
+
/oracleAnswer\s*>=?\s*0/,
|
|
48
56
|
/InvalidOracleAnswer/,
|
|
57
|
+
/answer\s*<=?\s*0/,
|
|
58
|
+
/price\s*<=?\s*0/,
|
|
59
|
+
/oracleAnswer\s*<=?\s*0/,
|
|
60
|
+
/Invalid(?:Oracle)?(?:Price|Answer|Round)/i,
|
|
61
|
+
/\.\s*minAnswer\s*\(\s*\)/,
|
|
49
62
|
]);
|
|
50
63
|
if (hasPositiveAnswerCheck)
|
|
51
64
|
return [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { firstLineMatching, hasAny, makeFinding } from "./helpers.js";
|
|
2
2
|
function hasFunctions(content) {
|
|
3
|
-
return /(FunctionsClient|FunctionsRequest|
|
|
3
|
+
return /(FunctionsClient|FunctionsRequest|\bsendRequest\s*\(|\bfulfillRequest\s*\(|DONHostedSecrets|sourceCode|secrets)/i.test(content);
|
|
4
4
|
}
|
|
5
5
|
export const functionsCreRules = [
|
|
6
6
|
{
|
package/dist/rules/helpers.js
CHANGED
|
@@ -5,8 +5,11 @@ export function firstLineMatching(lines, pattern) {
|
|
|
5
5
|
export function hasAny(content, patterns) {
|
|
6
6
|
return patterns.some((pattern) => pattern.test(content));
|
|
7
7
|
}
|
|
8
|
-
export function
|
|
9
|
-
|
|
8
|
+
export function escapeRegExp(value) {
|
|
9
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10
|
+
}
|
|
11
|
+
export function extractBlockBody(content, declarationPattern) {
|
|
12
|
+
const match = declarationPattern.exec(content);
|
|
10
13
|
if (!match)
|
|
11
14
|
return "";
|
|
12
15
|
const open = content.indexOf("{", match.index);
|
|
@@ -24,9 +27,18 @@ export function extractFunctionBody(content, functionName) {
|
|
|
24
27
|
}
|
|
25
28
|
return content.slice(open + 1);
|
|
26
29
|
}
|
|
30
|
+
export function extractFunctionBody(content, functionName) {
|
|
31
|
+
return extractBlockBody(content, new RegExp(`function\\s+${functionName}\\b`));
|
|
32
|
+
}
|
|
27
33
|
export function countMatches(content, patterns) {
|
|
28
34
|
return patterns.reduce((count, pattern) => count + (pattern.test(content) ? 1 : 0), 0);
|
|
29
35
|
}
|
|
36
|
+
export function isInterfaceOnlyFile(content) {
|
|
37
|
+
const declarations = [...content.matchAll(/\b(?:abstract\s+)?(contract|library|interface)\s+\w+(?:\s+is\s+[^{]+)?\s*\{/g)];
|
|
38
|
+
if (declarations.length === 0)
|
|
39
|
+
return false;
|
|
40
|
+
return declarations.every((match) => match[1] === "interface");
|
|
41
|
+
}
|
|
30
42
|
export function makeFinding(input) {
|
|
31
43
|
return {
|
|
32
44
|
ruleId: input.ruleId,
|
package/dist/rules/vrf.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { countMatches, extractFunctionBody, firstLineMatching, makeFinding } from "./helpers.js";
|
|
1
|
+
import { countMatches, extractFunctionBody, firstLineMatching, isInterfaceOnlyFile, makeFinding } from "./helpers.js";
|
|
2
2
|
function hasVrf(content) {
|
|
3
3
|
return /(VRFConsumerBase|VRFCoordinator|function\s+fulfillRandomWords\b|requestRandomWords)/.test(content);
|
|
4
4
|
}
|
|
@@ -78,6 +78,8 @@ export const vrfRules = [
|
|
|
78
78
|
scan(context) {
|
|
79
79
|
if (!/requestRandomWords\s*\(/.test(context.content))
|
|
80
80
|
return [];
|
|
81
|
+
if (isInterfaceOnlyFile(context.content))
|
|
82
|
+
return [];
|
|
81
83
|
const tracksBeforeFulfillment = /(pendingRequests|requestStatus|requests)\s*\[.*\]\s*=/.test(context.content);
|
|
82
84
|
if (tracksBeforeFulfillment)
|
|
83
85
|
return [];
|
package/dist/scanner.js
CHANGED
|
@@ -60,7 +60,16 @@ function isExcluded(fileOrDirectory, scanRoot, config) {
|
|
|
60
60
|
async function collectSolidityFiles(targetPath, scanRoot, config) {
|
|
61
61
|
if (isExcluded(targetPath, scanRoot, config))
|
|
62
62
|
return [];
|
|
63
|
-
|
|
63
|
+
let stat;
|
|
64
|
+
try {
|
|
65
|
+
stat = await fs.stat(targetPath);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
69
|
+
if (code === "ENOENT" || code === "ELOOP")
|
|
70
|
+
return [];
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
64
73
|
if (stat.isFile()) {
|
|
65
74
|
if (!targetPath.endsWith(".sol"))
|
|
66
75
|
return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chainlink-audit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Security review CLI for flagging unverified Chainlink integration risk leads in Solidity repositories.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"build": "tsc -p tsconfig.json",
|
|
42
42
|
"test": "vitest run",
|
|
43
43
|
"dev": "tsx src/index.ts",
|
|
44
|
+
"benchmark:ecosystem": "tsx scripts/benchmark-ecosystem.ts",
|
|
44
45
|
"prepack": "npm run build"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|