sicua
Version:
A tool for analyzing project structure and dependencies
221 lines (220 loc) • 8.3 kB
JavaScript
"use strict";
/**
* Pattern matching utilities for security vulnerability detection
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PatternMatcher = void 0;
class PatternMatcher {
/**
* Find all matches for a given pattern in content
*/
static findMatches(pattern, content) {
switch (pattern.type) {
case "regex":
return this.findRegexMatches(pattern, content);
case "string":
return this.findStringMatches(pattern, content);
case "context":
return this.findContextMatches(pattern, content);
default:
return [];
}
}
/**
* Find regex pattern matches
*/
static findRegexMatches(pattern, content) {
const matches = [];
const regex = new RegExp(pattern.expression.source, pattern.flags || pattern.expression.flags);
let match;
let matchCount = 0;
const maxMatches = pattern.maxMatches || this.MAX_MATCHES_PER_PATTERN;
while ((match = regex.exec(content)) !== null && matchCount < maxMatches) {
const location = this.getLocationFromIndex(content, match.index);
const context = this.extractContext(content, match.index, match[0].length);
matches.push({
match: match[0],
startIndex: match.index,
endIndex: match.index + match[0].length,
line: location.line,
column: location.column,
groups: match.slice(1),
context,
});
matchCount++;
// Prevent infinite loops with global regex
if (!regex.global)
break;
}
return matches;
}
/**
* Find string pattern matches
*/
static findStringMatches(pattern, content) {
const matches = [];
const searchValue = pattern.caseSensitive
? pattern.value
: pattern.value.toLowerCase();
const searchContent = pattern.caseSensitive
? content
: content.toLowerCase();
let startIndex = 0;
let foundIndex;
while ((foundIndex = searchContent.indexOf(searchValue, startIndex)) !== -1) {
// Check whole word constraint if specified
if (pattern.wholeWord &&
!this.isWholeWordMatch(content, foundIndex, pattern.value.length)) {
startIndex = foundIndex + 1;
continue;
}
const location = this.getLocationFromIndex(content, foundIndex);
const context = this.extractContext(content, foundIndex, pattern.value.length);
matches.push({
match: content.substring(foundIndex, foundIndex + pattern.value.length),
startIndex: foundIndex,
endIndex: foundIndex + pattern.value.length,
line: location.line,
column: location.column,
context,
});
startIndex = foundIndex + pattern.value.length;
}
return matches;
}
/**
* Find context-based pattern matches
*/
static findContextMatches(pattern, content) {
const matches = [];
const lines = content.split("\n");
const contextScope = pattern.contextScope || this.DEFAULT_CONTEXT_LINES;
// Find all required patterns first
const requiredMatches = pattern.requiredPatterns
.map((reqPattern) => this.findMatches(reqPattern, content))
.flat();
// For each required match, check if context conditions are met
for (const reqMatch of requiredMatches) {
const lineIndex = reqMatch.line - 1;
const startLine = Math.max(0, lineIndex - contextScope);
const endLine = Math.min(lines.length - 1, lineIndex + contextScope);
const contextLines = lines.slice(startLine, endLine + 1);
const contextContent = contextLines.join("\n");
// Check if any excluded patterns are present in context
let hasExcludedPattern = false;
if (pattern.excludedPatterns) {
for (const excludedPattern of pattern.excludedPatterns) {
const excludedMatches = this.findMatches(excludedPattern, contextContent);
if (excludedMatches.length > 0) {
hasExcludedPattern = true;
break;
}
}
}
// Add match if no excluded patterns found
if (!hasExcludedPattern) {
matches.push({
...reqMatch,
context: contextContent,
});
}
}
return matches;
}
/**
* Check if a string match represents a whole word
*/
static isWholeWordMatch(content, startIndex, length) {
const wordRegex = /\w/;
// Check character before match
if (startIndex > 0) {
const charBefore = content[startIndex - 1];
if (wordRegex.test(charBefore)) {
return false;
}
}
// Check character after match
const endIndex = startIndex + length;
if (endIndex < content.length) {
const charAfter = content[endIndex];
if (wordRegex.test(charAfter)) {
return false;
}
}
return true;
}
/**
* Get line and column from character index
*/
static getLocationFromIndex(content, index) {
const beforeMatch = content.substring(0, index);
const lines = beforeMatch.split("\n");
return {
line: lines.length,
column: lines[lines.length - 1].length + 1,
};
}
/**
* Extract surrounding context for a match
*/
static extractContext(content, startIndex, contextLines = this.DEFAULT_CONTEXT_LINES) {
const lines = content.split("\n");
const location = this.getLocationFromIndex(content, startIndex);
const lineIndex = location.line - 1;
const startLine = Math.max(0, lineIndex - contextLines);
const endLine = Math.min(lines.length - 1, lineIndex + contextLines);
return lines.slice(startLine, endLine + 1).join("\n");
}
/**
* Apply a pattern definition to content and return results
*/
static applyPattern(patternDef, content, filePath) {
const matches = this.findMatches(patternDef.pattern, content);
return {
pattern: patternDef,
matches,
filePath,
requiresValidation: patternDef.pattern.type === "context" ||
matches.some((m) => m.groups && m.groups.length > 0),
};
}
/**
* Calculate entropy of a string (useful for secret detection)
*/
static calculateEntropy(str) {
const frequencies = {};
// Count character frequencies
for (const char of str) {
frequencies[char] = (frequencies[char] || 0) + 1;
}
// Calculate entropy
let entropy = 0;
const length = str.length;
for (const count of Object.values(frequencies)) {
const probability = count / length;
entropy -= probability * Math.log2(probability);
}
return entropy;
}
/**
* Check if a string looks like a potential secret based on entropy and patterns
*/
static isPotentialSecret(str, minEntropy = 4.5, minLength = 16) {
if (str.length < minLength) {
return false;
}
const entropy = this.calculateEntropy(str);
if (entropy < minEntropy) {
return false;
}
// Additional heuristics for secret-like strings
const hasNumbers = /\d/.test(str);
const hasLetters = /[a-zA-Z]/.test(str);
const hasSpecialChars = /[^a-zA-Z0-9]/.test(str);
// Likely a secret if it has mixed character types
return (hasNumbers && hasLetters) || hasSpecialChars;
}
}
exports.PatternMatcher = PatternMatcher;
PatternMatcher.DEFAULT_CONTEXT_LINES = 3;
PatternMatcher.MAX_MATCHES_PER_PATTERN = 1000;