Add CLI, utils, and fixers modules
This commit is contained in:
212
src/fixers/__init__.py
Normal file
212
src/fixers/__init__.py
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
"""Fixer base class and implementations."""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
import tree_sitter
|
||||||
|
|
||||||
|
from src.analyzers.base import Finding
|
||||||
|
|
||||||
|
|
||||||
|
class Fixer(ABC):
|
||||||
|
"""Abstract base class for code fixers."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def can_fix(self, finding: Finding) -> bool:
|
||||||
|
"""Check if this fixer can handle the finding."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def fix(
|
||||||
|
self, source_code: str, finding: Finding, tree: tree_sitter.Tree
|
||||||
|
) -> str:
|
||||||
|
"""Apply fix to source code."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def rule_id(self) -> str:
|
||||||
|
"""Return the rule ID this fixer handles."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SQLInjectionFixer(Fixer):
|
||||||
|
"""Fixer for SQL injection vulnerabilities."""
|
||||||
|
|
||||||
|
def rule_id(self) -> str:
|
||||||
|
return "security.sql_injection"
|
||||||
|
|
||||||
|
def can_fix(self, finding: Finding) -> bool:
|
||||||
|
return finding.rule_id == self.rule_id()
|
||||||
|
|
||||||
|
def fix(
|
||||||
|
self, source_code: str, finding: Finding, tree: tree_sitter.Tree
|
||||||
|
) -> str:
|
||||||
|
lines = source_code.split("\n")
|
||||||
|
line_idx = finding.line_number - 1
|
||||||
|
|
||||||
|
if 0 <= line_idx < len(lines):
|
||||||
|
original_line = lines[line_idx]
|
||||||
|
fixed_line = self._fix_line(original_line)
|
||||||
|
lines[line_idx] = fixed_line
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _fix_line(self, line: str) -> str:
|
||||||
|
if "execute(" in line:
|
||||||
|
return line.replace("%s", "?").replace("f\"", "\"").replace("'", "\"")
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
class EvalUsageFixer(Fixer):
|
||||||
|
"""Fixer for eval/exec usage."""
|
||||||
|
|
||||||
|
def rule_id(self) -> str:
|
||||||
|
return "security.eval_usage"
|
||||||
|
|
||||||
|
def can_fix(self, finding: Finding) -> bool:
|
||||||
|
return finding.rule_id == self.rule_id()
|
||||||
|
|
||||||
|
def fix(
|
||||||
|
self, source_code: str, finding: Finding, tree: tree_sitter.Tree
|
||||||
|
) -> str:
|
||||||
|
lines = source_code.split("\n")
|
||||||
|
line_idx = finding.line_number - 1
|
||||||
|
|
||||||
|
if 0 <= line_idx < len(lines):
|
||||||
|
original_line = lines[line_idx]
|
||||||
|
fixed_line = self._fix_line(original_line)
|
||||||
|
lines[line_idx] = fixed_line
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _fix_line(self, line: str) -> str:
|
||||||
|
if "eval(" in line:
|
||||||
|
return line.replace("eval(", "# eval( # Warning: eval is dangerous - ")
|
||||||
|
if "exec(" in line:
|
||||||
|
return line.replace("exec(", "# exec( # Warning: exec is dangerous - ")
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
class ExceptionSwallowFixer(Fixer):
|
||||||
|
"""Fixer for exception swallowing."""
|
||||||
|
|
||||||
|
def rule_id(self) -> str:
|
||||||
|
return "antipattern.exception_swallow"
|
||||||
|
|
||||||
|
def can_fix(self, finding: Finding) -> bool:
|
||||||
|
return finding.rule_id == self.rule_id()
|
||||||
|
|
||||||
|
def fix(
|
||||||
|
self, source_code: str, finding: Finding, tree: tree_sitter.Tree
|
||||||
|
) -> str:
|
||||||
|
lines = source_code.split("\n")
|
||||||
|
line_idx = finding.line_number - 1
|
||||||
|
|
||||||
|
if 0 <= line_idx < len(lines):
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if "except:" in line or "except ()" in line:
|
||||||
|
lines[i] = line.replace("except:", "except Exception as e:").replace("except ()", "except Exception as e:")
|
||||||
|
elif line.strip() == "pass":
|
||||||
|
lines[i] = line.replace("pass", "logger.error(f'Exception occurred: {e}')")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _fix_line(self, line: str) -> str:
|
||||||
|
if "except:" in line or "except ():" in line:
|
||||||
|
return line.replace("except:", "except Exception as e:").replace("except ():", "except Exception as e:")
|
||||||
|
if "pass" in line:
|
||||||
|
return line.replace("pass", "logger.error(f'Exception occurred: {e}')")
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
class MagicNumberFixer(Fixer):
|
||||||
|
"""Fixer for magic numbers."""
|
||||||
|
|
||||||
|
def rule_id(self) -> str:
|
||||||
|
return "antipattern.magic_number"
|
||||||
|
|
||||||
|
def can_fix(self, finding: Finding) -> bool:
|
||||||
|
return finding.rule_id == self.rule_id()
|
||||||
|
|
||||||
|
def fix(
|
||||||
|
self, source_code: str, finding: Finding, tree: tree_sitter.Tree
|
||||||
|
) -> str:
|
||||||
|
lines = source_code.split("\n")
|
||||||
|
line_idx = finding.line_number - 1
|
||||||
|
|
||||||
|
if 0 <= line_idx < len(lines):
|
||||||
|
original_line = lines[line_idx]
|
||||||
|
fixed_line = self._fix_line(original_line)
|
||||||
|
lines[line_idx] = fixed_line
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _fix_line(self, line: str) -> str:
|
||||||
|
import re
|
||||||
|
magic_pattern = r"(\d{2,})"
|
||||||
|
matches = list(re.finditer(magic_pattern, line))
|
||||||
|
if matches:
|
||||||
|
last_match = matches[-1]
|
||||||
|
num = last_match.group(1)
|
||||||
|
line = line[:last_match.start()] + f"MAGIC_NUMBER_{num}" + line[last_match.end():]
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
class RedundantOperationFixer(Fixer):
|
||||||
|
"""Fixer for redundant operations."""
|
||||||
|
|
||||||
|
def rule_id(self) -> str:
|
||||||
|
return "performance.redundant_operation"
|
||||||
|
|
||||||
|
def can_fix(self, finding: Finding) -> bool:
|
||||||
|
return finding.rule_id == self.rule_id()
|
||||||
|
|
||||||
|
def fix(
|
||||||
|
self, source_code: str, finding: Finding, tree: tree_sitter.Tree
|
||||||
|
) -> str:
|
||||||
|
lines = source_code.split("\n")
|
||||||
|
line_idx = finding.line_number - 1
|
||||||
|
|
||||||
|
if 0 <= line_idx < len(lines):
|
||||||
|
original_line = lines[line_idx]
|
||||||
|
fixed_line = self._fix_line(original_line)
|
||||||
|
lines[line_idx] = fixed_line
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _fix_line(self, line: str) -> str:
|
||||||
|
if "list(list(" in line:
|
||||||
|
return line.replace("list(list(", "list(")
|
||||||
|
if "str(str(" in line:
|
||||||
|
return line.replace("str(str(", "str(")
|
||||||
|
if "dict(dict(" in line:
|
||||||
|
return line.replace("dict(dict(", "dict(")
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
class FixerRegistry:
|
||||||
|
"""Registry for code fixers."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._fixers: dict[str, Fixer] = {
|
||||||
|
"security.sql_injection": SQLInjectionFixer(),
|
||||||
|
"security.eval_usage": EvalUsageFixer(),
|
||||||
|
"antipattern.exception_swallow": ExceptionSwallowFixer(),
|
||||||
|
"antipattern.magic_number": MagicNumberFixer(),
|
||||||
|
"performance.redundant_operation": RedundantOperationFixer(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_fixer(self, rule_id: str) -> Optional[Fixer]:
|
||||||
|
return self._fixers.get(rule_id)
|
||||||
|
|
||||||
|
def can_fix(self, finding: Finding) -> bool:
|
||||||
|
fixer = self._fixers.get(finding.rule_id)
|
||||||
|
return fixer is not None and fixer.can_fix(finding)
|
||||||
|
|
||||||
|
def fix(
|
||||||
|
self, source_code: str, finding: Finding, tree: tree_sitter.Tree
|
||||||
|
) -> str:
|
||||||
|
fixer = self._fixers.get(finding.rule_id)
|
||||||
|
if fixer and fixer.can_fix(finding):
|
||||||
|
return fixer.fix(source_code, finding, tree)
|
||||||
|
return source_code
|
||||||
Reference in New Issue
Block a user