@syntropysoft/praetorian
Version:
Praetorian CLI ā A universal multi-environment configuration validator for DevSecOps teams. Validate, compare, and secure YAML/ENV files with ease.
226 lines ⢠9.16 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@oclif/core");
const chalk_1 = __importDefault(require("chalk"));
const ConfigParser_1 = require("../infrastructure/parsers/ConfigParser");
const EqualityRule_1 = require("../domain/rules/EqualityRule");
const FileReaderService_1 = require("../infrastructure/adapters/FileReaderService");
class EmptyKeys extends core_1.Command {
async run() {
const { args, flags } = await this.parse(EmptyKeys);
try {
const filesToAnalyze = this.determineFilesToAnalyze(args, flags);
const configFiles = await this.loadFiles(filesToAnalyze);
const result = await new EqualityRule_1.EqualityRule().execute(configFiles);
this.displayEmptyKeysReport(result, flags);
}
catch (error) {
this.error(error instanceof Error ? error.message : 'Unknown error');
this.exit(1);
}
}
determineFilesToAnalyze(args, flags) {
if (args.files?.length > 0) {
return Array.isArray(args.files) ? args.files : [args.files];
}
const configParser = new ConfigParser_1.ConfigParser(flags.config);
if (!configParser.exists()) {
this.error(`Configuration file not found: ${flags.config}`);
this.log(chalk_1.default.yellow('\nCreate a configuration file with:'));
this.log(chalk_1.default.gray('praetorian init'));
throw new Error('Configuration file not found');
}
return flags.env
? configParser.getEnvironmentFiles(flags.env)
: configParser.getFilesToCompare();
}
async loadFiles(filePaths) {
const fileReaderService = new FileReaderService_1.FileReaderService();
const { valid, invalid } = fileReaderService.validateFiles(filePaths);
if (invalid.length > 0) {
const supportedExtensions = fileReaderService.getSupportedExtensions().join(', ');
throw new Error(`Unsupported file formats: ${invalid.join(', ')}. ` +
`Supported extensions: ${supportedExtensions}`);
}
return await fileReaderService.readFiles(valid);
}
displayEmptyKeysReport(result, flags) {
const emptyKeys = result.info || [];
const totalFiles = result.metadata?.filesCompared || 0;
const outputHandlers = {
json: () => this.displayJsonReport(emptyKeys, totalFiles, result.metadata),
csv: () => this.displayCsvReport(emptyKeys, flags),
pretty: () => this.displayPrettyReport(emptyKeys, totalFiles, flags)
};
const handler = outputHandlers[flags.output] || outputHandlers.pretty;
handler();
}
displayJsonReport(emptyKeys, totalFiles, metadata) {
const report = {
summary: {
totalFiles,
totalEmptyKeys: emptyKeys.length,
filesWithEmptyKeys: this.getUniqueFiles(emptyKeys).length,
},
emptyKeys: this.transformToEmptyKeyInfo(emptyKeys),
metadata,
};
console.log(JSON.stringify(report, null, 2));
}
displayCsvReport(emptyKeys, flags) {
const headers = ['Key', 'File', 'Value Type', 'Value'];
const rows = this.transformToCsvRows(emptyKeys, flags);
console.log(headers.join(','));
rows.forEach(row => console.log(row.join(',')));
}
displayPrettyReport(emptyKeys, totalFiles, flags) {
console.log(chalk_1.default.blue('\nš Empty Keys Report:\n'));
if (emptyKeys.length === 0) {
console.log(chalk_1.default.green('ā
No empty keys found!'));
console.log(chalk_1.default.gray(`Analyzed ${totalFiles} file(s)`));
return;
}
this.displaySummary(emptyKeys, totalFiles);
this.displayEmptyKeysList(emptyKeys, flags);
this.displayRecommendations();
}
displaySummary(emptyKeys, totalFiles) {
const summaryLines = [
` ⢠Files analyzed: ${totalFiles}`,
` ⢠Total empty keys: ${emptyKeys.length}`,
` ⢠Files with empty keys: ${this.getUniqueFiles(emptyKeys).length}`
];
console.log(chalk_1.default.blue('š Summary:'));
summaryLines.forEach(line => console.log(line));
}
displayEmptyKeysList(emptyKeys, flags) {
if (flags['group-by-file']) {
this.displayGroupedByFile(emptyKeys, flags);
}
else {
this.displayFlatList(emptyKeys, flags);
}
}
displayGroupedByFile(emptyKeys, flags) {
const grouped = this.groupByFile(emptyKeys);
Object.entries(grouped)
.map(([filePath, keys]) => ({
filePath,
keys,
count: keys.length
}))
.forEach(({ filePath, keys, count }) => {
console.log(chalk_1.default.cyan(`\nš ${filePath} (${count} empty keys):`));
this.renderEmptyKeys(keys, flags, ' ');
});
}
displayFlatList(emptyKeys, flags) {
console.log(chalk_1.default.blue('\nš Empty Keys List:'));
this.renderEmptyKeys(emptyKeys, flags, ' ');
}
renderEmptyKeys(emptyKeys, flags, prefix) {
emptyKeys
.map(key => this.formatEmptyKeyDisplay(key, flags, prefix))
.forEach(line => console.log(line));
}
formatEmptyKeyDisplay(key, flags, prefix) {
const keyDisplay = chalk_1.default.blue(`${prefix}⢠${key.path}`);
const fileDisplay = chalk_1.default.gray(` [${key.context?.file}]`);
const valueDisplay = flags['include-values']
? chalk_1.default.gray(` (${key.context?.valueType}: ${JSON.stringify(key.context?.value)})`)
: '';
return `${keyDisplay}${fileDisplay}${valueDisplay}`;
}
displayRecommendations() {
const recommendations = [
' ⢠Review empty keys to ensure they are intentional',
' ⢠Consider using environment-specific values for empty keys',
' ⢠Add empty keys to ignore list if they are expected',
' ⢠Use --include-values to see actual empty values'
];
console.log(chalk_1.default.yellow('\nš” Recommendations:'));
recommendations.forEach(rec => console.log(chalk_1.default.gray(rec)));
}
// Pure functions for data transformation
transformToEmptyKeyInfo(emptyKeys) {
return emptyKeys.map(key => ({
path: key.path,
file: key.context?.file || '',
value: key.context?.value,
valueType: key.context?.valueType || '',
message: key.message || ''
}));
}
transformToCsvRows(emptyKeys, flags) {
return emptyKeys.map(key => [
`"${key.path}"`,
`"${key.context?.file || ''}"`,
`"${key.context?.valueType || ''}"`,
`"${flags['include-values'] ? JSON.stringify(key.context?.value || '') : ''}"`,
]);
}
groupByFile(emptyKeys) {
return emptyKeys.reduce((acc, key) => {
const filePath = key.context?.file || 'unknown';
return {
...acc,
[filePath]: [...(acc[filePath] || []), key]
};
}, {});
}
getUniqueFiles(emptyKeys) {
return emptyKeys
.map(key => key.context?.file)
.filter(Boolean)
.reduce((unique, file) => unique.includes(file) ? unique : [...unique, file], []);
}
}
EmptyKeys.description = 'Generate a report of empty keys in configuration files';
EmptyKeys.examples = [
'$ praetorian empty-keys',
'$ praetorian empty-keys --env dev',
'$ praetorian empty-keys config-dev.yaml config-prod.yaml',
'$ praetorian empty-keys --output json',
'$ praetorian empty-keys --include-values',
];
EmptyKeys.flags = {
env: core_1.Flags.string({
char: 'e',
description: 'Environment to analyze (dev, staging, prod)',
required: false,
}),
output: core_1.Flags.string({
char: 'o',
description: 'Output format (pretty, json, csv)',
options: ['pretty', 'json', 'csv'],
default: 'pretty',
}),
config: core_1.Flags.string({
char: 'c',
description: 'Path to praetorian.yaml configuration file',
default: 'praetorian.yaml',
}),
'include-values': core_1.Flags.boolean({
char: 'v',
description: 'Include empty values in the report',
default: false,
}),
'group-by-file': core_1.Flags.boolean({
char: 'g',
description: 'Group empty keys by file',
default: false,
}),
help: core_1.Flags.help({ char: 'h' }),
};
EmptyKeys.args = {
files: core_1.Args.string({
description: 'Configuration files to analyze',
required: false,
multiple: true,
}),
};
exports.default = EmptyKeys;
//# sourceMappingURL=empty-keys.js.map