gitleaks-secret-scanner
Version:
A powerful, intelligent wrapper for the Gitleaks engine that provides accurate and safe secret scanning for local pre-commit hooks and CI/CD pipelines.
259 lines (226 loc) • 7.71 kB
JavaScript
const fs = require("fs");
const path = require("path");
const os = require("os");
const https = require("https");
const tar = require("tar");
const unzipper = require("unzipper");
let packageInfo = {
name: "gitleaks-secret-scanner",
version: "2.1.1",
repository: { url: "https://github.com/criisv7/gitleaks-secret-scanner" },
};
try {
const packagePath = path.join(__dirname, "..", "package.json");
if (fs.existsSync(packagePath)) {
packageInfo = require(packagePath);
}
} catch (e) {
console.warn("⚠️ Error loading package.json:", e.message);
}
const CACHE_DIR = path.join(os.homedir(), ".gitleaks-cache");
if (!fs.existsSync(CACHE_DIR)) {
fs.mkdirSync(CACHE_DIR, { recursive: true });
}
async function fetchLatestVersion() {
return new Promise((resolve, reject) => {
const options = {
hostname: "api.github.com",
path: "/repos/gitleaks/gitleaks/releases/latest",
method: "GET",
headers: {
"User-Agent": `${packageInfo.name}/${packageInfo.version}`,
Accept: "application/vnd.github+json",
},
};
const req = https.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
if (res.statusCode !== 200) {
console.warn("⚠️ Could not fetch latest version, using fallback");
return resolve("8.30.0"); // Fallback to known stable version
}
try {
const release = JSON.parse(data);
const version = release.tag_name.replace("v", "");
resolve(version);
} catch (error) {
console.warn("⚠️ Could not parse latest version, using fallback");
resolve("8.30.0");
}
});
});
req.on("error", () => {
console.warn("⚠️ Network error fetching latest version, using fallback");
resolve("8.30.0");
});
req.setTimeout(5000, () => {
req.destroy();
console.warn("⚠️ Timeout fetching latest version, using fallback");
resolve("8.30.0");
});
req.end();
});
}
module.exports.installGitleaks = async (config) => {
const platform = os.platform();
const arch = os.arch();
const binaryName = platform === "win32" ? "gitleaks.exe" : "gitleaks";
let version = config.version;
// Fetch latest version if not specified
if (!version) {
version = await fetchLatestVersion();
}
console.log("Using Gitleaks version:", version);
const versionDir = path.join(CACHE_DIR, `v${version}`);
const binaryPath = path.join(versionDir, binaryName);
if (fs.existsSync(binaryPath)) {
return binaryPath;
}
if (!fs.existsSync(versionDir)) {
fs.mkdirSync(versionDir, { recursive: true });
}
try {
const fileName = getFileName(version, platform, arch);
const downloadUrl = `https://github.com/gitleaks/gitleaks/releases/download/v${version}/${fileName}`;
await downloadAndExtract(downloadUrl, versionDir, platform);
if (platform !== "win32") {
fs.chmodSync(binaryPath, 0o755);
}
return binaryPath;
} catch (error) {
console.error(
`\n❌ Failed to download or extract Gitleaks: ${error.message}\n`
);
if (error.message.includes("404")) {
console.error(
`⚠️ Binary not found for your system (platform: ${platform}, arch: ${arch})`
);
console.log("\n📖 Manual Installation Options:\n");
console.log(`1. Download directly from GitHub:`);
console.log(
` https://github.com/gitleaks/gitleaks/releases/tag/v${version}`
);
console.log(`\n2. Use a different version:`);
console.log(` npx gitleaks-secret-scanner --select-version`);
console.log(`\n3. Install Gitleaks globally and use it directly:`);
console.log(` brew install gitleaks # macOS`);
console.log(
` # or download from: https://github.com/gitleaks/gitleaks/releases\n`
);
} else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
console.error(`⚠️ Network error - unable to reach GitHub`);
console.log("\n💡 Troubleshooting:\n");
console.log(`1. Check your internet connection`);
console.log(`2. Check if you're behind a proxy or firewall`);
console.log(`3. Try again later`);
console.log(
`4. Download manually from: https://github.com/gitleaks/gitleaks/releases\n`
);
} else if (error.code === "EACCES" || error.code === "EPERM") {
console.error(`⚠️ Permission denied - cannot write to cache directory`);
console.log("\n💡 Solutions:\n");
console.log(`1. Run with appropriate permissions`);
console.log(`2. Check permissions on: ${CACHE_DIR}`);
console.log(`3. Try: sudo chown -R $(whoami) ${CACHE_DIR}\n`);
} else {
console.log("\n💡 Troubleshooting:\n");
console.log(
`1. Try a different version: npx gitleaks-secret-scanner --select-version`
);
console.log(
`2. Check available versions: https://github.com/gitleaks/gitleaks/releases`
);
console.log(
`3. Report this issue: https://github.com/criisv7/gitleaks-secret-scanner/issues\n`
);
}
throw error;
}
};
function getFileName(version, platform, arch) {
let osName, archName;
switch (platform) {
case "darwin":
osName = "darwin";
break;
case "linux":
osName = "linux";
break;
case "win32":
osName = "windows";
break;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
switch (arch) {
case "x64":
archName = "x64";
break;
case "arm64":
archName = "arm64";
break;
case "arm":
// Node.js doesn't easily distinguish between armv6 and armv7.
// default to the more common armv7 but warn the user.
archName = "armv7";
console.warn(
`⚠️ Detected 'arm' architecture. Assuming 'armv7'. If you need 'armv6', this may fail.`
);
break;
case "ia32":
// This case handles both 'linux_x32' and 'windows_x32'.
archName = "x32";
break;
default:
throw new Error(`Unsupported architecture: ${arch}.`);
}
const ext = osName === "windows" ? "zip" : "tar.gz";
const fileName = `gitleaks_${version}_${osName}_${archName}.${ext}`;
return fileName;
}
async function downloadAndExtract(url, targetDir, platform) {
return new Promise((resolve, reject) => {
const headers = {
"User-Agent": `${packageInfo.name}/${packageInfo.version}`,
};
const request = https.get(url, { headers }, (response) => {
if (
response.statusCode >= 300 &&
response.statusCode < 400 &&
response.headers.location
) {
downloadAndExtract(response.headers.location, targetDir, platform)
.then(resolve)
.catch(reject);
return;
}
if (response.statusCode !== 200) {
response.resume();
return reject(
new Error(`Download failed with status code: ${response.statusCode}`)
);
}
let extractor;
if (platform === "win32") {
extractor = unzipper.Extract({ path: targetDir });
} else {
extractor = tar.x({ C: targetDir });
}
response
.pipe(extractor)
.on("finish", () => {
resolve();
})
.on("error", (err) => {
const archiveType = platform === "win32" ? "ZIP" : "TAR";
reject(new Error(`${archiveType} extraction failed: ${err.message}`));
});
});
request.on("error", (err) => {
reject(new Error(`Download request failed: ${err.message}`));
});
});
}