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.
- analyzer/__init__.py +2 -1
- analyzer/cli.py +49 -146
- analyzer/commands.py +163 -0
- analyzer/engine.py +121 -58
- analyzer/reporters/markdown_reporter.py +41 -0
- analyzer/reporters/summary_reporter.py +67 -3
- analyzer/rules/qgis_rules.py +3 -1
- analyzer/scanner.py +31 -603
- analyzer/secrets.py +84 -0
- analyzer/security_checker.py +85 -0
- analyzer/security_rules.py +127 -0
- analyzer/visitors.py +455 -0
- {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.5.0.dist-info}/METADATA +20 -7
- {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.5.0.dist-info}/RECORD +18 -13
- {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.5.0.dist-info}/WHEEL +1 -1
- {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.5.0.dist-info}/entry_points.txt +0 -0
- {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {qgis_plugin_analyzer-1.4.0.dist-info → qgis_plugin_analyzer-1.5.0.dist-info}/top_level.txt +0 -0
|
@@ -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":
|
|
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
|
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
|
},
|