qgis-plugin-analyzer 1.4.0__py3-none-any.whl → 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- analyzer/__init__.py +2 -1
- analyzer/cli/__init__.py +14 -0
- analyzer/cli/app.py +147 -0
- analyzer/cli/base.py +93 -0
- analyzer/cli/commands/__init__.py +19 -0
- analyzer/cli/commands/analyze.py +47 -0
- analyzer/cli/commands/fix.py +58 -0
- analyzer/cli/commands/init.py +41 -0
- analyzer/cli/commands/list_rules.py +41 -0
- analyzer/cli/commands/security.py +46 -0
- analyzer/cli/commands/summary.py +52 -0
- analyzer/cli/commands/version.py +41 -0
- analyzer/cli.py +4 -281
- analyzer/commands.py +163 -0
- analyzer/engine.py +491 -245
- analyzer/fixer.py +206 -130
- analyzer/reporters/markdown_reporter.py +88 -14
- analyzer/reporters/summary_reporter.py +226 -49
- analyzer/rules/qgis_rules.py +3 -1
- analyzer/scanner.py +219 -711
- analyzer/secrets.py +84 -0
- analyzer/security_checker.py +85 -0
- analyzer/security_rules.py +127 -0
- analyzer/transformers.py +29 -8
- analyzer/utils/__init__.py +2 -0
- analyzer/utils/path_utils.py +53 -1
- analyzer/validators.py +90 -55
- analyzer/visitors/__init__.py +19 -0
- analyzer/visitors/base.py +75 -0
- analyzer/visitors/composite_visitor.py +73 -0
- analyzer/visitors/imports_visitor.py +85 -0
- analyzer/visitors/metrics_visitor.py +158 -0
- analyzer/visitors/security_visitor.py +52 -0
- analyzer/visitors/standards_visitor.py +284 -0
- {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/METADATA +32 -10
- qgis_plugin_analyzer-1.6.0.dist-info/RECORD +52 -0
- {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/WHEEL +1 -1
- qgis_plugin_analyzer-1.4.0.dist-info/RECORD +0 -30
- {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/entry_points.txt +0 -0
- {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/top_level.txt +0 -0
|
@@ -9,6 +9,36 @@ import json
|
|
|
9
9
|
import pathlib
|
|
10
10
|
from typing import Any, Dict, List
|
|
11
11
|
|
|
12
|
+
# Helper functions for formatting
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def print_header(title: str) -> None:
|
|
16
|
+
"""Print a formatted section header.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
title: The header title to display.
|
|
20
|
+
"""
|
|
21
|
+
print(f"\n\033[1m{title}\033[0m")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def print_separator(char: str = "=", length: int = 45) -> None:
|
|
25
|
+
"""Print a separator line.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
char: The character to use for the separator.
|
|
29
|
+
length: The length of the separator line.
|
|
30
|
+
"""
|
|
31
|
+
print(char * length)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def print_success(message: str) -> None:
|
|
35
|
+
"""Print a success message in green.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
message: The success message to display.
|
|
39
|
+
"""
|
|
40
|
+
print(f"\n\033[92m{message}\033[0m")
|
|
41
|
+
|
|
12
42
|
|
|
13
43
|
def print_colored_score(label: str, score: Any) -> None:
|
|
14
44
|
"""Prints a score with ANSI colors based on its value.
|
|
@@ -39,7 +69,7 @@ def report_summary(input_path: pathlib.Path, by: str = "total") -> bool:
|
|
|
39
69
|
|
|
40
70
|
Args:
|
|
41
71
|
input_path: Path to the project_context.json file.
|
|
42
|
-
by: Granularity level ('total', 'modules', 'functions', 'classes').
|
|
72
|
+
by: Granularity level ('total', 'modules', 'functions', 'classes', 'security').
|
|
43
73
|
|
|
44
74
|
Returns:
|
|
45
75
|
True if the report was successfully generated, False otherwise.
|
|
@@ -60,6 +90,8 @@ def report_summary(input_path: pathlib.Path, by: str = "total") -> bool:
|
|
|
60
90
|
return _report_by_functions(data)
|
|
61
91
|
elif by == "classes":
|
|
62
92
|
return _report_by_classes(data)
|
|
93
|
+
elif by == "security":
|
|
94
|
+
return _report_security(data)
|
|
63
95
|
else:
|
|
64
96
|
print(f"\033[91mError: Unknown summary mode '{by}'\033[0m")
|
|
65
97
|
return False
|
|
@@ -69,65 +101,122 @@ def report_summary(input_path: pathlib.Path, by: str = "total") -> bool:
|
|
|
69
101
|
return False
|
|
70
102
|
|
|
71
103
|
|
|
72
|
-
|
|
73
|
-
"""Prints the executive total summary."""
|
|
74
|
-
print("\n\033[1m📋 QGIS Plugin Analyzer: Project Summary\033[0m")
|
|
75
|
-
print("=" * 45)
|
|
104
|
+
# Specialized methods for _report_total
|
|
76
105
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
106
|
+
|
|
107
|
+
def _print_quality_indicators(metrics: Dict[str, Any]) -> None:
|
|
108
|
+
"""Print quality scores section.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
metrics: Dictionary containing quality metrics.
|
|
112
|
+
"""
|
|
113
|
+
print_header("📊 Quality Indicators")
|
|
80
114
|
print_colored_score("- Module Stability Score", metrics.get("quality_score", "N/A"))
|
|
81
115
|
print_colored_score("- Code Maintainability Score", metrics.get("maintainability_score", "N/A"))
|
|
116
|
+
print_colored_score("- Security Score (Bandit)", metrics.get("security_score", "N/A"))
|
|
117
|
+
|
|
82
118
|
|
|
83
|
-
|
|
84
|
-
research
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
119
|
+
def _print_research_metrics(research: Dict[str, Any]) -> None:
|
|
120
|
+
"""Print research-based metrics section.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
research: Dictionary containing research metrics.
|
|
124
|
+
"""
|
|
125
|
+
if not research:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
print_header("🔬 Research-based Metrics")
|
|
129
|
+
params_cov = research.get("type_hint_coverage", 0)
|
|
130
|
+
returns_cov = research.get("return_hint_coverage", 0)
|
|
131
|
+
doc_cov = research.get("docstring_coverage", 0)
|
|
132
|
+
styles = research.get("detected_docstring_styles", [])
|
|
133
|
+
style = styles[0] if styles else "Unknown"
|
|
134
|
+
|
|
135
|
+
print(f"- Type Hint Coverage (Params): {params_cov:.1f}%")
|
|
136
|
+
print(f"- Type Hint Coverage (Returns): {returns_cov:.1f}%")
|
|
137
|
+
print(f"- Docstring Coverage: {doc_cov:.1f}%")
|
|
138
|
+
print(f"- Documentation Style: {style}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _collect_all_issues(data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
142
|
+
"""Collect and merge AST issues and security findings.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
data: The full analysis results dictionary.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of all issues with file paths added.
|
|
149
|
+
"""
|
|
99
150
|
issues: List[Dict[str, Any]] = []
|
|
151
|
+
|
|
152
|
+
# Collect AST issues
|
|
100
153
|
for module in data.get("modules", []):
|
|
101
154
|
mod_path = module.get("path", "unknown")
|
|
102
155
|
for issue in module.get("ast_issues", []):
|
|
103
156
|
issue["file"] = mod_path
|
|
104
157
|
issues.append(issue)
|
|
105
158
|
|
|
159
|
+
# Add security findings
|
|
160
|
+
security_findings = data.get("security", {}).get("findings", [])
|
|
161
|
+
for finding in security_findings:
|
|
162
|
+
finding["type"] = f"SECURITY:{finding.get('type', 'generic')}"
|
|
163
|
+
issues.append(finding)
|
|
164
|
+
|
|
165
|
+
return issues
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _print_issue_statistics(issues: List[Dict[str, Any]]) -> None:
|
|
169
|
+
"""Print issue counts grouped by type.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
issues: List of all issues.
|
|
173
|
+
"""
|
|
174
|
+
print(f"\n\033[1m⚠️ Issue Statistics ({len(issues)} total)\033[0m")
|
|
175
|
+
counts: Dict[str, int] = {}
|
|
176
|
+
for issue in issues:
|
|
177
|
+
issue_type = issue.get("type", "unknown")
|
|
178
|
+
counts[issue_type] = counts.get(issue_type, 0) + 1
|
|
179
|
+
|
|
180
|
+
for issue_type, count in sorted(counts.items(), key=lambda x: x[1], reverse=True):
|
|
181
|
+
print(f"- {issue_type}: {count}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _print_sample_issues(issues: List[Dict[str, Any]], limit: int = 5) -> None:
|
|
185
|
+
"""Print sample issues with formatting.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
issues: List of all issues.
|
|
189
|
+
limit: Maximum number of issues to display.
|
|
190
|
+
"""
|
|
191
|
+
print_header("🔍 Sample Issues")
|
|
192
|
+
for issue in issues[:limit]:
|
|
193
|
+
severity = issue.get("severity", "info").upper()
|
|
194
|
+
sev_color = "\033[91m" if severity == "ERROR" else "\033[93m"
|
|
195
|
+
print(
|
|
196
|
+
f"{sev_color}[{severity}]\033[0m {issue['file']}:{issue.get('line', '?')} - {issue['message']}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if len(issues) > limit:
|
|
200
|
+
print(f"... and {len(issues) - limit} more issues.")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _report_total(data: Dict[str, Any]) -> bool:
|
|
204
|
+
"""Prints the executive total summary."""
|
|
205
|
+
print_header("📋 QGIS Plugin Analyzer: Project Summary")
|
|
206
|
+
print_separator()
|
|
207
|
+
|
|
208
|
+
_print_quality_indicators(data.get("metrics", {}))
|
|
209
|
+
_print_research_metrics(data.get("research_summary", {}))
|
|
210
|
+
|
|
211
|
+
issues = _collect_all_issues(data)
|
|
106
212
|
if not issues:
|
|
107
|
-
|
|
213
|
+
print_success("✅ No issues detected! Your project looks great.")
|
|
108
214
|
else:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for t, c in sorted(counts.items(), key=lambda x: x[1], reverse=True):
|
|
116
|
-
print(f"- {t}: {c}")
|
|
117
|
-
|
|
118
|
-
# 4. Sample Issues
|
|
119
|
-
print("\n\033[1m🔍 Sample Issues\033[0m")
|
|
120
|
-
for i in issues[:5]:
|
|
121
|
-
severity = i.get("severity", "info").upper()
|
|
122
|
-
sev_color = "\033[91m" if severity == "ERROR" else "\033[93m"
|
|
123
|
-
print(
|
|
124
|
-
f"{sev_color}[{severity}]\033[0m {i['file']}:{i.get('line', '?')} - {i['message']}"
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
if len(issues) > 5:
|
|
128
|
-
print(f"... and {len(issues) - 5} more issues.")
|
|
129
|
-
|
|
130
|
-
print("\n" + "=" * 45)
|
|
215
|
+
_print_issue_statistics(issues)
|
|
216
|
+
_print_sample_issues(issues)
|
|
217
|
+
|
|
218
|
+
print()
|
|
219
|
+
print_separator()
|
|
131
220
|
return True
|
|
132
221
|
|
|
133
222
|
|
|
@@ -144,11 +233,10 @@ def _report_by_modules(data: Dict[str, Any]) -> bool:
|
|
|
144
233
|
# Calculate issues per module
|
|
145
234
|
mod_stats = []
|
|
146
235
|
for m in modules:
|
|
147
|
-
issues_count = len(m.get("ast_issues", []))
|
|
148
236
|
mod_stats.append(
|
|
149
237
|
{
|
|
150
238
|
"path": m.get("path"),
|
|
151
|
-
"issues":
|
|
239
|
+
"issues": len(m.get("ast_issues", [])) + len(m.get("security_issues", [])),
|
|
152
240
|
"complexity": m.get("complexity", 1),
|
|
153
241
|
"lines": m.get("lines", 0),
|
|
154
242
|
}
|
|
@@ -220,3 +308,92 @@ def _report_by_classes(data: Dict[str, Any]) -> bool:
|
|
|
220
308
|
print(f"\nTotal: {len(all_classes)} classes found.")
|
|
221
309
|
print("=" * 60)
|
|
222
310
|
return True
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# Specialized methods for _report_security
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _group_findings_by_severity(findings: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
|
317
|
+
"""Group security findings by severity level.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
findings: List of security findings.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Dictionary mapping severity levels to lists of findings.
|
|
324
|
+
"""
|
|
325
|
+
by_severity: Dict[str, List[Dict[str, Any]]] = {"high": [], "medium": [], "low": []}
|
|
326
|
+
for finding in findings:
|
|
327
|
+
sev = finding.get("severity", "medium").lower()
|
|
328
|
+
if sev in by_severity:
|
|
329
|
+
by_severity[sev].append(finding)
|
|
330
|
+
else:
|
|
331
|
+
by_severity.setdefault("other", []).append(finding)
|
|
332
|
+
return by_severity
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _print_security_finding(finding: Dict[str, Any], severity: str) -> None:
|
|
336
|
+
"""Print a single security finding with formatting.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
finding: The security finding dictionary.
|
|
340
|
+
severity: The severity level (high, medium, low).
|
|
341
|
+
"""
|
|
342
|
+
sev_color = (
|
|
343
|
+
"\033[91m" if severity == "high" else ("\033[93m" if severity == "medium" else "\033[94m")
|
|
344
|
+
)
|
|
345
|
+
print(
|
|
346
|
+
f"{sev_color}[{severity.upper()}]\033[0m {finding.get('file')}:{finding.get('line')} - {finding.get('type')}"
|
|
347
|
+
)
|
|
348
|
+
print(f" \033[2mMessage: {finding.get('message')}\033[0m")
|
|
349
|
+
code_snippet = finding.get("code")
|
|
350
|
+
if isinstance(code_snippet, str) and code_snippet.strip():
|
|
351
|
+
print(f" \033[2mCode : {code_snippet.strip()}\033[0m")
|
|
352
|
+
print()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _print_security_findings_by_severity(by_severity: Dict[str, List[Dict[str, Any]]]) -> None:
|
|
356
|
+
"""Print all findings grouped by severity.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
by_severity: Dictionary mapping severity levels to findings.
|
|
360
|
+
"""
|
|
361
|
+
print_header("🛑 Detailed Findings")
|
|
362
|
+
print_separator("-", 60)
|
|
363
|
+
|
|
364
|
+
for severity in ["high", "medium", "low"]:
|
|
365
|
+
group = by_severity.get(severity, [])
|
|
366
|
+
if not group:
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
for finding in group:
|
|
370
|
+
_print_security_finding(finding, severity)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _report_security(data: Dict[str, Any]) -> bool:
|
|
374
|
+
"""Prints a focused security analysis report.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
data: The full analysis results dictionary.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
True if the report was successfully generated.
|
|
381
|
+
"""
|
|
382
|
+
print_header("🛡️ QGIS Plugin Analyzer: Security Scan")
|
|
383
|
+
print_separator("=", 60)
|
|
384
|
+
|
|
385
|
+
security = data.get("security", {})
|
|
386
|
+
findings = security.get("findings", [])
|
|
387
|
+
sec_score = security.get("score", 0.0)
|
|
388
|
+
|
|
389
|
+
print_colored_score("Security Health Score", sec_score)
|
|
390
|
+
print(f"Total vulnerabilities detected: {len(findings)}")
|
|
391
|
+
|
|
392
|
+
if not findings:
|
|
393
|
+
print_success("✅ No security vulnerabilities found!")
|
|
394
|
+
else:
|
|
395
|
+
by_severity = _group_findings_by_severity(findings)
|
|
396
|
+
_print_security_findings_by_severity(by_severity)
|
|
397
|
+
|
|
398
|
+
print_separator("=", 60)
|
|
399
|
+
return True
|
analyzer/rules/qgis_rules.py
CHANGED
|
@@ -56,7 +56,9 @@ def get_qgis_audit_rules() -> List[Dict[str, Any]]:
|
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"id": "BLOCKING_NETWORK_CALL",
|
|
59
|
-
"pattern": re.compile(
|
|
59
|
+
"pattern": re.compile(
|
|
60
|
+
r"\b(?:requests\.(?:get|post|put|delete|patch)|urllib\.request\.urlopen)\("
|
|
61
|
+
),
|
|
60
62
|
"message": "Synchronous network call detected. UI blocking risk. Use QgsTask or QNetworkAccessManager.",
|
|
61
63
|
"severity": "high",
|
|
62
64
|
},
|