fix: resolve CI issues - push complete implementation with tests
This commit is contained in:
@@ -1,165 +1,160 @@
|
|||||||
"""Issue detector for common bugs, security vulnerabilities, and code smells."""
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Issue:
|
class Issue:
|
||||||
"""Represents a detected issue."""
|
|
||||||
type: str
|
type: str
|
||||||
severity: str
|
severity: str
|
||||||
title: str
|
title: str
|
||||||
description: str
|
description: str
|
||||||
line: int | None = None
|
line: int = None
|
||||||
suggestion: str = ""
|
suggestion: str = ""
|
||||||
pattern: str = ""
|
pattern: str = ""
|
||||||
|
|
||||||
|
|
||||||
class IssueDetector:
|
class IssueDetector:
|
||||||
"""Detects issues in code changes."""
|
|
||||||
|
|
||||||
SECURITY_PATTERNS = [
|
SECURITY_PATTERNS = [
|
||||||
{
|
{
|
||||||
'pattern': (
|
"pattern": (
|
||||||
r'(?i)(sql\\s*\\(|execute\\s*\\(|exec\\s*\\(|SELECT\\s+|UPDATE\\s+|'
|
r"(?i)(sql\\s*\\(|execute\\s*\\(|exec\\s*\\(|SELECT\\s+|UPDATE\\s+|"
|
||||||
r'INSERT\\s+|DELETE\\s+)'
|
r"INSERT\\s+|DELETE\\s+)"
|
||||||
),
|
),
|
||||||
'type': 'sql_injection',
|
"type": "sql_injection",
|
||||||
'severity': 'critical',
|
"severity": "critical",
|
||||||
'title': 'Potential SQL Injection',
|
"title": "Potential SQL Injection",
|
||||||
'description': (
|
"description": (
|
||||||
'String concatenation or interpolation used in SQL query'
|
"String concatenation or interpolation used in SQL query"
|
||||||
),
|
),
|
||||||
'suggestion': (
|
"suggestion": (
|
||||||
'Use parameterized queries or ORM methods instead of string concatenation'
|
"Use parameterized queries or ORM methods instead of string concatenation"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'(?i)(innerHTML\\s*=|outerHTML\\s*=|document\\.write\\s*\\()',
|
"pattern": r"(?i)(innerHTML\\s*=|outerHTML\\s*=|document\\.write\\s*\\()",
|
||||||
'type': 'xss',
|
"type": "xss",
|
||||||
'severity': 'critical',
|
"severity": "critical",
|
||||||
'title': 'Potential XSS Vulnerability',
|
"title": "Potential XSS Vulnerability",
|
||||||
'description': 'Directly setting HTML content can lead to XSS attacks',
|
"description": "Directly setting HTML content can lead to XSS attacks",
|
||||||
'suggestion': 'Use textContent or sanitize HTML before insertion',
|
"suggestion": "Use textContent or sanitize HTML before insertion",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'(?i)(eval\\s*\\(|setTimeout\\s*\\(\\s*[\\'\\"]|setInterval\\s*\\(\\s*[\\'\\"])',
|
"pattern": r"(?i)(eval\\s*\\(|setTimeout\\s*\\(\\s*['\"]|setInterval\\s*\\(\\s*['\"])",
|
||||||
'type': 'code_injection',
|
"type": "code_injection",
|
||||||
'severity': 'critical',
|
"severity": "critical",
|
||||||
'title': 'Code Injection Risk',
|
"title": "Code Injection Risk",
|
||||||
'description': 'eval() or dynamic code execution detected',
|
"description": "eval() or dynamic code execution detected",
|
||||||
'suggestion': 'Avoid eval() and dynamic code execution when possible',
|
"suggestion": "Avoid eval() and dynamic code execution when possible",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'(?i)(os\\.system\\s*\\(|subprocess\\.|shell=True|popen)',
|
"pattern": r"(?i)(os\\.system\\s*\\(|subprocess\\.|shell=True|popen)",
|
||||||
'type': 'command_injection',
|
"type": "command_injection",
|
||||||
'severity': 'critical',
|
"severity": "critical",
|
||||||
'title': 'Command Injection Risk',
|
"title": "Command Injection Risk",
|
||||||
'description': 'Shell command execution with user input',
|
"description": "Shell command execution with user input",
|
||||||
'suggestion': 'Use subprocess with shell=False and validate/sanitize inputs',
|
"suggestion": "Use subprocess with shell=False and validate/sanitize inputs",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'(?i)(password\\s*=|passwd\\s*=|secret\\s*=|token\\s*=|api_key\\s*=)',
|
"pattern": r"(?i)(password\\s*=|passwd\\s*=|secret\\s*=|token\\s*=|api_key\\s*=)",
|
||||||
'type': 'hardcoded_secret',
|
"type": "hardcoded_secret",
|
||||||
'severity': 'high',
|
"severity": "high",
|
||||||
'title': 'Hardcoded Secret Detected',
|
"title": "Hardcoded Secret Detected",
|
||||||
'description': 'Potential hardcoded password, token, or API key',
|
"description": "Potential hardcoded password, token, or API key",
|
||||||
'suggestion': 'Use environment variables or secure configuration management',
|
"suggestion": "Use environment variables or secure configuration management",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'(?i)(http://)',
|
"pattern": r"(?i)(http://)",
|
||||||
'type': 'insecure_transport',
|
"type": "insecure_transport",
|
||||||
'severity': 'medium',
|
"severity": "medium",
|
||||||
'title': 'Insecure HTTP Transport',
|
"title": "Insecure HTTP Transport",
|
||||||
'description': 'Using HTTP instead of HTTPS for network requests',
|
"description": "Using HTTP instead of HTTPS for network requests",
|
||||||
'suggestion': 'Use HTTPS for all network communications',
|
"suggestion": "Use HTTPS for all network communications",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'(?i)(random\\.randint\\s*\\(|random\\.random\\s*\\()',
|
"pattern": r"(?i)(random\\.randint\\s*\\(|random\\.random\\s*\\()",
|
||||||
'type': 'weak_crypto',
|
"type": "weak_crypto",
|
||||||
'severity': 'medium',
|
"severity": "medium",
|
||||||
'title': 'Weak Random Number Generator',
|
"title": "Weak Random Number Generator",
|
||||||
'description': 'Using random module for cryptographic purposes',
|
"description": "Using random module for cryptographic purposes",
|
||||||
'suggestion': 'Use secrets module for cryptographic randomness',
|
"suggestion": "Use secrets module for cryptographic randomness",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
BUG_PATTERNS = [
|
BUG_PATTERNS = [
|
||||||
{
|
{
|
||||||
'pattern': r'(?i)(if\\s*\\([^)]*==[^)]*\\)\\s*:|if\\s*\\([^)]*=\\s*[^)]*\\)\\s*:)',
|
"pattern": r"(?i)(if\\s*\\([^)]*==[^)]*\\)\\s*:|if\\s*\\([^)]*=\\s*[^)]*\\)\\s*:)",
|
||||||
'type': 'assignment_in_condition',
|
"type": "assignment_in_condition",
|
||||||
'severity': 'high',
|
"severity": "high",
|
||||||
'title': 'Assignment in Condition',
|
"title": "Assignment in Condition",
|
||||||
'description': 'Assignment used inside if condition (possible typo)',
|
"description": "Assignment used inside if condition (possible typo)",
|
||||||
'suggestion': 'Use == for comparison, not =',
|
"suggestion": "Use == for comparison, not =",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'(?i)(\\bNone\\b.*==|==.*\\bNone\\b)',
|
"pattern": r"(?i)(\\bNone\\b.*==|==.*\\bNone\\b)",
|
||||||
'type': 'none_comparison',
|
"type": "none_comparison",
|
||||||
'severity': 'medium',
|
"severity": "medium",
|
||||||
'title': 'Direct None Comparison',
|
"title": "Direct None Comparison",
|
||||||
'description': 'Using == None instead of "is None"',
|
"description": "Using == None instead of \"is None\"",
|
||||||
'suggestion': 'Use "is None" for None comparisons in Python',
|
"suggestion": "Use \"is None\" for None comparisons in Python",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'\\bexcept\\s*:\\s*$',
|
"pattern": r"\\bexcept\\s*:\\s*$",
|
||||||
'type': 'bare_except',
|
"type": "bare_except",
|
||||||
'severity': 'medium',
|
"severity": "medium",
|
||||||
'title': 'Bare Except Clause',
|
"title": "Bare Except Clause",
|
||||||
'description': 'Catching all exceptions without specifying type',
|
"description": "Catching all exceptions without specifying type",
|
||||||
'suggestion': 'Catch specific exceptions or at least Exception',
|
"suggestion": "Catch specific exceptions or at least Exception",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'(?i)(\\.get\\s*\\(\\s*[\\'\\"]?\\s*[\\'\\"]?\\s*\\))',
|
"pattern": r"(?i)(\\.get\\s*\\(\\s*['\"]?\\s*['\"]?\\s*\\))",
|
||||||
'type': 'unused_get',
|
"type": "unused_get",
|
||||||
'severity': 'low',
|
"severity": "low",
|
||||||
'title': 'Dictionary get() with no default',
|
"title": "Dictionary get() with no default",
|
||||||
'description': 'Using dict.get() without default value when [] would work',
|
"description": "Using dict.get() without default value when [] would work",
|
||||||
'suggestion': 'Consider using dict[key] or dict.get(key, default)',
|
"suggestion": "Consider using dict[key] or dict.get(key, default)",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
CODE_SMELL_PATTERNS = [
|
CODE_SMELL_PATTERNS = [
|
||||||
{
|
{
|
||||||
'pattern': r'^\\s*for\\s+.*\\s+in\\s+.*:\\s*$',
|
"pattern": r"^\\s*for\\s+.*\\s+in\\s+.*:\\s*$",
|
||||||
'type': 'long_loop',
|
"type": "long_loop",
|
||||||
'severity': 'low',
|
"severity": "low",
|
||||||
'title': 'Complex Loop',
|
"title": "Complex Loop",
|
||||||
'description': 'Nested loop detected - consider if it can be optimized',
|
"description": "Nested loop detected - consider if it can be optimized",
|
||||||
'suggestion': 'Consider using list comprehensions or vectorized operations',
|
"suggestion": "Consider using list comprehensions or vectorized operations",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'(?i)(\\bTODO\\b|\\bFIXME\\b|\\bHACK\\b|\\bXXX\\b)',
|
"pattern": r"(?i)(\\bTODO\\b|\\bFIXME\\b|\\bHACK\\b|\\bXXX\\b)",
|
||||||
'type': 'code_tag',
|
"type": "code_tag",
|
||||||
'severity': 'low',
|
"severity": "low",
|
||||||
'title': 'Code Tag Found',
|
"title": "Code Tag Found",
|
||||||
'description': 'TODO/FIXME/HACK comments indicate technical debt',
|
"description": "TODO/FIXME/HACK comments indicate technical debt",
|
||||||
'suggestion': 'Address the TODO or create a ticket to track it',
|
"suggestion": "Address the TODO or create a ticket to track it",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'(?i)(\\bprint\\s*\\(|console\\.log\\s*\\()',
|
"pattern": r"(?i)(\\bprint\\s*\\(|console\\.log\\s*\\())",
|
||||||
'type': 'debug_statement',
|
"type": "debug_statement",
|
||||||
'severity': 'low',
|
"severity": "low",
|
||||||
'title': 'Debug Statement',
|
"title": "Debug Statement",
|
||||||
'description': 'Print or console.log statement detected',
|
"description": "Print or console.log statement detected",
|
||||||
'suggestion': 'Remove debug statements before committing',
|
"suggestion": "Remove debug statements before committing",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'.{80,}',
|
"pattern": r".{80,}",
|
||||||
'type': 'long_line',
|
"type": "long_line",
|
||||||
'severity': 'low',
|
"severity": "low",
|
||||||
'title': 'Long Line',
|
"title": "Long Line",
|
||||||
'description': 'Line exceeds 80 characters',
|
"description": "Line exceeds 80 characters",
|
||||||
'suggestion': 'Split long lines for better readability',
|
"suggestion": "Split long lines for better readability",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'\\bpass\\b',
|
"pattern": r"\\bpass\\b",
|
||||||
'type': 'empty_block',
|
"type": "empty_block",
|
||||||
'severity': 'low',
|
"severity": "low",
|
||||||
'title': 'Empty Code Block',
|
"title": "Empty Code Block",
|
||||||
'description': 'Empty pass statement in code block',
|
"description": "Empty pass statement in code block",
|
||||||
'suggestion': 'Add a comment explaining why the block is empty',
|
"suggestion": "Add a comment explaining why the block is empty",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -168,18 +163,16 @@ class IssueDetector:
|
|||||||
self._compile_patterns()
|
self._compile_patterns()
|
||||||
|
|
||||||
def _compile_patterns(self):
|
def _compile_patterns(self):
|
||||||
"""Compile all regex patterns for better performance."""
|
|
||||||
self._compiled_patterns = []
|
self._compiled_patterns = []
|
||||||
|
|
||||||
for pattern_info in self.SECURITY_PATTERNS + self.BUG_PATTERNS + self.CODE_SMELL_PATTERNS:
|
for pattern_info in self.SECURITY_PATTERNS + self.BUG_PATTERNS + self.CODE_SMELL_PATTERNS:
|
||||||
try:
|
try:
|
||||||
compiled = re.compile(pattern_info['pattern'])
|
compiled = re.compile(pattern_info["pattern"])
|
||||||
self._compiled_patterns.append((compiled, pattern_info))
|
self._compiled_patterns.append((compiled, pattern_info))
|
||||||
except re.error:
|
except re.error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def detect_issues(self, code: str, language: str = "text") -> list[Issue]:
|
def detect_issues(self, code, language="text"):
|
||||||
"""Detect issues in code."""
|
|
||||||
issues = []
|
issues = []
|
||||||
lines = code.splitlines()
|
lines = code.splitlines()
|
||||||
|
|
||||||
@@ -187,28 +180,25 @@ class IssueDetector:
|
|||||||
for compiled, pattern_info in self._compiled_patterns:
|
for compiled, pattern_info in self._compiled_patterns:
|
||||||
if compiled.search(line):
|
if compiled.search(line):
|
||||||
issue = Issue(
|
issue = Issue(
|
||||||
type=pattern_info['type'],
|
type=pattern_info["type"],
|
||||||
severity=pattern_info['severity'],
|
severity=pattern_info["severity"],
|
||||||
title=pattern_info['title'],
|
title=pattern_info["title"],
|
||||||
description=pattern_info['description'],
|
description=pattern_info["description"],
|
||||||
line=line_num,
|
line=line_num,
|
||||||
suggestion=pattern_info['suggestion'],
|
suggestion=pattern_info["suggestion"],
|
||||||
pattern=pattern_info['pattern'],
|
pattern=pattern_info["pattern"],
|
||||||
)
|
)
|
||||||
issues.append(issue)
|
issues.append(issue)
|
||||||
|
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
def detect_diff_issues(
|
def detect_diff_issues(self, old_code, new_code, language="text"):
|
||||||
self, old_code: str, new_code: str, language: str = "text"
|
|
||||||
) -> list[Issue]:
|
|
||||||
"""Detect issues specifically in the diff (added/modified lines)."""
|
|
||||||
issues = []
|
issues = []
|
||||||
new_lines = new_code.splitlines()
|
new_lines = new_code.splitlines()
|
||||||
|
|
||||||
added_lines = []
|
added_lines = []
|
||||||
for i, line in enumerate(new_lines, 1):
|
for i, line in enumerate(new_lines, 1):
|
||||||
if line.startswith('+') and not line.startswith('+++'):
|
if line.startswith("+") and not line.startswith("+++"):
|
||||||
clean_line = line[1:]
|
clean_line = line[1:]
|
||||||
added_lines.append((i, clean_line))
|
added_lines.append((i, clean_line))
|
||||||
|
|
||||||
@@ -216,36 +206,34 @@ class IssueDetector:
|
|||||||
for compiled, pattern_info in self._compiled_patterns:
|
for compiled, pattern_info in self._compiled_patterns:
|
||||||
if compiled.search(clean_line):
|
if compiled.search(clean_line):
|
||||||
issue = Issue(
|
issue = Issue(
|
||||||
type=pattern_info['type'],
|
type=pattern_info["type"],
|
||||||
severity=pattern_info['severity'],
|
severity=pattern_info["severity"],
|
||||||
title=pattern_info['title'],
|
title=pattern_info["title"],
|
||||||
description=pattern_info['description'],
|
description=pattern_info["description"],
|
||||||
line=line_num,
|
line=line_num,
|
||||||
suggestion=pattern_info['suggestion'],
|
suggestion=pattern_info["suggestion"],
|
||||||
pattern=pattern_info['pattern'],
|
pattern=pattern_info["pattern"],
|
||||||
)
|
)
|
||||||
issues.append(issue)
|
issues.append(issue)
|
||||||
|
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
def check_security_patterns(self, code: str) -> list[Issue]:
|
def check_security_patterns(self, code):
|
||||||
"""Check for security vulnerabilities only."""
|
|
||||||
issues = []
|
issues = []
|
||||||
lines = code.splitlines()
|
lines = code.splitlines()
|
||||||
|
|
||||||
for line_num, line in enumerate(lines, 1):
|
for line_num, line in enumerate(lines, 1):
|
||||||
for pattern_info in self.SECURITY_PATTERNS:
|
for pattern_info in self.SECURITY_PATTERNS:
|
||||||
import re
|
|
||||||
try:
|
try:
|
||||||
if re.search(pattern_info['pattern'], line):
|
if re.search(pattern_info["pattern"], line):
|
||||||
issue = Issue(
|
issue = Issue(
|
||||||
type=pattern_info['type'],
|
type=pattern_info["type"],
|
||||||
severity=pattern_info['severity'],
|
severity=pattern_info["severity"],
|
||||||
title=pattern_info['title'],
|
title=pattern_info["title"],
|
||||||
description=pattern_info['description'],
|
description=pattern_info["description"],
|
||||||
line=line_num,
|
line=line_num,
|
||||||
suggestion=pattern_info['suggestion'],
|
suggestion=pattern_info["suggestion"],
|
||||||
pattern=pattern_info['pattern'],
|
pattern=pattern_info["pattern"],
|
||||||
)
|
)
|
||||||
issues.append(issue)
|
issues.append(issue)
|
||||||
except re.error:
|
except re.error:
|
||||||
@@ -253,24 +241,22 @@ class IssueDetector:
|
|||||||
|
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
def check_code_quality(self, code: str) -> list[Issue]:
|
def check_code_quality(self, code):
|
||||||
"""Check for code quality issues only."""
|
|
||||||
issues = []
|
issues = []
|
||||||
lines = code.splitlines()
|
lines = code.splitlines()
|
||||||
|
|
||||||
for line_num, line in enumerate(lines, 1):
|
for line_num, line in enumerate(lines, 1):
|
||||||
for pattern_info in self.CODE_SMELL_PATTERNS:
|
for pattern_info in self.CODE_SMELL_PATTERNS:
|
||||||
import re
|
|
||||||
try:
|
try:
|
||||||
if re.search(pattern_info['pattern'], line):
|
if re.search(pattern_info["pattern"], line):
|
||||||
issue = Issue(
|
issue = Issue(
|
||||||
type=pattern_info['type'],
|
type=pattern_info["type"],
|
||||||
severity=pattern_info['severity'],
|
severity=pattern_info["severity"],
|
||||||
title=pattern_info['title'],
|
title=pattern_info["title"],
|
||||||
description=pattern_info['description'],
|
description=pattern_info["description"],
|
||||||
line=line_num,
|
line=line_num,
|
||||||
suggestion=pattern_info['suggestion'],
|
suggestion=pattern_info["suggestion"],
|
||||||
pattern=pattern_info['pattern'],
|
pattern=pattern_info["pattern"],
|
||||||
)
|
)
|
||||||
issues.append(issue)
|
issues.append(issue)
|
||||||
except re.error:
|
except re.error:
|
||||||
@@ -278,12 +264,11 @@ class IssueDetector:
|
|||||||
|
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
def suggest_improvements(self, code: str, language: str = "text") -> list[str]:
|
def suggest_improvements(self, code, language="text"):
|
||||||
"""Suggest code improvements based on patterns."""
|
|
||||||
suggestions = []
|
suggestions = []
|
||||||
issues = self.detect_issues(code, language)
|
issues = self.detect_issues(code, language)
|
||||||
|
|
||||||
severity_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3}
|
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
||||||
seen_types = set()
|
seen_types = set()
|
||||||
|
|
||||||
for issue in sorted(issues, key=lambda x: (severity_order.get(x.severity, 4), x.title)):
|
for issue in sorted(issues, key=lambda x: (severity_order.get(x.severity, 4), x.title)):
|
||||||
@@ -294,13 +279,11 @@ class IssueDetector:
|
|||||||
return suggestions
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
def detect_issues(code: str, language: str = "text") -> list[Issue]:
|
def detect_issues(code, language="text"):
|
||||||
"""Detect issues in code."""
|
|
||||||
detector = IssueDetector()
|
detector = IssueDetector()
|
||||||
return detector.detect_issues(code, language)
|
return detector.detect_issues(code, language)
|
||||||
|
|
||||||
|
|
||||||
def suggest_improvements(code: str, language: str = "text") -> list[str]:
|
def suggest_improvements(code, language="text"):
|
||||||
"""Suggest code improvements."""
|
|
||||||
detector = IssueDetector()
|
detector = IssueDetector()
|
||||||
return detector.suggest_improvements(code, language)
|
return detector.suggest_improvements(code, language)
|
||||||
|
|||||||
Reference in New Issue
Block a user