UNPKG

@syntropysoft/praetorian

Version:

Praetorian CLI – A universal multi-environment configuration validator for DevSecOps teams. Validate, compare, and secure YAML/ENV files with ease.

195 lines 6.27 kB
"use strict"; /** * Secret Detector - Functional Programming * * Single Responsibility: Detect exposed secrets only * Pure functions, no state, no side effects */ Object.defineProperty(exports, "__esModule", { value: true }); exports.isValidSecretContext = exports.getSecretSeverity = exports.looksLikeSecret = exports.detectSecrets = void 0; /** * Pure function to detect secrets in content */ const detectSecrets = (content, rules, context) => { // Guard clause: no content if (!content || content.trim().length === 0) { return []; } // Guard clause: no rules if (!rules || rules.length === 0) { return []; } return rules .filter(rule => rule.enabled) .flatMap(rule => detectSecretsWithRule(content, rule, context)); }; exports.detectSecrets = detectSecrets; /** * Pure function to detect secrets with a specific rule */ const detectSecretsWithRule = (content, rule, context) => { // Guard clause: invalid rule if (!rule || !rule.pattern) { return []; } const matches = findPatternMatches(content, rule.pattern); return matches .filter(match => !isFalsePositive(match.value, rule, context)) .map(match => createSecretDetectionResult(match, rule, content)); }; /** * Pure function to find all pattern matches */ const findPatternMatches = (content, pattern) => { const matches = []; let match; // Reset regex lastIndex to ensure global search works pattern.lastIndex = 0; while ((match = pattern.exec(content)) !== null) { matches.push({ value: match[0], index: match.index }); // Prevent infinite loop on zero-length matches if (match.index === pattern.lastIndex) { pattern.lastIndex++; } } return matches; }; /** * Pure function to check if match is false positive */ const isFalsePositive = (value, rule, context) => { // Guard clause: no exclude patterns if (!rule.excludePatterns || rule.excludePatterns.length === 0) { return false; } // Check against exclude patterns return rule.excludePatterns.some(excludePattern => excludePattern.test(value)); }; /** * Pure function to create secret detection result */ const createSecretDetectionResult = (match, rule, content) => { const { lineNumber, columnNumber } = getLineAndColumn(content, match.index); const context = getContextAroundMatch(content, match.index, 50); return { secretType: rule.name, maskedValue: maskSecret(match.value), confidence: calculateConfidence(match.value, rule), context, lineNumber, columnNumber }; }; /** * Pure function to get line and column from index */ const getLineAndColumn = (content, index) => { const beforeMatch = content.substring(0, index); const lineNumber = (beforeMatch.match(/\n/g) || []).length + 1; const lastNewline = beforeMatch.lastIndexOf('\n'); const columnNumber = index - lastNewline; return { lineNumber, columnNumber }; }; /** * Pure function to get context around match */ const getContextAroundMatch = (content, index, contextLength) => { const start = Math.max(0, index - contextLength); const end = Math.min(content.length, index + contextLength); return content.substring(start, end); }; /** * Pure function to mask secret value */ const maskSecret = (value) => { // Guard clause: empty value if (!value || value.length === 0) { return value; } // Guard clause: very short value if (value.length <= 4) { return '*'.repeat(value.length); } // Show first 2 and last 2 characters, mask the rest const start = value.substring(0, 2); const end = value.substring(value.length - 2); const middle = '*'.repeat(Math.max(4, value.length - 4)); return `${start}${middle}${end}`; }; /** * Pure function to calculate confidence level */ const calculateConfidence = (value, rule) => { // Guard clause: empty value if (!value || value.length === 0) { return 0; } let confidence = 50; // Base confidence // Length-based confidence if (value.length >= 20) confidence += 20; if (value.length >= 40) confidence += 10; // Character diversity const hasNumbers = /\d/.test(value); const hasLetters = /[a-zA-Z]/.test(value); const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value); if (hasNumbers && hasLetters) confidence += 15; if (hasSpecial) confidence += 10; // Pattern-specific confidence if (rule.id.includes('API_KEY') && value.startsWith('sk-')) confidence += 20; if (rule.id.includes('JWT') && value.includes('.')) confidence += 15; return Math.min(100, confidence); }; /** * Pure function to check if value looks like a secret */ const looksLikeSecret = (value) => { // Guard clause: empty value if (!value || value.length === 0) { return false; } // Common secret patterns const secretPatterns = [ /^[a-zA-Z0-9]{20,}$/, // Long alphanumeric /^[a-f0-9]{32,}$/i, // Hex strings /^[A-Za-z0-9+/]{40,}={0,2}$/, // Base64-like /^sk-[a-zA-Z0-9]{20,}$/, // Stripe-like keys /^pk_[a-zA-Z0-9]{20,}$/, // Public keys /^[a-zA-Z0-9]{24,}$/ // Generic long strings ]; return secretPatterns.some(pattern => pattern.test(value)); }; exports.looksLikeSecret = looksLikeSecret; /** * Pure function to get secret severity */ const getSecretSeverity = (confidence) => { if (confidence >= 90) return 'critical'; if (confidence >= 75) return 'high'; if (confidence >= 50) return 'medium'; return 'low'; }; exports.getSecretSeverity = getSecretSeverity; /** * Pure function to validate secret context */ const isValidSecretContext = (value, context, validContexts) => { // Guard clause: no valid contexts defined if (!validContexts || validContexts.length === 0) { return true; } return validContexts.some(validContext => context.toLowerCase().includes(validContext.toLowerCase())); }; exports.isValidSecretContext = isValidSecretContext; //# sourceMappingURL=SecretDetector.js.map