qgis-plugin-analyzer 1.4.0__py3-none-any.whl → 1.5.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.
@@ -43,6 +43,10 @@ def _build_markdown_header(
43
43
  qgis_score = compliance.get("compliance_score", 0)
44
44
  lines.append(f"- **QGIS Compliance**: `{qgis_score}/100`")
45
45
 
46
+ # Add Security Score
47
+ sec_score = analyses.get("security", {}).get("score", 0)
48
+ lines.append(f"- **Security Score**: `{sec_score}/100` (Bandit-inspired)")
49
+
46
50
  lines.append("")
47
51
  return lines
48
52
 
@@ -142,6 +146,37 @@ def _build_markdown_repo_standards(analyses: Dict[str, Any]) -> List[str]:
142
146
  return lines
143
147
 
144
148
 
149
+ def _build_markdown_security_section(security: Dict[str, Any]) -> List[str]:
150
+ """Builds the security analysis section with findings.
151
+
152
+ Args:
153
+ security: The security analysis dictionary.
154
+
155
+ Returns:
156
+ A list of Markdown lines for the security section.
157
+ """
158
+ lines = ["\n## 🛡️ Security Analysis"]
159
+ findings = security.get("findings", [])
160
+ score = security.get("score", 0)
161
+
162
+ lines.append(f"Security score: `{score}/100` (Based on AST and secret scanning)")
163
+ lines.append(f"Detected **{len(findings)}** potential security risks.")
164
+
165
+ if not findings:
166
+ lines.append("- ✅ No security vulnerabilities detected.")
167
+ else:
168
+ for finding in findings:
169
+ severity = finding.get("severity", "medium").upper()
170
+ icon = "🛑" if severity == "HIGH" else "⚠️"
171
+ lines.append(
172
+ f"- {icon} **[{severity}]** `{finding.get('file')}:{finding.get('line')}`: {finding.get('message')}"
173
+ )
174
+ if finding.get("code"):
175
+ lines.append(f" - Code: `{finding.get('code')}`")
176
+
177
+ return lines
178
+
179
+
145
180
  def generate_markdown_summary(analyses: Dict[str, Any], output_path: pathlib.Path) -> None:
146
181
  """Generates a professional PROJECT_SUMMARY.md report.
147
182
 
@@ -195,6 +230,12 @@ def generate_markdown_summary(analyses: Dict[str, Any], output_path: pathlib.Pat
195
230
  semantic_lines = _build_markdown_semantic_section(semantic)
196
231
  f.write("\n".join(semantic_lines))
197
232
 
233
+ # Security analysis
234
+ security = analyses.get("security", {})
235
+ if security:
236
+ security_lines = _build_markdown_security_section(security)
237
+ f.write("\n".join(security_lines))
238
+
198
239
  # Repository standards (QGIS only)
199
240
  if project_type == "qgis":
200
241
  repo_standards_lines = _build_markdown_repo_standards(analyses)
@@ -39,7 +39,7 @@ def report_summary(input_path: pathlib.Path, by: str = "total") -> bool:
39
39
 
40
40
  Args:
41
41
  input_path: Path to the project_context.json file.
42
- by: Granularity level ('total', 'modules', 'functions', 'classes').
42
+ by: Granularity level ('total', 'modules', 'functions', 'classes', 'security').
43
43
 
44
44
  Returns:
45
45
  True if the report was successfully generated, False otherwise.
@@ -60,6 +60,8 @@ def report_summary(input_path: pathlib.Path, by: str = "total") -> bool:
60
60
  return _report_by_functions(data)
61
61
  elif by == "classes":
62
62
  return _report_by_classes(data)
63
+ elif by == "security":
64
+ return _report_security(data)
63
65
  else:
64
66
  print(f"\033[91mError: Unknown summary mode '{by}'\033[0m")
65
67
  return False
@@ -79,6 +81,7 @@ def _report_total(data: Dict[str, Any]) -> bool:
79
81
  print("\n\033[1m📊 Quality Indicators\033[0m")
80
82
  print_colored_score("- Module Stability Score", metrics.get("quality_score", "N/A"))
81
83
  print_colored_score("- Code Maintainability Score", metrics.get("maintainability_score", "N/A"))
84
+ print_colored_score("- Security Score (Bandit)", metrics.get("security_score", "N/A"))
82
85
 
83
86
  # 2. Research Metrics
84
87
  research = data.get("research_summary", {})
@@ -103,6 +106,12 @@ def _report_total(data: Dict[str, Any]) -> bool:
103
106
  issue["file"] = mod_path
104
107
  issues.append(issue)
105
108
 
109
+ # Add Security Findings
110
+ security_findings = data.get("security", {}).get("findings", [])
111
+ for finding in security_findings:
112
+ finding["type"] = f"SECURITY:{finding.get('type', 'generic')}"
113
+ issues.append(finding)
114
+
106
115
  if not issues:
107
116
  print("\n\033[92m✅ No issues detected! Your project looks great.\033[0m")
108
117
  else:
@@ -144,11 +153,10 @@ def _report_by_modules(data: Dict[str, Any]) -> bool:
144
153
  # Calculate issues per module
145
154
  mod_stats = []
146
155
  for m in modules:
147
- issues_count = len(m.get("ast_issues", []))
148
156
  mod_stats.append(
149
157
  {
150
158
  "path": m.get("path"),
151
- "issues": issues_count,
159
+ "issues": len(m.get("ast_issues", [])) + len(m.get("security_issues", [])),
152
160
  "complexity": m.get("complexity", 1),
153
161
  "lines": m.get("lines", 0),
154
162
  }
@@ -220,3 +228,59 @@ def _report_by_classes(data: Dict[str, Any]) -> bool:
220
228
  print(f"\nTotal: {len(all_classes)} classes found.")
221
229
  print("=" * 60)
222
230
  return True
231
+
232
+
233
+ def _report_security(data: Dict[str, Any]) -> bool:
234
+ """Prints a focused security analysis report.
235
+
236
+ Args:
237
+ data: The full analysis results dictionary.
238
+
239
+ Returns:
240
+ True if the report was successfully generated.
241
+ """
242
+ print("\n\033[1m🛡️ QGIS Plugin Analyzer: Security Scan\033[0m")
243
+ print("=" * 60)
244
+
245
+ security = data.get("security", {})
246
+ findings = security.get("findings", [])
247
+ sec_score = security.get("score", 0.0)
248
+
249
+ print_colored_score("Security Health Score", sec_score)
250
+ print(f"Total vulnerabilities detected: {len(findings)}")
251
+
252
+ if not findings:
253
+ print("\n\033[92m✅ No security vulnerabilities found!\033[0m")
254
+ else:
255
+ print("\n\033[1m🛑 Detailed Findings\033[0m")
256
+ print("-" * 60)
257
+
258
+ # Group by severity
259
+ by_severity: Dict[str, List[Dict[str, Any]]] = {"high": [], "medium": [], "low": []}
260
+ for f in findings:
261
+ sev = f.get("severity", "medium").lower()
262
+ if sev in by_severity:
263
+ by_severity[sev].append(f)
264
+ else:
265
+ by_severity.setdefault("other", []).append(f)
266
+
267
+ for sev in ["high", "medium", "low"]:
268
+ group = by_severity.get(sev, [])
269
+ if not group:
270
+ continue
271
+
272
+ sev_color = (
273
+ "\033[91m" if sev == "high" else ("\033[93m" if sev == "medium" else "\033[94m")
274
+ )
275
+ for f in group:
276
+ print(
277
+ f"{sev_color}[{sev.upper()}]\033[0m {f.get('file')}:{f.get('line')} - {f.get('type')}"
278
+ )
279
+ print(f" \033[2mMessage: {f.get('message')}\033[0m")
280
+ code_snippet = f.get("code")
281
+ if isinstance(code_snippet, str) and code_snippet.strip():
282
+ print(f" \033[2mCode : {code_snippet.strip()}\033[0m")
283
+ print()
284
+
285
+ print("=" * 60)
286
+ 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
  },