fix: Address high-severity bandit issues

This commit is contained in:
DSS
2025-12-11 07:13:06 -03:00
parent bcb4475744
commit 5b2a328dd1
167 changed files with 7051 additions and 7168 deletions

View File

@@ -1,5 +1,5 @@
"""
React Project Analyzer
React Project Analyzer.
Analyzes React codebases to extract component information,
detect patterns, and identify style usage.
@@ -7,90 +7,58 @@ detect patterns, and identify style usage.
import re
from pathlib import Path
from typing import List, Dict, Any, Optional, Set, Tuple
from dataclasses import dataclass, field
from .base import (
ComponentInfo,
Location,
StylePattern,
StylingApproach,
)
from typing import Any, Dict, List, Optional, Set
from .base import ComponentInfo, Location
# Patterns for React component detection
FUNCTIONAL_COMPONENT = re.compile(
r'(?:export\s+)?(?:const|let|var|function)\s+([A-Z][A-Za-z0-9]*)\s*(?::\s*(?:React\.)?FC)?'
r'\s*(?:=\s*(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>|\()',
re.MULTILINE
r"(?:export\s+)?(?:const|let|var|function)\s+([A-Z][A-Za-z0-9]*)\s*(?::\s*(?:React\.)?FC)?"
r"\s*(?:=\s*(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>|\()",
re.MULTILINE,
)
CLASS_COMPONENT = re.compile(
r'class\s+([A-Z][A-Za-z0-9]*)\s+extends\s+(?:React\.)?(?:Component|PureComponent)',
re.MULTILINE
r"class\s+([A-Z][A-Za-z0-9]*)\s+extends\s+(?:React\.)?(?:Component|PureComponent)", re.MULTILINE
)
FORWARD_REF = re.compile(
r'(?:export\s+)?(?:const|let)\s+([A-Z][A-Za-z0-9]*)\s*=\s*(?:React\.)?forwardRef',
re.MULTILINE
r"(?:export\s+)?(?:const|let)\s+([A-Z][A-Za-z0-9]*)\s*=\s*(?:React\.)?forwardRef", re.MULTILINE
)
MEMO_COMPONENT = re.compile(
r'(?:export\s+)?(?:const|let)\s+([A-Z][A-Za-z0-9]*)\s*=\s*(?:React\.)?memo\(',
re.MULTILINE
r"(?:export\s+)?(?:const|let)\s+([A-Z][A-Za-z0-9]*)\s*=\s*(?:React\.)?memo\(", re.MULTILINE
)
# Import patterns
IMPORT_PATTERN = re.compile(
r'import\s+(?:\{[^}]+\}|\*\s+as\s+\w+|\w+)\s+from\s+["\']([^"\']+)["\']',
re.MULTILINE
r'import\s+(?:\{[^}]+\}|\*\s+as\s+\w+|\w+)\s+from\s+["\']([^"\']+)["\']', re.MULTILINE
)
STYLE_IMPORT = re.compile(
r'import\s+(?:(\w+)\s+from\s+)?["\']([^"\']+\.(?:css|scss|sass|less|styl))["\']',
re.MULTILINE
r'import\s+(?:(\w+)\s+from\s+)?["\']([^"\']+\.(?:css|scss|sass|less|styl))["\']', re.MULTILINE
)
# Inline style patterns
INLINE_STYLE_OBJECT = re.compile(
r'style\s*=\s*\{\s*\{([^}]+)\}\s*\}',
re.MULTILINE | re.DOTALL
)
INLINE_STYLE_OBJECT = re.compile(r"style\s*=\s*\{\s*\{([^}]+)\}\s*\}", re.MULTILINE | re.DOTALL)
INLINE_STYLE_VAR = re.compile(
r'style\s*=\s*\{(\w+)\}',
re.MULTILINE
)
INLINE_STYLE_VAR = re.compile(r"style\s*=\s*\{(\w+)\}", re.MULTILINE)
# Props extraction
PROPS_DESTRUCTURE = re.compile(
r'\(\s*\{\s*([^}]+)\s*\}\s*(?::\s*[^)]+)?\)',
re.MULTILINE
)
PROPS_DESTRUCTURE = re.compile(r"\(\s*\{\s*([^}]+)\s*\}\s*(?::\s*[^)]+)?\)", re.MULTILINE)
PROPS_INTERFACE = re.compile(
r'interface\s+\w*Props\s*\{([^}]+)\}',
re.MULTILINE | re.DOTALL
)
PROPS_INTERFACE = re.compile(r"interface\s+\w*Props\s*\{([^}]+)\}", re.MULTILINE | re.DOTALL)
PROPS_TYPE = re.compile(
r'type\s+\w*Props\s*=\s*\{([^}]+)\}',
re.MULTILINE | re.DOTALL
)
PROPS_TYPE = re.compile(r"type\s+\w*Props\s*=\s*\{([^}]+)\}", re.MULTILINE | re.DOTALL)
class ReactAnalyzer:
"""
Analyzes React projects for component structure and style usage.
"""
"""Analyzes React projects for component structure and style usage."""
def __init__(self, root_path: str):
self.root = Path(root_path).resolve()
async def analyze(
self,
component_files: Optional[List[Path]] = None
) -> List[ComponentInfo]:
async def analyze(self, component_files: Optional[List[Path]] = None) -> List[ComponentInfo]:
"""
Analyze React components in the project.
@@ -110,7 +78,7 @@ class ReactAnalyzer:
try:
file_components = await self._analyze_file(file_path)
components.extend(file_components)
except Exception as e:
except Exception:
# Log error but continue
continue
@@ -118,21 +86,23 @@ class ReactAnalyzer:
def _find_component_files(self) -> List[Path]:
"""Find all potential React component files."""
skip_dirs = {'node_modules', '.git', 'dist', 'build', '.next'}
skip_dirs = {"node_modules", ".git", "dist", "build", ".next"}
component_files = []
for ext in ['*.jsx', '*.tsx']:
for ext in ["*.jsx", "*.tsx"]:
for path in self.root.rglob(ext):
if not any(skip in path.parts for skip in skip_dirs):
component_files.append(path)
# Also check .js/.ts files that look like components
for ext in ['*.js', '*.ts']:
for ext in ["*.js", "*.ts"]:
for path in self.root.rglob(ext):
if any(skip in path.parts for skip in skip_dirs):
continue
# Skip config and utility files
if any(x in path.name.lower() for x in ['config', 'util', 'helper', 'hook', 'context']):
if any(
x in path.name.lower() for x in ["config", "util", "helper", "hook", "context"]
):
continue
# Check if PascalCase (likely component)
if path.stem[0].isupper():
@@ -142,7 +112,7 @@ class ReactAnalyzer:
async def _analyze_file(self, file_path: Path) -> List[ComponentInfo]:
"""Analyze a single file for React components."""
content = file_path.read_text(encoding='utf-8', errors='ignore')
content = file_path.read_text(encoding="utf-8", errors="ignore")
components = []
# Find all components in the file
@@ -152,22 +122,22 @@ class ReactAnalyzer:
for match in FUNCTIONAL_COMPONENT.finditer(content):
name = match.group(1)
if self._is_valid_component_name(name):
component_matches.append((name, 'functional', match.start()))
component_matches.append((name, "functional", match.start()))
# Class components
for match in CLASS_COMPONENT.finditer(content):
name = match.group(1)
component_matches.append((name, 'class', match.start()))
component_matches.append((name, "class", match.start()))
# forwardRef components
for match in FORWARD_REF.finditer(content):
name = match.group(1)
component_matches.append((name, 'forwardRef', match.start()))
component_matches.append((name, "forwardRef", match.start()))
# memo components
for match in MEMO_COMPONENT.finditer(content):
name = match.group(1)
component_matches.append((name, 'memo', match.start()))
component_matches.append((name, "memo", match.start()))
# Dedupe by name (keep first occurrence)
seen_names = set()
@@ -193,19 +163,21 @@ class ReactAnalyzer:
# Check if component has styles
has_styles = bool(style_files) or bool(inline_styles)
components.append(ComponentInfo(
name=name,
path=str(file_path.relative_to(self.root)),
type=comp_type,
props=props,
has_styles=has_styles,
style_files=style_files,
inline_style_count=len(inline_styles),
imports=imports,
exports=self._find_exports(content, name),
children=children,
line_count=content.count('\n') + 1,
))
components.append(
ComponentInfo(
name=name,
path=str(file_path.relative_to(self.root)),
type=comp_type,
props=props,
has_styles=has_styles,
style_files=style_files,
inline_style_count=len(inline_styles),
imports=imports,
exports=self._find_exports(content, name),
children=children,
line_count=content.count("\n") + 1,
)
)
return components
@@ -217,10 +189,22 @@ class ReactAnalyzer:
# Filter out common non-component patterns
invalid_names = {
'React', 'Component', 'PureComponent', 'Fragment',
'Suspense', 'Provider', 'Consumer', 'Context',
'Error', 'ErrorBoundary', 'Wrapper', 'Container',
'Props', 'State', 'Type', 'Interface',
"React",
"Component",
"PureComponent",
"Fragment",
"Suspense",
"Provider",
"Consumer",
"Context",
"Error",
"ErrorBoundary",
"Wrapper",
"Container",
"Props",
"State",
"Type",
"Interface",
}
return name not in invalid_names
@@ -231,7 +215,7 @@ class ReactAnalyzer:
for match in IMPORT_PATTERN.finditer(content):
import_path = match.group(1)
# Skip node_modules style imports for brevity
if not import_path.startswith('.') and '/' not in import_path:
if not import_path.startswith(".") and "/" not in import_path:
continue
imports.append(import_path)
return imports
@@ -250,11 +234,13 @@ class ReactAnalyzer:
# style={{ ... }}
for match in INLINE_STYLE_OBJECT.finditer(content):
line = content[:match.start()].count('\n') + 1
locations.append(Location(
file_path="", # Will be set by caller
line=line,
))
line = content[: match.start()].count("\n") + 1
locations.append(
Location(
file_path="", # Will be set by caller
line=line,
)
)
return locations
@@ -266,7 +252,7 @@ class ReactAnalyzer:
for match in PROPS_DESTRUCTURE.finditer(content):
props_str = match.group(1)
# Extract prop names from destructuring
for prop in re.findall(r'(\w+)(?:\s*[=:])?', props_str):
for prop in re.findall(r"(\w+)(?:\s*[=:])?", props_str):
if prop and not prop[0].isupper(): # Skip types
props.add(prop)
@@ -275,28 +261,24 @@ class ReactAnalyzer:
for match in pattern.finditer(content):
props_str = match.group(1)
# Extract prop names
for line in props_str.split('\n'):
prop_match = re.match(r'\s*(\w+)\s*[?:]', line)
for line in props_str.split("\n"):
prop_match = re.match(r"\s*(\w+)\s*[?:]", line)
if prop_match:
props.add(prop_match.group(1))
return list(props)
def _find_child_components(
self,
content: str,
current_components: Set[str]
) -> List[str]:
def _find_child_components(self, content: str, current_components: Set[str]) -> List[str]:
"""Find child components used in JSX."""
children = set()
# Find JSX elements that look like components (PascalCase)
jsx_pattern = re.compile(r'<([A-Z][A-Za-z0-9]*)')
jsx_pattern = re.compile(r"<([A-Z][A-Za-z0-9]*)")
for match in jsx_pattern.finditer(content):
component_name = match.group(1)
# Skip current file's components and React built-ins
if component_name not in current_components:
if component_name not in {'Fragment', 'Suspense', 'Provider'}:
if component_name not in {"Fragment", "Suspense", "Provider"}:
children.add(component_name)
return list(children)
@@ -306,16 +288,16 @@ class ReactAnalyzer:
exports = []
# Default export
if re.search(rf'export\s+default\s+{component_name}\b', content):
exports.append('default')
if re.search(rf'export\s+default\s+(?:function|const)\s+{component_name}\b', content):
exports.append('default')
if re.search(rf"export\s+default\s+{component_name}\b", content):
exports.append("default")
if re.search(rf"export\s+default\s+(?:function|const)\s+{component_name}\b", content):
exports.append("default")
# Named export
if re.search(rf'export\s+(?:const|function|class)\s+{component_name}\b', content):
exports.append('named')
if re.search(r'export\s*\{[^}]*\b' + re.escape(component_name) + r'\b[^}]*\}', content):
exports.append('named')
if re.search(rf"export\s+(?:const|function|class)\s+{component_name}\b", content):
exports.append("named")
if re.search(r"export\s*\{[^}]*\b" + re.escape(component_name) + r"\b[^}]*\}", content):
exports.append("named")
return exports
@@ -332,39 +314,44 @@ class ReactAnalyzer:
search_path = Path(path) if path else self.root
results = []
for ext in ['*.jsx', '*.tsx', '*.js', '*.ts']:
for ext in ["*.jsx", "*.tsx", "*.js", "*.ts"]:
for file_path in search_path.rglob(ext):
if any(skip in file_path.parts for skip in
{'node_modules', '.git', 'dist', 'build'}):
if any(
skip in file_path.parts for skip in {"node_modules", ".git", "dist", "build"}
):
continue
try:
content = file_path.read_text(encoding='utf-8', errors='ignore')
content = file_path.read_text(encoding="utf-8", errors="ignore")
# Find style={{ ... }}
for match in INLINE_STYLE_OBJECT.finditer(content):
line = content[:match.start()].count('\n') + 1
line = content[: match.start()].count("\n") + 1
style_content = match.group(1).strip()
results.append({
'file': str(file_path.relative_to(self.root)),
'line': line,
'content': style_content[:200],
'type': 'object',
})
results.append(
{
"file": str(file_path.relative_to(self.root)),
"line": line,
"content": style_content[:200],
"type": "object",
}
)
# Find style={variable}
for match in INLINE_STYLE_VAR.finditer(content):
line = content[:match.start()].count('\n') + 1
line = content[: match.start()].count("\n") + 1
var_name = match.group(1)
results.append({
'file': str(file_path.relative_to(self.root)),
'line': line,
'content': f'style={{{var_name}}}',
'type': 'variable',
'variable': var_name,
})
results.append(
{
"file": str(file_path.relative_to(self.root)),
"line": line,
"content": f"style={{{var_name}}}",
"type": "variable",
"variable": var_name,
}
)
except Exception:
continue
@@ -392,48 +379,50 @@ class ReactAnalyzer:
Returns dict with pattern types and their occurrences.
"""
patterns = {
'inline_styles': [],
'css_modules': [],
'styled_components': [],
'emotion': [],
'tailwind': [],
'css_classes': [],
"inline_styles": [],
"css_modules": [],
"styled_components": [],
"emotion": [],
"tailwind": [],
"css_classes": [],
}
component_files = self._find_component_files()
for file_path in component_files:
try:
content = file_path.read_text(encoding='utf-8', errors='ignore')
content = file_path.read_text(encoding="utf-8", errors="ignore")
rel_path = str(file_path.relative_to(self.root))
# CSS Modules
if re.search(r'import\s+\w+\s+from\s+["\'].*\.module\.', content):
patterns['css_modules'].append({'file': rel_path})
patterns["css_modules"].append({"file": rel_path})
# styled-components
if re.search(r'styled\.|from\s+["\']styled-components', content):
patterns['styled_components'].append({'file': rel_path})
patterns["styled_components"].append({"file": rel_path})
# Emotion
if re.search(r'@emotion|css`', content):
patterns['emotion'].append({'file': rel_path})
if re.search(r"@emotion|css`", content):
patterns["emotion"].append({"file": rel_path})
# Tailwind (className with utility classes)
if re.search(r'className\s*=\s*["\'][^"\']*(?:flex|grid|p-\d|m-\d|bg-)', content):
patterns['tailwind'].append({'file': rel_path})
patterns["tailwind"].append({"file": rel_path})
# Regular CSS classes
if re.search(r'className\s*=\s*["\'][a-zA-Z]', content):
patterns['css_classes'].append({'file': rel_path})
patterns["css_classes"].append({"file": rel_path})
# Inline styles
for match in INLINE_STYLE_OBJECT.finditer(content):
line = content[:match.start()].count('\n') + 1
patterns['inline_styles'].append({
'file': rel_path,
'line': line,
})
line = content[: match.start()].count("\n") + 1
patterns["inline_styles"].append(
{
"file": rel_path,
"line": line,
}
)
except Exception:
continue