UNPKG

modality-safe

Version:

Advanced security scanner that detects API key leaks and sensitive information in source code. Scans TypeScript, JavaScript, Markdown, and configuration files for AWS keys, OpenAI tokens, GitHub/GitLab PATs, Slack/Discord tokens, JWT tokens, and other cre

510 lines (507 loc) 15.5 kB
// src/index.ts var {readdir, readFile, writeFile} = (() => ({})); // node:path function assertPath(path) { if (typeof path !== "string") throw new TypeError("Path must be a string. Received " + JSON.stringify(path)); } function normalizeStringPosix(path, allowAboveRoot) { var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code; for (var i = 0;i <= path.length; ++i) { if (i < path.length) code = path.charCodeAt(i); else if (code === 47) break; else code = 47; if (code === 47) { if (lastSlash === i - 1 || dots === 1) ; else if (lastSlash !== i - 1 && dots === 2) { if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) { if (res.length > 2) { var lastSlashIndex = res.lastIndexOf("/"); if (lastSlashIndex !== res.length - 1) { if (lastSlashIndex === -1) res = "", lastSegmentLength = 0; else res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); lastSlash = i, dots = 0; continue; } } else if (res.length === 2 || res.length === 1) { res = "", lastSegmentLength = 0, lastSlash = i, dots = 0; continue; } } if (allowAboveRoot) { if (res.length > 0) res += "/.."; else res = ".."; lastSegmentLength = 2; } } else { if (res.length > 0) res += "/" + path.slice(lastSlash + 1, i); else res = path.slice(lastSlash + 1, i); lastSegmentLength = i - lastSlash - 1; } lastSlash = i, dots = 0; } else if (code === 46 && dots !== -1) ++dots; else dots = -1; } return res; } function _format(sep, pathObject) { var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || ""); if (!dir) return base; if (dir === pathObject.root) return dir + base; return dir + sep + base; } function resolve() { var resolvedPath = "", resolvedAbsolute = false, cwd; for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) { var path; if (i >= 0) path = arguments[i]; else { if (cwd === undefined) cwd = process.cwd(); path = cwd; } if (assertPath(path), path.length === 0) continue; resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47; } if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute) if (resolvedPath.length > 0) return "/" + resolvedPath; else return "/"; else if (resolvedPath.length > 0) return resolvedPath; else return "."; } function normalize(path) { if (assertPath(path), path.length === 0) return "."; var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47; if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute) path = "."; if (path.length > 0 && trailingSeparator) path += "/"; if (isAbsolute) return "/" + path; return path; } function isAbsolute(path) { return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47; } function join() { if (arguments.length === 0) return "."; var joined; for (var i = 0;i < arguments.length; ++i) { var arg = arguments[i]; if (assertPath(arg), arg.length > 0) if (joined === undefined) joined = arg; else joined += "/" + arg; } if (joined === undefined) return "."; return normalize(joined); } function relative(from, to) { if (assertPath(from), assertPath(to), from === to) return ""; if (from = resolve(from), to = resolve(to), from === to) return ""; var fromStart = 1; for (;fromStart < from.length; ++fromStart) if (from.charCodeAt(fromStart) !== 47) break; var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1; for (;toStart < to.length; ++toStart) if (to.charCodeAt(toStart) !== 47) break; var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0; for (;i <= length; ++i) { if (i === length) { if (toLen > length) { if (to.charCodeAt(toStart + i) === 47) return to.slice(toStart + i + 1); else if (i === 0) return to.slice(toStart + i); } else if (fromLen > length) { if (from.charCodeAt(fromStart + i) === 47) lastCommonSep = i; else if (i === 0) lastCommonSep = 0; } break; } var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i); if (fromCode !== toCode) break; else if (fromCode === 47) lastCommonSep = i; } var out = ""; for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i) if (i === fromEnd || from.charCodeAt(i) === 47) if (out.length === 0) out += ".."; else out += "/.."; if (out.length > 0) return out + to.slice(toStart + lastCommonSep); else { if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47) ++toStart; return to.slice(toStart); } } function _makeLong(path) { return path; } function dirname(path) { if (assertPath(path), path.length === 0) return "."; var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true; for (var i = path.length - 1;i >= 1; --i) if (code = path.charCodeAt(i), code === 47) { if (!matchedSlash) { end = i; break; } } else matchedSlash = false; if (end === -1) return hasRoot ? "/" : "."; if (hasRoot && end === 1) return "//"; return path.slice(0, end); } function basename(path, ext) { if (ext !== undefined && typeof ext !== "string") throw new TypeError('"ext" argument must be a string'); assertPath(path); var start = 0, end = -1, matchedSlash = true, i; if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { if (ext.length === path.length && ext === path) return ""; var extIdx = ext.length - 1, firstNonSlashEnd = -1; for (i = path.length - 1;i >= 0; --i) { var code = path.charCodeAt(i); if (code === 47) { if (!matchedSlash) { start = i + 1; break; } } else { if (firstNonSlashEnd === -1) matchedSlash = false, firstNonSlashEnd = i + 1; if (extIdx >= 0) if (code === ext.charCodeAt(extIdx)) { if (--extIdx === -1) end = i; } else extIdx = -1, end = firstNonSlashEnd; } } if (start === end) end = firstNonSlashEnd; else if (end === -1) end = path.length; return path.slice(start, end); } else { for (i = path.length - 1;i >= 0; --i) if (path.charCodeAt(i) === 47) { if (!matchedSlash) { start = i + 1; break; } } else if (end === -1) matchedSlash = false, end = i + 1; if (end === -1) return ""; return path.slice(start, end); } } function extname(path) { assertPath(path); var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0; for (var i = path.length - 1;i >= 0; --i) { var code = path.charCodeAt(i); if (code === 47) { if (!matchedSlash) { startPart = i + 1; break; } continue; } if (end === -1) matchedSlash = false, end = i + 1; if (code === 46) { if (startDot === -1) startDot = i; else if (preDotState !== 1) preDotState = 1; } else if (startDot !== -1) preDotState = -1; } if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) return ""; return path.slice(startDot, end); } function format(pathObject) { if (pathObject === null || typeof pathObject !== "object") throw new TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject); return _format("/", pathObject); } function parse(path) { assertPath(path); var ret = { root: "", dir: "", base: "", ext: "", name: "" }; if (path.length === 0) return ret; var code = path.charCodeAt(0), isAbsolute2 = code === 47, start; if (isAbsolute2) ret.root = "/", start = 1; else start = 0; var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0; for (;i >= start; --i) { if (code = path.charCodeAt(i), code === 47) { if (!matchedSlash) { startPart = i + 1; break; } continue; } if (end === -1) matchedSlash = false, end = i + 1; if (code === 46) { if (startDot === -1) startDot = i; else if (preDotState !== 1) preDotState = 1; } else if (startDot !== -1) preDotState = -1; } if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) { if (end !== -1) if (startPart === 0 && isAbsolute2) ret.base = ret.name = path.slice(1, end); else ret.base = ret.name = path.slice(startPart, end); } else { if (startPart === 0 && isAbsolute2) ret.name = path.slice(1, startDot), ret.base = path.slice(1, end); else ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end); ret.ext = path.slice(startDot, end); } if (startPart > 0) ret.dir = path.slice(0, startPart - 1); else if (isAbsolute2) ret.dir = "/"; return ret; } var sep = "/"; var delimiter = ":"; var posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null }); // src/index.ts var API_KEY_PATTERNS = [ /AIza[0-9A-Za-z-_]{35}/g, /sk-[a-zA-Z0-9]{48}/g, /api[_-]?key["\s]*[:=]["\s]*[a-zA-Z0-9-_]{20,}/gi, /secret[_-]?key["\s]*[:=]["\s]*[a-zA-Z0-9-_]{20,}/gi, /bearer[\s]+[a-zA-Z0-9-_]{20,}/gi, /eyJ[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+/g, /AKIA[0-9A-Z]{16}/g, /(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[A-Za-z0-9/+=]{40}(?=\s|$|"|')/g, /xox[baprs]-[0-9a-zA-Z-]{10,48}/g, /[MN][A-Za-z\d]{23}\.[\w-]{6}\.[\w-]{27}/g, /ghp_[0-9a-zA-Z]{36}/g, /glpat-[0-9a-zA-Z-_]{20}/g ]; var WHITE_LIST_PATTERNS = [ /your[_-]?api[_-]?key/gi, /your[_-]?actual[_-]?api[_-]?key/gi, /example[_-]?key/gi, /test[_-]?api[_-]?key/gi, /demo[_-]?key/gi, /your_.*_key/gi, /your_.*_api_key/gi, /^=+$/, /^-+$/, /^\*+$/, /^#+$/, /apiKey:\s*""\s*,?/gi ]; var EXCLUDE_PATTERNS = [ "node_modules", "dist", "build", ".git", "coverage", "bun.lockb" ]; var SCANNED_FILE_EXTENSIONS = [ ".ts", ".tsx", ".js", ".jsx", "mjs", "cjs", ".md", ".mdx", ".txt", ".yml", ".yaml", ".json" ]; function isSafePattern(text, content, lineNumber) { if (content && lineNumber) { const codeBlockRegex = /```[\s\S]*?```/g; const codeBlockMatches = content.matchAll(codeBlockRegex); for (const match of codeBlockMatches) { if (match.index !== undefined) { const codeBlockStart = match.index; const codeBlockEnd = codeBlockStart + match[0].length; const lineStartIndex = content.substring(0, codeBlockStart).split(` `).length; const lineEndIndex = content.substring(0, codeBlockEnd).split(` `).length; if (lineNumber >= lineStartIndex && lineNumber <= lineEndIndex) { return true; } } } } const trimmedLine = text.trim(); if (/^[=\-*#]+$/.test(trimmedLine) && trimmedLine.length > 10) { return true; } return WHITE_LIST_PATTERNS.some((pattern) => pattern.test(text)); } var loadCustomWhitelist = async (whitelistPath) => { try { const content = await readFile(whitelistPath, "utf8"); const lines = content.split(` `).filter((line) => line.trim() !== ""); return new Set(lines); } catch { return new Set; } }; var saveCustomWhitelist = async (whitelistPath, whitelist) => { const sortedList = Array.from(whitelist).sort(); await writeFile(whitelistPath, sortedList.join(` `) + ` `, "utf8"); }; var isCustomWhitelisted = (item, whitelist) => { return whitelist.has(item); }; var addLeaksToWhitelist = (whitelist, leaks) => { const initialSize = whitelist.size; leaks.forEach((leak) => { const entryToAdd = leak.key || leak.match; whitelist.add(entryToAdd); }); return whitelist.size - initialSize; }; async function getAllSourceFiles(dir) { const files = []; async function scanDirectory(currentDir) { const entries = await readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(currentDir, entry.name); const relativePath = fullPath.replace(process.cwd() + "/", ""); if (EXCLUDE_PATTERNS.some((pattern) => relativePath.includes(pattern))) { continue; } if (entry.isDirectory()) { await scanDirectory(fullPath); } else if (entry.isFile() && SCANNED_FILE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) { files.push(fullPath); } } } await scanDirectory(dir); return files; } var getSafePattern = () => { return { API_KEY_PATTERNS, WHITE_LIST_PATTERNS, EXCLUDE_PATTERNS, SCANNED_FILE_EXTENSIONS }; }; function detectAPIKeyLeaks(content) { const lines = content.split(` `); const leaks = []; for (let i = 0;i < lines.length; i++) { const line = lines[i]; for (const pattern of API_KEY_PATTERNS) { const matches = line.match(pattern); if (matches) { for (const match of matches) { if (!isSafePattern(line, content, i + 1)) { leaks.push({ line: i + 1, match, pattern: pattern.toString() }); } } } } } return leaks; } async function detectAPIKeyLeaksWithFiles(directory, customWhitelistPath, updateWhitelist) { const files = await getAllSourceFiles(directory); const customWhitelist = customWhitelistPath ? await loadCustomWhitelist(customWhitelistPath) : new Set; const allLeaks = []; for (const filePath of files) { try { const content = await readFile(filePath, "utf8"); const basicLeaks = detectAPIKeyLeaks(content); const leaksWithFile = basicLeaks.map((leak) => { const key = `${filePath}:${leak.match}`; return { ...leak, filePath, key }; }).filter((leak) => !isCustomWhitelisted(leak.key, customWhitelist)); allLeaks.push(...leaksWithFile); } catch (error) { throw new Error(`Could not read file ${filePath}: ${error}`); } } if (updateWhitelist && customWhitelistPath && allLeaks.length > 0) { const newItemsAdded = addLeaksToWhitelist(customWhitelist, allLeaks); await saveCustomWhitelist(customWhitelistPath, customWhitelist); console.log(`Added ${newItemsAdded} new API key leaks to whitelist: ${customWhitelistPath}`); } return allLeaks; } export { getSafePattern, detectAPIKeyLeaksWithFiles, detectAPIKeyLeaks };