UNPKG

secret-protection-custom-pattern-automation

Version:

A Playwright-based tool to automate GitHub secret scanning custom pattern management.

192 lines (155 loc) 7.56 kB
import chalk from 'chalk'; import Table from 'cli-table3'; import { Pattern, PatternFile, Config } from './secret_protection.js'; export interface ValidationResult { isValid: boolean; errors: string[]; warnings: string[]; suggestions: string[]; } export class PatternValidator { public static validatePattern(pattern: Pattern): ValidationResult { const result: ValidationResult = { isValid: true, errors: [], warnings: [], suggestions: [] }; // Basic validation this.validateBasicStructure(pattern, result); result.isValid = result.errors.length === 0; return result; } public static validatePatternFile(patternFile: PatternFile, config: Config): ValidationResult { const aggregateResult: ValidationResult = { isValid: true, errors: [], warnings: [], suggestions: [] }; if (!patternFile.name?.trim()) { aggregateResult.errors.push("Pattern file must have a name"); } if (!patternFile.patterns || patternFile.patterns.length === 0) { aggregateResult.errors.push("Pattern file must contain at least one pattern"); } const patternNames = new Set<string>(); for (const pattern of (patternFile.patterns || [])) { if (config.patternsToInclude && !config.patternsToInclude.includes(pattern.name)) { continue; } if (config.patternsToExclude && config.patternsToExclude.includes(pattern.name)) { continue; } const patternResult = this.validatePattern(pattern); // Check for duplicate names if (patternNames.has(pattern.name)) { aggregateResult.errors.push(`Duplicate pattern name: "${pattern.name}"`); } patternNames.add(pattern.name); // Aggregate results aggregateResult.errors.push(...patternResult.errors.map(e => `Pattern "${pattern.name}": ${e}`)); aggregateResult.warnings.push(...patternResult.warnings.map(w => `Pattern "${pattern.name}": ${w}`)); aggregateResult.suggestions.push(...patternResult.suggestions.map(s => `Pattern "${pattern.name}": ${s}`)); } aggregateResult.isValid = aggregateResult.errors.length === 0; return aggregateResult; } private static validateBasicStructure(pattern: Pattern, result: ValidationResult): void { if (!pattern.name?.trim()) { result.errors.push("Pattern name is required"); } else if (pattern.name.length > 100) { result.warnings.push("Pattern name is very long (>100 characters)"); } if (pattern.regex?.pattern?.trim() === '' || !pattern.regex?.pattern) { result.errors.push("Pattern regex is required"); } if (pattern.regex?.pattern.length === 1) { result.errors.push("Pattern regex is very short (1 character)"); } if (pattern.regex?.pattern.length < 5) { result.errors.push("Pattern regex is very short (less than 5 characters)"); } if (pattern.regex?.version === undefined) { result.suggestions.push("Pattern version is not specified. Consider adding a version to track changes more precisely."); } if (pattern.regex?.start && !pattern.regex.start.trim()) { result.suggestions.push("Pattern start regex is empty, so this will use the default start regex. Consider specifying a start regex for better accuracy."); } if (pattern.regex?.end && !pattern.regex.end.trim()) { result.suggestions.push("Pattern end regex is empty, so this will use the default end regex. Consider specifying an end regex for better accuracy."); } // check that additional_match and additional_not_match are arrays if (pattern.regex?.additional_match && !Array.isArray(pattern.regex.additional_match)) { result.errors.push("Pattern additional_match must be an array"); } else if (pattern.regex?.additional_match) { for (const rule of pattern.regex.additional_match) { if (typeof rule !== 'string' || !rule.trim()) { result.errors.push("Each additional_match rule must be a non-empty string"); } } } if (pattern.regex?.additional_not_match && !Array.isArray(pattern.regex.additional_not_match)) { result.errors.push("Pattern additional_not_match must be an array"); } else if (pattern.regex?.additional_not_match) { for (const rule of pattern.regex.additional_not_match) { if (typeof rule !== 'string' || !rule.trim()) { result.errors.push("Each additional_not_match rule must be a non-empty string"); } } } if (!pattern.test?.data) { result.suggestions.push("Pattern test data is missing. Having test data helps ensure the pattern works as expected."); } } public static printValidationReport(result: ValidationResult, patternName?: string): void { const title = patternName ? `Validation Report for "${patternName}"` : 'Validation Report'; console.log(chalk.bold.underline(`\n${title}`)); if (patternName) { if (result.isValid) { console.log(chalk.green('✓ Pattern is valid')); } else { console.log(chalk.red('✗ Pattern has errors')); } } else { if (result.isValid) { console.log(chalk.green('✓ All patterns are valid')); } else { console.log(chalk.red('✗ Some patterns have errors')); } } if (result.errors.length > 0) { console.log(chalk.red.bold('\nErrors:')); for (const error of result.errors) { console.log(chalk.red(` ✗ ${error}`)); } } if (result.warnings.length > 0) { console.log(chalk.yellow.bold('\nWarnings:')); for (const warning of result.warnings) { console.log(chalk.yellow(` ⚠ ${warning}`)); } } if (result.suggestions.length > 0) { console.log(chalk.blue.bold('\nSuggestions:')); for (const suggestion of result.suggestions) { console.log(chalk.blue(` 💡 ${suggestion}`)); } } console.log(); // Empty line } public static createSummaryTable(results: Array<{ name: string; result: ValidationResult }>): string { const table = new Table({ head: ['Pattern Name', 'Status', 'Errors', 'Warnings', 'Suggestions'], colWidths: [30, 10, 10, 12, 15] }); for (const { name, result } of results) { const status = result.isValid ? chalk.green('✓ Valid') : chalk.red('✗ Invalid'); const errors = result.errors.length > 0 ? chalk.red(result.errors.length.toString()) : '0'; const warnings = result.warnings.length > 0 ? chalk.yellow(result.warnings.length.toString()) : '0'; const suggestions = result.suggestions.length > 0 ? chalk.blue(result.suggestions.length.toString()) : '0'; table.push([name, status, errors, warnings, suggestions]); } return table.toString(); } }