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
JavaScript
// 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
};