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.
@@ -0,0 +1,133 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { defaultConfig, loadConfig, severityRank } from "./config.js";
4
+ import { rules } from "./rules/index.js";
5
+ const ignoredDirectories = new Set([
6
+ ".git",
7
+ "node_modules",
8
+ "out",
9
+ "cache",
10
+ "broadcast",
11
+ "artifacts",
12
+ "dist",
13
+ "build",
14
+ ]);
15
+ function normalizePath(value) {
16
+ return value.split(path.sep).join("/");
17
+ }
18
+ function escapeRegExp(value) {
19
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20
+ }
21
+ function globToRegExp(pattern) {
22
+ const normalized = normalizePath(pattern);
23
+ let source = "";
24
+ for (let index = 0; index < normalized.length; index++) {
25
+ const char = normalized[index];
26
+ const next = normalized[index + 1];
27
+ if (char === "*" && next === "*") {
28
+ source += ".*";
29
+ index++;
30
+ }
31
+ else if (char === "*") {
32
+ source += "[^/]*";
33
+ }
34
+ else {
35
+ source += escapeRegExp(char);
36
+ }
37
+ }
38
+ return new RegExp(`(^|/)${source}($|/)`);
39
+ }
40
+ function matchesExclude(relativePath, pattern) {
41
+ const normalizedPath = normalizePath(relativePath).replace(/^\/+/, "");
42
+ const normalizedPattern = normalizePath(pattern).replace(/^\/+/, "");
43
+ if (normalizedPattern.length === 0)
44
+ return false;
45
+ if (normalizedPattern.includes("*")) {
46
+ return globToRegExp(normalizedPattern).test(normalizedPath);
47
+ }
48
+ const directoryPattern = normalizedPattern.endsWith("/")
49
+ ? normalizedPattern.slice(0, -1)
50
+ : normalizedPattern;
51
+ return (normalizedPath === directoryPattern ||
52
+ normalizedPath.startsWith(`${directoryPattern}/`) ||
53
+ normalizedPath.includes(`/${directoryPattern}/`));
54
+ }
55
+ function isExcluded(fileOrDirectory, scanRoot, config) {
56
+ const relativePath = path.relative(scanRoot, fileOrDirectory);
57
+ const normalized = normalizePath(relativePath || path.basename(fileOrDirectory));
58
+ return config.exclude.some((pattern) => matchesExclude(normalized, pattern));
59
+ }
60
+ async function collectSolidityFiles(targetPath, scanRoot, config) {
61
+ if (isExcluded(targetPath, scanRoot, config))
62
+ return [];
63
+ const stat = await fs.stat(targetPath);
64
+ if (stat.isFile()) {
65
+ if (!targetPath.endsWith(".sol"))
66
+ return [];
67
+ return [targetPath];
68
+ }
69
+ const entries = await fs.readdir(targetPath, { withFileTypes: true });
70
+ const nested = await Promise.all(entries
71
+ .filter((entry) => !(entry.isDirectory() && ignoredDirectories.has(entry.name)))
72
+ .filter((entry) => !entry.name.startsWith(".") || entry.name === ".")
73
+ .map((entry) => collectSolidityFiles(path.join(targetPath, entry.name), scanRoot, config)));
74
+ return nested.flat();
75
+ }
76
+ function detectProducts(files) {
77
+ const products = new Set();
78
+ const allContent = files.map((file) => file.content).join("\n");
79
+ if (/(AggregatorV3Interface|latestRoundData|IChainlinkAggregator)/.test(allContent))
80
+ products.add("data-feeds");
81
+ if (/(CCIPReceiver|Any2EVMMessage|IRouterClient|_ccipReceive|ccipReceive|sourceChainSelector)/.test(allContent))
82
+ products.add("ccip");
83
+ if (/(VRFConsumerBase|VRFCoordinator|fulfillRandomWords|requestRandomWords)/.test(allContent))
84
+ products.add("vrf");
85
+ if (/(AutomationCompatibleInterface|KeeperCompatibleInterface|checkUpkeep|performUpkeep)/.test(allContent))
86
+ products.add("automation");
87
+ if (/(FunctionsClient|FunctionsRequest|DONHostedSecrets|sendRequest|fulfillRequest)/i.test(allContent))
88
+ products.add("functions-cre");
89
+ if (/(DataStreams|StreamsLookup|ILogAutomation|VerifierProxy|reportContext|ReportV3|BasicReport|websocket)/i.test(allContent))
90
+ products.add("data-streams");
91
+ return products;
92
+ }
93
+ function detectL2Target(files) {
94
+ const combined = files.map((file) => `${file.file}\n${file.content}`).join("\n");
95
+ return /(\barbitrum\b|\boptimism\b|\bbase\b|\bpolygon\b|\bzksync\b|\bscroll\b|\blinea\b|\bmantle\b|\bblast\b|sepolia.*\bbase\b|\bl2\b)/i.test(combined);
96
+ }
97
+ export async function scanPath(targetPath, options = {}) {
98
+ const absoluteTarget = path.resolve(targetPath);
99
+ const config = options.config ?? await loadConfig(absoluteTarget);
100
+ const scanRoot = (await fs.stat(absoluteTarget)).isFile() ? path.dirname(absoluteTarget) : absoluteTarget;
101
+ const solidityFiles = await collectSolidityFiles(absoluteTarget, scanRoot, config);
102
+ const files = await Promise.all(solidityFiles.map(async (file) => ({
103
+ file: path.relative(process.cwd(), file) || file,
104
+ content: await fs.readFile(file, "utf8"),
105
+ })));
106
+ const repoSignals = {
107
+ products: detectProducts(files),
108
+ targetsL2: detectL2Target(files),
109
+ files,
110
+ };
111
+ const findings = [];
112
+ for (const file of files) {
113
+ const lines = file.content.split(/\r?\n/);
114
+ for (const rule of rules) {
115
+ findings.push(...rule.scan({ file: file.file, content: file.content, lines, repoSignals }));
116
+ }
117
+ }
118
+ const minSeverityRank = severityRank(config.minSeverity ?? defaultConfig.minSeverity);
119
+ const filteredFindings = findings.filter((finding) => severityRank(finding.severity) >= minSeverityRank);
120
+ filteredFindings.sort((a, b) => {
121
+ const fileCompare = a.file.localeCompare(b.file);
122
+ if (fileCompare !== 0)
123
+ return fileCompare;
124
+ return a.line - b.line || a.ruleId.localeCompare(b.ruleId);
125
+ });
126
+ return {
127
+ targetPath,
128
+ scannedFiles: files.length,
129
+ products: [...repoSignals.products].sort(),
130
+ findings: filteredFindings,
131
+ config,
132
+ };
133
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "chainlink-audit",
3
+ "version": "0.1.0",
4
+ "description": "Security CLI for detecting potential Chainlink integration risks in Solidity repositories.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "chainlink",
9
+ "solidity",
10
+ "security",
11
+ "audit",
12
+ "web3",
13
+ "cli",
14
+ "foundry",
15
+ "ccip",
16
+ "vrf",
17
+ "automation",
18
+ "oracle"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/alva-p/chainlink-integration-audit-kit.git",
23
+ "directory": "packages/cli"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/alva-p/chainlink-integration-audit-kit/issues"
27
+ },
28
+ "homepage": "https://github.com/alva-p/chainlink-integration-audit-kit#readme",
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "bin": {
38
+ "chainlink-audit": "dist/index.js"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "test": "vitest run",
43
+ "dev": "tsx src/index.ts",
44
+ "prepack": "npm run build"
45
+ },
46
+ "dependencies": {
47
+ "commander": "^12.1.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.10.2",
51
+ "tsx": "^4.19.2",
52
+ "typescript": "^5.7.2",
53
+ "vitest": "^4.1.8"
54
+ },
55
+ "engines": {
56
+ "node": ">=20"
57
+ }
58
+ }