UNPKG

solidityscan

Version:

SolidityScan is a tool for scanning Solidity smart contracts for vulnerabilities.

531 lines 23.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.displayScanSummary = exports.displayScanResults = exports.uploadToS3 = exports.getUploadPresignedUrl = exports.createProjectZip = exports.initializeWebSocket = void 0; exports.showSpinnerWithStatus = showSpinnerWithStatus; exports.stopSpinner = stopSpinner; exports.startLocalFileServer = startLocalFileServer; const request_1 = __importDefault(require("request")); const ws_1 = __importDefault(require("ws")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const archiver_1 = __importDefault(require("archiver")); const axios_1 = __importDefault(require("axios")); const cli_table3_1 = __importDefault(require("cli-table3")); const cli_spinners_1 = __importDefault(require("cli-spinners")); const spinner = cli_spinners_1.default.dots; const getApi = (apiToken) => { const apiBaseUrl = "https://api.solidityscan.com/"; if (apiToken) { const instance = axios_1.default.create({ baseURL: apiBaseUrl, headers: { "Content-Type": "application/json", "accept": "application/json, text/plain, */*", "Authorization": `Bearer ${apiToken}`, "cache-control": "no-cache", } }); return instance; } else { const instance = axios_1.default.create({ baseURL: apiBaseUrl, headers: { "Content-Type": "application/json", "CF-Access-Client-Secret": "", "CF-Access-Client-Id": "", } }); return instance; } }; const initializeWebSocket = (apiToken, payload, spinner = true) => { const wsUrl = 'wss://api-ws.solidityscan.com/'; const ws = new ws_1.default(wsUrl, { rejectUnauthorized: false }); const emitMessage = (messagePayload) => { ws.send(JSON.stringify({ action: "message", payload: messagePayload, })); }; return new Promise((resolve, reject) => { const connectionTimeout = setTimeout(() => { ws.close(); reject(new Error("WebSocket connection timed out waiting for scan results")); }, 60000); // 60 second timeout ws.on("open", () => { if (apiToken) { emitMessage({ type: "auth_token_register", body: { auth_token: apiToken, }, }); } else { console.log("No authentication token provided, sending payload directly"); emitMessage(payload); } }); ws.on("message", (data) => { try { const receivedMessage = JSON.parse(data.toString()); clearTimeout(connectionTimeout); if (receivedMessage.type === "auth_token_register") { if (payload.payload) { emitMessage(payload.payload); } else { emitMessage(payload); } } else if (receivedMessage.type === "scan_status") { if (receivedMessage.payload?.scan_status === "scan_done") { resolve(receivedMessage.payload); ws.close(); } } else if (receivedMessage.type === "quick_scan_status") { if (receivedMessage.payload?.scan_status === "scan_done") { resolve(receivedMessage.payload); ws.close(); } else { console.log(`\n[WebSocket] Waiting for scan to complete. Current status: ${receivedMessage.payload?.scan_status || receivedMessage.payload?.quick_scan_status || 'processing'}`); } } else if (receivedMessage.type === "report_generation_status") { if (receivedMessage.payload?.report_status === "report_generated") { resolve(receivedMessage.payload); ws.close(); } else { console.log(`\n[WebSocket] Report generation payload: ${receivedMessage.payload}`); } } else if (receivedMessage.type === "quick_scan_result") { if (receivedMessage.payload?.scan_details?.link) { request_1.default.get(receivedMessage.payload.scan_details.link, (error, response, body) => { if (error) { resolve(error); } else if (response.statusCode !== 200) { resolve(error); } else { try { const scan_result = JSON.parse(body); resolve(scan_result.scan_report || scan_result); } catch (e) { resolve(body); } } ws.close(); }); } } else if (receivedMessage.type === "error") { console.log("\n Error received from server:", receivedMessage.payload?.payload?.error_message || receivedMessage.payload?.error_message || "Unknown error"); ws.close(); reject(receivedMessage.payload?.payload?.error_message || receivedMessage.payload?.error_message || "Unknown error from server"); } else { if (spinner) { process.stdout.write("."); } } } catch (error) { console.error("\nError processing message:", error); console.error("\nRaw message data:", data.toString()); } }); ws.on("error", (error) => { console.log(error); reject(error); }); ws.on("close", () => { }); }); }; exports.initializeWebSocket = initializeWebSocket; const createProjectZip = async (projectDirectory) => { try { const zipFileName = "project.zip"; const output = fs_1.default.createWriteStream(zipFileName); const archive = (0, archiver_1.default)("zip", { zlib: { level: 9 } }); archive.pipe(output); const gatherSolFiles = (directory) => { const files = fs_1.default.readdirSync(directory); files.forEach((file) => { const filePath = path_1.default.join(directory, file); if (fs_1.default.statSync(filePath).isDirectory() && file !== "node_modules") { gatherSolFiles(filePath); } else if (path_1.default.extname(file) === ".sol") { const relativePath = path_1.default.relative(projectDirectory, filePath); const fileContent = fs_1.default.readFileSync(filePath); archive.append(fileContent, { name: relativePath }); } }); }; gatherSolFiles(projectDirectory); await archive.finalize(); return zipFileName; } catch (error) { throw new Error(`Error creating project ZIP: ${error.message}`); } }; exports.createProjectZip = createProjectZip; const getUploadPresignedUrl = async (fileName, apiToken) => { try { const apiUrl = `private/api-get-presigned-url/?file_name=${fileName}`; const API = getApi(apiToken); const response = await API.get(apiUrl); if (response.status === 200 && response.data && response.data.result) { return response.data.result.url; } else { return null; } } catch (error) { throw new Error(`Failed to get presigned URL: ${error.message}`); } }; exports.getUploadPresignedUrl = getUploadPresignedUrl; const uploadToS3 = async (fileData, uploadUrl) => { try { const response = await axios_1.default.put(uploadUrl, fileData, { headers: { "Content-Type": "application/octet-stream", } }); if (response.status === 200 || response.status === 204) { return true; } else { return false; } } catch (error) { return false; } }; exports.uploadToS3 = uploadToS3; function capitalizeFirstLetter(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function formatHtmlForTerminal(htmlContent) { if (!htmlContent) return ''; let text = htmlContent.replace(/<br\s*\/>/gi, '\n'); text = text.replace(/<code>(.*?)<\/code>/gi, '`$1`'); text = text.replace(/<\/?[^>]+(>|$)/g, ''); text = text.replace(/\s+/g, ' ').trim(); return text; } const displayScanResults = (scan) => { const table = new cli_table3_1.default({ head: ["#", "NAME", "SEVERITY", "CONFIDENCE", "DESCRIPTION", "REMEDIATION"], chars: { 'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐', 'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘', 'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼', 'right': '│', 'right-mid': '┤', 'middle': '│' }, style: { head: ['bold'] }, // Configure optimal column widths colWidths: [5, 20, 12, 12, 35, 35], wordWrap: true }); let issueCount = 0; scan.multi_file_scan_details.forEach((detail) => { const { template_details } = detail; if (detail.metric_wise_aggregated_findings) { detail.metric_wise_aggregated_findings.forEach((bug) => { issueCount++; const filePath = bug.findings[0].file_path; const location = `${filePath.replace("/project", "")}\nL${bug.findings[0].line_nos_start} - L${bug.findings[0].line_nos_end}`; const description = formatHtmlForTerminal(template_details.issue_description); const fullDescription = `${description}\n\nLocation:\n${location}`; const row = [ `${issueCount}.`, template_details.issue_name, capitalizeFirstLetter(template_details.issue_severity), template_details.issue_confidence, fullDescription, formatHtmlForTerminal(template_details.issue_remediation) ]; table.push(row); }); } }); if (issueCount === 0) { console.log('No security issues found!'); } else { console.log('SECURITY SCAN RESULTS:'); console.log(table.toString()); console.log(`Found ${issueCount} security ${issueCount === 1 ? 'issue' : 'issues'}.`); } }; exports.displayScanResults = displayScanResults; const displayScanSummary = (scan) => { const table = new cli_table3_1.default(); const issues_count = scan.multi_file_scan_summary.issue_severity_distribution.critical + scan.multi_file_scan_summary.issue_severity_distribution.high + scan.multi_file_scan_summary.issue_severity_distribution.medium + scan.multi_file_scan_summary.issue_severity_distribution.low + scan.multi_file_scan_summary.issue_severity_distribution.informational + scan.multi_file_scan_summary.issue_severity_distribution.gas; table.push({ Critical: scan.multi_file_scan_summary.issue_severity_distribution.critical, }, { High: scan.multi_file_scan_summary.issue_severity_distribution.high }, { Medium: scan.multi_file_scan_summary.issue_severity_distribution.medium, }, { Low: scan.multi_file_scan_summary.issue_severity_distribution.low }, { Informational: scan.multi_file_scan_summary.issue_severity_distribution.informational, }, { Gas: scan.multi_file_scan_summary.issue_severity_distribution.gas }, { "Security Score": `${scan.multi_file_scan_summary.score_v2} / 100` }); console.log(table.toString()); console.log(`Scan successful! ${issues_count} issues found. To view detailed results and generate a report navigate to solidityscan.com.`); }; exports.displayScanSummary = displayScanSummary; // Function to display a spinner with dynamic status async function showSpinnerWithStatus(statusMessage, spinnerFrames) { process.stdout.write(`${statusMessage}... `); let frameIndex = 0; const interval = setInterval(() => { process.stdout.write(spinnerFrames[frameIndex]); process.stdout.write("\b"); frameIndex = (frameIndex + 1) % spinnerFrames.length; }, 100); return interval; } // Function to stop the spinner function stopSpinner(interval, statusMessage) { clearInterval(interval); process.stdout.write("\r"); console.log(`${statusMessage}... Done`); } // New helper to serve local directory over WebSocket function startLocalFileServer(rootDirectory, port = 8080) { if (!fs_1.default.existsSync(rootDirectory)) { throw new Error(`Directory not found: ${rootDirectory}`); } const absoluteRoot = path_1.default.resolve(rootDirectory); const wss = new ws_1.default.Server({ port, verifyClient: (info, done) => { if (!originIsAllowed(info.origin)) { done(false); console.log(`Connection from origin ${info.origin} is not allowed`); return; } done(true); } }); console.log(`SolidityScan local file server started\nServing directory: ${absoluteRoot}`); wss.on("connection", (socket) => { socket.on("message", async (raw) => { let message; try { message = JSON.parse(raw); } catch (err) { socket.send(JSON.stringify({ type: "error", error: "Invalid JSON message" })); return; } const { action, payload } = message; if (action === "listFiles") { // Return hierarchical folder tree with metadata const buildTree = (dir, relPath = "") => { const name = path_1.default.basename(dir); const stat = fs_1.default.statSync(dir); if (stat.isDirectory()) { const dirs = []; const files = []; fs_1.default.readdirSync(dir).forEach((entry) => { if (entry === "node_modules") return; const abs = path_1.default.join(dir, entry); const rootName = path_1.default.basename(absoluteRoot); // Build a raw relative path using native separators let childRelRaw; if (relPath === "") { childRelRaw = path_1.default.join(rootName, entry); } else { childRelRaw = path_1.default.join(relPath, entry); } // Convert to POSIX style with a single "/" separator for JSON responses const childRel = childRelRaw .split(path_1.default.sep) .join("/") .replace(/\/+/g, "/"); if (fs_1.default.statSync(abs).isDirectory()) { // Recurse with the raw path to preserve correct joining behaviour dirs.push(buildTree(abs, childRelRaw + path_1.default.sep)); } else { const fStat = fs_1.default.statSync(abs); files.push({ path: childRel, name: entry, size: fStat.size, mtimeMs: fStat.mtimeMs, checked: entry.endsWith(".sol"), }); } }); // Determine checked / isChildCheck flags const numSol = files.filter((f) => f.checked).length; const numNonSol = files.length - numSol; let checkedDir = false; let isChildCheck = false; if (numSol === 0) { checkedDir = false; isChildCheck = false; } else if (numNonSol === 0) { checkedDir = true; isChildCheck = true; } else { checkedDir = false; isChildCheck = true; } // Build directory path (raw) and then normalise to POSIX let dirPathRaw; if (dir === absoluteRoot) { dirPathRaw = path_1.default.basename(absoluteRoot) + path_1.default.sep; } else { dirPathRaw = relPath + (relPath && !relPath.endsWith(path_1.default.sep) ? path_1.default.sep : ""); } let dirPath = dirPathRaw .split(path_1.default.sep) .join("/") .replace(/\/+/g, "/"); // Ensure directory paths end with a single trailing slash if (!dirPath.endsWith("/")) { dirPath += "/"; } return { name, path: dirPath, tree: dirs, isChildCheck, checked: checkedDir, blobs: files, size: 0, mtimeMs: stat.mtimeMs, }; } // Should not reach here for files as we handle in parent return undefined; }; const rootTreeInternal = buildTree(absoluteRoot, ""); const responseTree = { name: "", path: "", tree: [rootTreeInternal], isChildCheck: rootTreeInternal.isChildCheck, checked: rootTreeInternal.checked, blobs: [], size: 0, mtimeMs: rootTreeInternal.mtimeMs, }; socket.send(JSON.stringify({ type: "folderStructure", tree: responseTree })); } else if (action === "zipAndSendFiles") { const presignedUrl = payload.presigned_url; if (!originIsAllowed(payload.origin)) { socket.send(JSON.stringify({ type: "error", error: "origin not allowed" })); return; } const skip = new Set(payload.skip_file_paths || []); if (!presignedUrl) { socket.send(JSON.stringify({ type: "error", error: "presigned_url missing" })); return; } const archive = (0, archiver_1.default)("zip", { zlib: { level: 9 } }); const chunks = []; archive.on("data", (chunk) => chunks.push(chunk)); archive.on("warning", (err) => { if (err.code !== "ENOENT") { socket.send(JSON.stringify({ type: "error", error: err.message })); } }); archive.on("error", (err) => { socket.send(JSON.stringify({ type: "error", error: err.message })); }); archive.on("end", async () => { const buffer = Buffer.concat(chunks); let success = false; try { if (presignedUrl.startsWith("memory://")) { // test stub success = true; } else { success = await uploadToS3(buffer, presignedUrl); } } catch (e) { console.log("error uploading file", e); success = false; } socket.send(JSON.stringify({ type: "uploadStatus", success, })); }); // recursively walk and add files not skipped const walkAdd = (dir, rel = "") => { fs_1.default.readdirSync(dir).forEach((entry) => { if (entry === "node_modules") return; const abs = path_1.default.join(dir, entry); const relPath = path_1.default.join(rel, entry); const relPathPosix = relPath .split(path_1.default.sep) .join("/") .replace(/\/+/g, "/"); const stat = fs_1.default.statSync(abs); if (stat.isDirectory()) { walkAdd(abs, relPath); } else { if (!skip.has(relPathPosix)) { // Use POSIX-style path inside the archive to avoid platform-specific separators archive.file(abs, { name: relPathPosix }); } } }); }; walkAdd(absoluteRoot, ""); archive.finalize(); } else { socket.send(JSON.stringify({ type: "error", error: "Unknown action" })); } }); }); return wss; } function getDomain(url) { const domainMatch = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:\/\n?]+)/img); return domainMatch ? domainMatch[0] : null; } function originIsAllowed(origin) { return true; const DOMAIN = getDomain(origin) || ""; const allowedOrigins = ["https://solidityscan.com", "https://develop.solidityscan.com", "https://credshields-prod.s3.amazonaws.com", "https://credshields-dev.s3.amazonaws.com/"]; return allowedOrigins.includes(DOMAIN); } //# sourceMappingURL=utils.js.map