trufflehog-js
Version:
TypeScript wrapper for TruffleHog secret scanner
271 lines (235 loc) • 6.56 kB
text/typescript
/**
* Copyright (c) 2025 maloma7. All rights reserved.
* SPDX-License-Identifier: MIT
*/
import type { ScanOptions, ScanResult } from "./types.ts";
import { ExitCode } from "./types.ts";
import { createLogger } from "./logger.ts";
import { TruffleHogScanner, throwIfSecretsFound } from "./scanner.ts";
import { handleProcessExit, TruffleHogError } from "./errors.ts";
interface CliOptions {
staged: boolean;
quiet: boolean;
verbose: boolean;
config?: string;
exclude: string[];
timeout: number;
verify?: boolean;
includeDetectors: string[];
excludeDetectors: string[];
help: boolean;
version: boolean;
}
function parseArgs(args: string[]): CliOptions {
const options: CliOptions = {
staged: false,
quiet: false,
verbose: false,
exclude: [],
timeout: 120000, // 2 minutes
includeDetectors: [],
excludeDetectors: [],
help: false,
version: false,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--staged":
options.staged = true;
break;
case "--quiet":
options.quiet = true;
break;
case "--verbose":
options.verbose = true;
break;
case "--config": {
const configArg = args[++i];
if (configArg !== undefined) {
options.config = configArg;
}
break;
}
case "--exclude": {
const excludeArg = args[++i];
if (excludeArg !== undefined) {
options.exclude.push(excludeArg);
}
break;
}
case "--timeout": {
const timeoutArg = args[++i];
if (timeoutArg !== undefined) {
options.timeout = parseInt(timeoutArg, 10);
}
break;
}
case "--verify":
options.verify = true;
break;
case "--no-verify":
options.verify = false;
break;
case "--include-detectors": {
const includeDetectorsArg = args[++i];
if (includeDetectorsArg !== undefined) {
options.includeDetectors.push(...includeDetectorsArg.split(","));
}
break;
}
case "--exclude-detectors": {
const excludeDetectorsArg = args[++i];
if (excludeDetectorsArg !== undefined) {
options.excludeDetectors.push(...excludeDetectorsArg.split(","));
}
break;
}
case "--help":
case "-h":
options.help = true;
break;
case "--version":
case "-v":
options.version = true;
break;
default:
if (arg?.startsWith("--")) {
throw new TruffleHogError(`Unknown option: ${arg}`, ExitCode.ERROR);
}
break;
}
}
return options;
}
function printHelp(): void {
const help = `
TruffleHog-JS - TypeScript wrapper for TruffleHog secret scanner
USAGE:
bunx trufflehog-js [OPTIONS]
OPTIONS:
--staged Scan only staged files (default for pre-commit)
--quiet Minimal output for commit hooks
--verbose Detailed output for debugging
--config <path> Custom configuration file
--exclude <glob> Exclude file patterns (can be used multiple times)
--timeout <ms> Maximum execution time (default: 120000)
--verify Enable secret verification
--no-verify Disable secret verification
--include-detectors Comma-separated list of detectors to include
--exclude-detectors Comma-separated list of detectors to exclude
--help, -h Show this help message
--version, -v Show version information
EXAMPLES:
# Scan staged files (typical pre-commit usage)
bunx trufflehog-js --staged --quiet
# Scan with verbose output
bunx trufflehog-js --staged --verbose
# Scan with custom timeout and excluded patterns
bunx trufflehog-js --staged --timeout 60000 --exclude "*.test.ts"
EXIT CODES:
0 No secrets found (success)
1 Secrets detected (commit should be blocked)
2 Tool error (commit should be blocked)
LEFTHOOK INTEGRATION:
Add to .lefthook.yml:
pre-commit:
commands:
secrets:
run: bunx trufflehog-js --staged --quiet
fail_text: "🚨 Secrets detected! Please remove before committing."
`;
globalThis.console.log(help.trim());
}
function printVersion(): void {
globalThis.console.log("trufflehog-js v1.0.0");
}
async function runScan(options: CliOptions): Promise<void> {
const logger = createLogger({
quiet: options.quiet,
verbose: options.verbose,
level: options.verbose ? "debug" : options.quiet ? "error" : "warn",
});
const scanOptions: ScanOptions = {
staged: options.staged,
quiet: options.quiet,
verbose: options.verbose,
timeout: options.timeout,
...(options.verify !== undefined ? { verify: options.verify } : {}),
excludePaths: options.exclude,
...(options.includeDetectors.length > 0
? { includeDetectors: options.includeDetectors }
: {}),
...(options.excludeDetectors.length > 0
? { excludeDetectors: options.excludeDetectors }
: {}),
};
const scanner = new TruffleHogScanner({
timeout: options.timeout,
logger,
});
let results: ScanResult[];
if (options.staged) {
logger.debug("Scanning staged files...");
results = await scanner.scanStagedFiles(scanOptions);
} else {
// If not staged, we would need to implement scanning of all files
// For V1, we focus on staged files only
throw new TruffleHogError(
"Non-staged scanning not implemented in V1. Use --staged flag.",
ExitCode.ERROR,
);
}
if (results.length === 0) {
if (!options.quiet) {
logger.info("✅ No secrets detected");
}
return;
}
// Secrets found - format output
if (!options.quiet) {
logger.error(
`🚨 Found ${results.length} secret${results.length === 1 ? "" : "s"}:`,
);
for (const result of results) {
logger.error(
` ${result.detector} in ${result.file}${result.verified ? " (verified)" : ""}`,
);
if (options.verbose) {
logger.error(` Secret: ${result.secret}`);
}
}
}
throwIfSecretsFound(results);
}
async function main(): Promise<void> {
try {
const args = Bun.argv.slice(2); // Remove 'bun' and script path
const options = parseArgs(args);
if (options.help) {
printHelp();
process.exit(ExitCode.SUCCESS);
}
if (options.version) {
printVersion();
process.exit(ExitCode.SUCCESS);
}
// Default to staged scanning for pre-commit usage
if (!options.staged) {
options.staged = true;
}
await runScan(options);
process.exit(ExitCode.SUCCESS);
} catch (error) {
const logger = createLogger({
quiet: false,
verbose: Bun.env.TRUFFLEHOG_VERBOSE === "true",
});
handleProcessExit(error as Error, logger);
}
}
// Run the CLI if this file is executed directly
if (import.meta.main) {
await main();
}