am-i-secure
Version:
A CLI tool to detect malicious npm packages in your project dependencies
201 lines (164 loc) • 5.86 kB
JavaScript
const chalk = require('chalk');
const Table = require('cli-table3');
const path = require('path');
class Logger {
constructor() {
this.verboseMode = false;
this.jsonOutput = false;
this.projectDir = null;
this.fullPaths = false;
}
setVerbose(verbose) {
this.verboseMode = verbose;
}
setJsonOutput(jsonOutput) {
this.jsonOutput = jsonOutput;
}
setProjectDir(projectDir) {
this.projectDir = projectDir;
}
setFullPaths(fullPaths) {
this.fullPaths = fullPaths;
}
info(message) {
if (!this.jsonOutput) {
console.log(chalk.blue('ℹ'), message);
}
}
success(message) {
if (!this.jsonOutput) {
console.log(chalk.green(message));
}
}
warn(message) {
if (!this.jsonOutput) {
console.log(chalk.yellow('⚠'), message);
}
}
error(message) {
if (!this.jsonOutput) {
console.error(chalk.red(message));
}
}
verbose(message) {
if (this.verboseMode && !this.jsonOutput) {
console.log(chalk.gray('→'), message);
}
}
displayResults(results) {
if (this.jsonOutput) {
return; // JSON output is handled in the main CLI
}
console.log();
this.displayScanSummary(results.summary);
if (results.findings.length > 0) {
console.log();
this.displayFindings(results.findings);
}
}
displayScanSummary(summary) {
console.log(chalk.bold('📊 Scan Summary:'));
console.log();
const summaryTable = new Table({
head: ['Metric', 'Count'],
colWidths: [30, 10]
});
summaryTable.push(
['Lock files scanned', summary.lockFilesScanned],
['node_modules scanned', summary.nodeModulesScanned ? 'Yes' : 'No'],
['Total packages checked', summary.totalPackagesChecked],
['Malicious packages found', chalk.red(summary.maliciousPackagesFound)]
);
console.log(summaryTable.toString());
}
displayFindings(findings) {
console.log(chalk.bold.red('🚨 Malicious Packages Found:'));
console.log();
const findingsTable = new Table({
head: ['Package', 'Version', 'Source', 'Introduced By', 'File Path'],
colWidths: this.fullPaths ? [25, 12, 15, 20, 120] : [25, 12, 15, 20, 60]
});
findings.forEach(finding => {
findingsTable.push([
chalk.red(finding.packageName),
chalk.red(finding.version),
this.formatSource(finding.source),
finding.introducedBy || 'Direct dependency',
this.formatFilePath(finding.filePath)
]);
});
console.log(findingsTable.toString());
// Add helpful tip about clickable paths
if (!this.fullPaths) {
console.log();
console.log(chalk.gray('💡 Tip: File paths should be clickable in most terminals. Use --full-paths for complete paths.'));
}
}
formatSource(source) {
const sourceColors = {
'package-lock.json': chalk.blue,
'yarn.lock': chalk.cyan,
'pnpm-lock.yaml': chalk.magenta,
'node_modules': chalk.yellow
};
const color = sourceColors[source] || chalk.white;
return color(source);
}
formatFilePath(filePath, maxLength = 55) {
if (!filePath) {
return 'Unknown';
}
// Get the absolute path for clickability
const absolutePath = path.resolve(filePath);
// If full paths are requested, return the complete absolute path
if (this.fullPaths) {
return absolutePath;
}
// For clickability, show the absolute path but truncate it intelligently
// Most terminals support clicking on absolute paths
if (absolutePath.length <= maxLength) {
return absolutePath;
}
// Smart truncation for absolute paths that preserves clickability
const segments = absolutePath.split('/');
if (segments.length > 4) {
const fileName = segments[segments.length - 1];
const parentDir = segments[segments.length - 2];
const start = segments.slice(0, 3).join('/'); // Keep /Users/username
const truncated = `${start}/.../${parentDir}/${fileName}`;
if (truncated.length <= maxLength) {
return truncated;
}
}
// Fallback: create a relative path display version
let displayPath = filePath;
if (this.projectDir) {
const relativePath = path.relative(this.projectDir, filePath);
// Add ./ prefix for clarity if it's a relative path within the project
if (!relativePath.startsWith('..')) {
displayPath = `./${relativePath}`;
} else {
displayPath = relativePath;
}
}
if (displayPath.length <= maxLength) {
return displayPath;
}
// Final fallback: truncate the relative path
const start = displayPath.substring(0, 15);
const end = displayPath.substring(displayPath.length - (maxLength - 18));
return `${start}...${end}`;
}
displayProgress(message) {
if (this.verboseMode && !this.jsonOutput) {
process.stdout.write(chalk.gray(`→ ${message}...\r`));
}
}
clearProgress() {
if (this.verboseMode && !this.jsonOutput) {
process.stdout.clearLine();
process.stdout.cursorTo(0);
}
}
}
module.exports = { Logger };