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.
Files changed (41) hide show
  1. analyzer/__init__.py +2 -1
  2. analyzer/cli/__init__.py +14 -0
  3. analyzer/cli/app.py +147 -0
  4. analyzer/cli/base.py +93 -0
  5. analyzer/cli/commands/__init__.py +19 -0
  6. analyzer/cli/commands/analyze.py +47 -0
  7. analyzer/cli/commands/fix.py +58 -0
  8. analyzer/cli/commands/init.py +41 -0
  9. analyzer/cli/commands/list_rules.py +41 -0
  10. analyzer/cli/commands/security.py +46 -0
  11. analyzer/cli/commands/summary.py +52 -0
  12. analyzer/cli/commands/version.py +41 -0
  13. analyzer/cli.py +4 -281
  14. analyzer/commands.py +163 -0
  15. analyzer/engine.py +491 -245
  16. analyzer/fixer.py +206 -130
  17. analyzer/reporters/markdown_reporter.py +88 -14
  18. analyzer/reporters/summary_reporter.py +226 -49
  19. analyzer/rules/qgis_rules.py +3 -1
  20. analyzer/scanner.py +219 -711
  21. analyzer/secrets.py +84 -0
  22. analyzer/security_checker.py +85 -0
  23. analyzer/security_rules.py +127 -0
  24. analyzer/transformers.py +29 -8
  25. analyzer/utils/__init__.py +2 -0
  26. analyzer/utils/path_utils.py +53 -1
  27. analyzer/validators.py +90 -55
  28. analyzer/visitors/__init__.py +19 -0
  29. analyzer/visitors/base.py +75 -0
  30. analyzer/visitors/composite_visitor.py +73 -0
  31. analyzer/visitors/imports_visitor.py +85 -0
  32. analyzer/visitors/metrics_visitor.py +158 -0
  33. analyzer/visitors/security_visitor.py +52 -0
  34. analyzer/visitors/standards_visitor.py +284 -0
  35. {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/METADATA +32 -10
  36. qgis_plugin_analyzer-1.6.0.dist-info/RECORD +52 -0
  37. {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/WHEEL +1 -1
  38. qgis_plugin_analyzer-1.4.0.dist-info/RECORD +0 -30
  39. {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/entry_points.txt +0 -0
  40. {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.6.0.dist-info}/licenses/LICENSE +0 -0
  41. {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
- def _report_total(data: Dict[str, Any]) -> bool:
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
- # 1. Quality Indicators
78
- metrics = data.get("metrics", {})
79
- print("\n\033[1m📊 Quality Indicators\033[0m")
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
- # 2. Research Metrics
84
- research = data.get("research_summary", {})
85
- if research:
86
- print("\n\033[1m🔬 Research-based Metrics\033[0m")
87
- params_cov = research.get("type_hint_coverage", 0)
88
- returns_cov = research.get("return_hint_coverage", 0)
89
- doc_cov = research.get("docstring_coverage", 0)
90
- styles = research.get("detected_docstring_styles", [])
91
- style = styles[0] if styles else "Unknown"
92
-
93
- print(f"- Type Hint Coverage (Params): {params_cov:.1f}%")
94
- print(f"- Type Hint Coverage (Returns): {returns_cov:.1f}%")
95
- print(f"- Docstring Coverage: {doc_cov:.1f}%")
96
- print(f"- Documentation Style: {style}")
97
-
98
- # 3. Issue Summary
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
- print("\n\033[92m✅ No issues detected! Your project looks great.\033[0m")
213
+ print_success("✅ No issues detected! Your project looks great.")
108
214
  else:
109
- print(f"\n\033[1m⚠️ Issue Statistics ({len(issues)} total)\033[0m")
110
- counts: Dict[str, int] = {}
111
- for i in issues:
112
- t = i.get("type", "unknown")
113
- counts[t] = counts.get(t, 0) + 1
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": issues_count,
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
@@ -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(r"\b(?:requests\.(?:get|post|put|delete|patch)|urllib\.request\.urlopen)\("),
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
  },