qgis-plugin-analyzer 1.4.0__tar.gz → 1.5.0__tar.gz
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.
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/PKG-INFO +20 -7
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/README.md +19 -6
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/pyproject.toml +2 -1
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/__init__.py +2 -1
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/cli.py +49 -146
- qgis_plugin_analyzer-1.5.0/src/analyzer/commands.py +163 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/engine.py +121 -58
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/reporters/markdown_reporter.py +41 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/reporters/summary_reporter.py +67 -3
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/rules/qgis_rules.py +3 -1
- qgis_plugin_analyzer-1.5.0/src/analyzer/scanner.py +222 -0
- qgis_plugin_analyzer-1.5.0/src/analyzer/secrets.py +84 -0
- qgis_plugin_analyzer-1.5.0/src/analyzer/security_checker.py +85 -0
- qgis_plugin_analyzer-1.5.0/src/analyzer/security_rules.py +127 -0
- qgis_plugin_analyzer-1.5.0/src/analyzer/visitors.py +455 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/qgis_plugin_analyzer.egg-info/PKG-INFO +20 -7
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/qgis_plugin_analyzer.egg-info/SOURCES.txt +5 -0
- qgis_plugin_analyzer-1.4.0/src/analyzer/scanner.py +0 -794
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/LICENSE +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/setup.cfg +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/__init__.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/fixer.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/models/__init__.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/models/analysis_models.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/reporters/__init__.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/reporters/html_reporter.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/rules/__init__.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/rules/modernization_rules.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/semantic.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/transformers.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/utils/__init__.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/utils/ast_utils.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/utils/config_utils.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/utils/logging_utils.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/utils/path_utils.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/utils/performance_utils.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/validators.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/qgis_plugin_analyzer.egg-info/dependency_links.txt +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/qgis_plugin_analyzer.egg-info/entry_points.txt +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/qgis_plugin_analyzer.egg-info/top_level.txt +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_analyzer.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_fixer.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_high_complexity.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_safety.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_scanner.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_security.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_semantic.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_validators.py +0 -0
- {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_vulnerability.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qgis-plugin-analyzer
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: A professional static analysis tool for QGIS (PyQGIS) plugins
|
|
5
5
|
Author-email: geociencio <juanbernales@gmail.com>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -34,13 +34,16 @@ Dynamic: license-file
|
|
|
34
34
|

|
|
35
35
|

|
|
36
36
|

|
|
37
|
-

|
|
38
|
+

|
|
39
|
+

|
|
39
40
|
|
|
40
41
|
The **QGIS Plugin Analyzer** is a static analysis tool designed specifically for QGIS (PyQGIS) plugin developers. Its goal is to elevate plugin quality by ensuring they follow community best practices and are optimized for AI-assisted development.
|
|
41
42
|
|
|
42
43
|
## ✨ Main Features
|
|
43
44
|
|
|
45
|
+
- **Security Core (Bandit-inspired)**: Professional vulnerability scanning detecting `eval`, `exec`, shell injections, and SQL injection risks.
|
|
46
|
+
- **Deep Entropy Secret Scanner**: Detects hardcoded API keys, passwords, and sensitive tokens using regex and information entropy.
|
|
44
47
|
- **High-Performance Engine**: Parallel analysis powered by `ProcessPoolExecutor` for ultra-fast execution on multi-core systems.
|
|
45
48
|
- **Project Auto-Detection**: Intelligently distinguishes between official QGIS Plugins and Generic Python Projects, tailoring validation logic accordingly.
|
|
46
49
|
- **Advanced Ignore Engine**: Robust `.analyzerignore` support with non-anchored patterns and smart default excludes (`.venv`, `build`, etc.).
|
|
@@ -63,9 +66,8 @@ The **QGIS Plugin Analyzer** is a static analysis tool designed specifically for
|
|
|
63
66
|
| **QGIS-Specific Rules**| ✅ (Precise AST) | ✅ (Regex/AST) | ❌ | ✅ |
|
|
64
67
|
| **Interactive Auto-Fix**| ✅ | ❌ | ❌ | ❌ |
|
|
65
68
|
| **Semantic Analysis** | ✅ | ❌ | ❌ | ❌ |
|
|
66
|
-
| **
|
|
67
|
-
| **
|
|
68
|
-
| **Architecture Audit** | ✅ (UI/Core) | ❌ | ❌ | ❌ |
|
|
69
|
+
| **Security Audit** | ✅ (Bandit-style) | ❌ | ❌ | ❌ |
|
|
70
|
+
| **Secret Scanning** | ✅ (Entropy) | ❌ | ❌ | ❌ |
|
|
69
71
|
| **HTML/MD Reports** | ✅ | ❌ | ❌ | ❌ |
|
|
70
72
|
| **AI Context Gen** | ✅ (Project Brain) | ❌ | ❌ | ❌ |
|
|
71
73
|
|
|
@@ -129,7 +131,7 @@ You can run `qgis-plugin-analyzer` automatically before every commit to ensure q
|
|
|
129
131
|
|
|
130
132
|
```yaml
|
|
131
133
|
- repo: https://github.com/geociencio/qgis-plugin-analyzer
|
|
132
|
-
rev: v1.
|
|
134
|
+
rev: v1.5.0 # Use the latest tag
|
|
133
135
|
hooks:
|
|
134
136
|
- id: qgis-plugin-analyzer
|
|
135
137
|
```
|
|
@@ -210,6 +212,17 @@ Shows a professional, color-coded summary of findings directly in your terminal.
|
|
|
210
212
|
| `-b`, `--by` | Granularity of the summary: `total`, `modules`, `functions`, `classes`. | `total` |
|
|
211
213
|
| `-i`, `--input` | Path to the `project_context.json` file to summarize. | `analysis_results/project_context.json` |
|
|
212
214
|
|
|
215
|
+
### `qgis-analyzer security`
|
|
216
|
+
Performs a focused security scan on a file or directory.
|
|
217
|
+
|
|
218
|
+
| Argument | Description | Default |
|
|
219
|
+
| :--- | :--- | :--- |
|
|
220
|
+
| `path` | **(Required)** Path to the file or directory to scan. | N/A |
|
|
221
|
+
| `-p`, `--profile`| Configuration profile. | `default` |
|
|
222
|
+
|
|
223
|
+
### `qgis-analyzer version`
|
|
224
|
+
Shows the current version of the analyzer.
|
|
225
|
+
|
|
213
226
|
**Example:**
|
|
214
227
|
```bash
|
|
215
228
|
# Executive summary
|
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|

|
|
7
7
|

|
|
8
8
|

|
|
9
|
-

|
|
10
|
+

|
|
11
|
+

|
|
11
12
|
|
|
12
13
|
The **QGIS Plugin Analyzer** is a static analysis tool designed specifically for QGIS (PyQGIS) plugin developers. Its goal is to elevate plugin quality by ensuring they follow community best practices and are optimized for AI-assisted development.
|
|
13
14
|
|
|
14
15
|
## ✨ Main Features
|
|
15
16
|
|
|
17
|
+
- **Security Core (Bandit-inspired)**: Professional vulnerability scanning detecting `eval`, `exec`, shell injections, and SQL injection risks.
|
|
18
|
+
- **Deep Entropy Secret Scanner**: Detects hardcoded API keys, passwords, and sensitive tokens using regex and information entropy.
|
|
16
19
|
- **High-Performance Engine**: Parallel analysis powered by `ProcessPoolExecutor` for ultra-fast execution on multi-core systems.
|
|
17
20
|
- **Project Auto-Detection**: Intelligently distinguishes between official QGIS Plugins and Generic Python Projects, tailoring validation logic accordingly.
|
|
18
21
|
- **Advanced Ignore Engine**: Robust `.analyzerignore` support with non-anchored patterns and smart default excludes (`.venv`, `build`, etc.).
|
|
@@ -35,9 +38,8 @@ The **QGIS Plugin Analyzer** is a static analysis tool designed specifically for
|
|
|
35
38
|
| **QGIS-Specific Rules**| ✅ (Precise AST) | ✅ (Regex/AST) | ❌ | ✅ |
|
|
36
39
|
| **Interactive Auto-Fix**| ✅ | ❌ | ❌ | ❌ |
|
|
37
40
|
| **Semantic Analysis** | ✅ | ❌ | ❌ | ❌ |
|
|
38
|
-
| **
|
|
39
|
-
| **
|
|
40
|
-
| **Architecture Audit** | ✅ (UI/Core) | ❌ | ❌ | ❌ |
|
|
41
|
+
| **Security Audit** | ✅ (Bandit-style) | ❌ | ❌ | ❌ |
|
|
42
|
+
| **Secret Scanning** | ✅ (Entropy) | ❌ | ❌ | ❌ |
|
|
41
43
|
| **HTML/MD Reports** | ✅ | ❌ | ❌ | ❌ |
|
|
42
44
|
| **AI Context Gen** | ✅ (Project Brain) | ❌ | ❌ | ❌ |
|
|
43
45
|
|
|
@@ -101,7 +103,7 @@ You can run `qgis-plugin-analyzer` automatically before every commit to ensure q
|
|
|
101
103
|
|
|
102
104
|
```yaml
|
|
103
105
|
- repo: https://github.com/geociencio/qgis-plugin-analyzer
|
|
104
|
-
rev: v1.
|
|
106
|
+
rev: v1.5.0 # Use the latest tag
|
|
105
107
|
hooks:
|
|
106
108
|
- id: qgis-plugin-analyzer
|
|
107
109
|
```
|
|
@@ -182,6 +184,17 @@ Shows a professional, color-coded summary of findings directly in your terminal.
|
|
|
182
184
|
| `-b`, `--by` | Granularity of the summary: `total`, `modules`, `functions`, `classes`. | `total` |
|
|
183
185
|
| `-i`, `--input` | Path to the `project_context.json` file to summarize. | `analysis_results/project_context.json` |
|
|
184
186
|
|
|
187
|
+
### `qgis-analyzer security`
|
|
188
|
+
Performs a focused security scan on a file or directory.
|
|
189
|
+
|
|
190
|
+
| Argument | Description | Default |
|
|
191
|
+
| :--- | :--- | :--- |
|
|
192
|
+
| `path` | **(Required)** Path to the file or directory to scan. | N/A |
|
|
193
|
+
| `-p`, `--profile`| Configuration profile. | `default` |
|
|
194
|
+
|
|
195
|
+
### `qgis-analyzer version`
|
|
196
|
+
Shows the current version of the analyzer.
|
|
197
|
+
|
|
185
198
|
**Example:**
|
|
186
199
|
```bash
|
|
187
200
|
# Executive summary
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "qgis-plugin-analyzer"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.5.0"
|
|
4
4
|
description = "A professional static analysis tool for QGIS (PyQGIS) plugins"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.8"
|
|
@@ -43,6 +43,7 @@ dev = [
|
|
|
43
43
|
"ruff>=0.9.0",
|
|
44
44
|
"mypy>=1.0.0",
|
|
45
45
|
"qgis-stubs>=0.1",
|
|
46
|
+
"pytest>=8.3.5",
|
|
46
47
|
]
|
|
47
48
|
|
|
48
49
|
[tool.mypy]
|
|
@@ -23,7 +23,15 @@ import argparse
|
|
|
23
23
|
import pathlib
|
|
24
24
|
import sys
|
|
25
25
|
|
|
26
|
-
from .
|
|
26
|
+
from . import __version__
|
|
27
|
+
from .commands import (
|
|
28
|
+
handle_analyze,
|
|
29
|
+
handle_fix,
|
|
30
|
+
handle_init,
|
|
31
|
+
handle_list_rules,
|
|
32
|
+
handle_security,
|
|
33
|
+
handle_summary,
|
|
34
|
+
)
|
|
27
35
|
from .utils import logger, setup_logger
|
|
28
36
|
|
|
29
37
|
|
|
@@ -36,6 +44,7 @@ def _setup_argument_parser() -> argparse.ArgumentParser:
|
|
|
36
44
|
parser = argparse.ArgumentParser(
|
|
37
45
|
description="QGIS Plugin Analyzer - A guardian for your PyQGIS code"
|
|
38
46
|
)
|
|
47
|
+
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
39
48
|
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
|
40
49
|
|
|
41
50
|
# Analyze Command
|
|
@@ -60,6 +69,27 @@ def _setup_argument_parser() -> argparse.ArgumentParser:
|
|
|
60
69
|
default="default",
|
|
61
70
|
)
|
|
62
71
|
|
|
72
|
+
# Security Command
|
|
73
|
+
security_parser = subparsers.add_parser("security", help="Run a focused security scan")
|
|
74
|
+
security_parser.add_argument("project_path", help="Path to the QGIS project to scan")
|
|
75
|
+
security_parser.add_argument(
|
|
76
|
+
"-o",
|
|
77
|
+
"--output",
|
|
78
|
+
help="Output directory for reports",
|
|
79
|
+
default="./analysis_results",
|
|
80
|
+
)
|
|
81
|
+
security_parser.add_argument(
|
|
82
|
+
"-p",
|
|
83
|
+
"--profile",
|
|
84
|
+
help="Configuration profile from pyproject.toml",
|
|
85
|
+
default="default",
|
|
86
|
+
)
|
|
87
|
+
security_parser.add_argument(
|
|
88
|
+
"--deep",
|
|
89
|
+
action="store_true",
|
|
90
|
+
help="Run more intensive (but slower) security checks",
|
|
91
|
+
)
|
|
92
|
+
|
|
63
93
|
# Fix Command
|
|
64
94
|
fix_parser = subparsers.add_parser("fix", help="Auto-fix common QGIS plugin issues")
|
|
65
95
|
fix_parser.add_argument("path", type=str, help="Path to the QGIS plugin directory")
|
|
@@ -90,6 +120,9 @@ def _setup_argument_parser() -> argparse.ArgumentParser:
|
|
|
90
120
|
# List Rules Command
|
|
91
121
|
subparsers.add_parser("list-rules", help="List all available QGIS audit rules")
|
|
92
122
|
|
|
123
|
+
# Version Command
|
|
124
|
+
subparsers.add_parser("version", help="Show the current version of the analyzer")
|
|
125
|
+
|
|
93
126
|
# Init Command
|
|
94
127
|
subparsers.add_parser("init", help="Initialize a new .analyzerignore with defaults")
|
|
95
128
|
|
|
@@ -114,141 +147,6 @@ def _setup_argument_parser() -> argparse.ArgumentParser:
|
|
|
114
147
|
return parser
|
|
115
148
|
|
|
116
149
|
|
|
117
|
-
def _handle_fix_command(args: argparse.Namespace) -> bool:
|
|
118
|
-
"""Handles the execution of the 'fix' command.
|
|
119
|
-
|
|
120
|
-
Args:
|
|
121
|
-
args: Parsed command line arguments.
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
True if the fix process completed successfully, False otherwise.
|
|
125
|
-
"""
|
|
126
|
-
import json
|
|
127
|
-
|
|
128
|
-
from .fixer import AutoFixer
|
|
129
|
-
|
|
130
|
-
project_path = pathlib.Path(args.path).resolve()
|
|
131
|
-
if not project_path.exists():
|
|
132
|
-
print(f"❌ Path not found: {project_path}")
|
|
133
|
-
return False
|
|
134
|
-
|
|
135
|
-
# Run analysis first
|
|
136
|
-
print("🔍 Analyzing project for fixable issues...")
|
|
137
|
-
analyzer = ProjectAnalyzer(
|
|
138
|
-
str(project_path),
|
|
139
|
-
args.output if hasattr(args, "output") else "./analysis_results",
|
|
140
|
-
args.profile if hasattr(args, "profile") else "default",
|
|
141
|
-
)
|
|
142
|
-
analyzer.run()
|
|
143
|
-
|
|
144
|
-
# Load issues
|
|
145
|
-
context_file = analyzer.output_dir / "project_context.json"
|
|
146
|
-
with open(context_file) as f:
|
|
147
|
-
context = json.load(f)
|
|
148
|
-
|
|
149
|
-
all_issues = []
|
|
150
|
-
for module in context.get("modules", []):
|
|
151
|
-
all_issues.extend(module.get("ast_issues", []))
|
|
152
|
-
|
|
153
|
-
if args.rules:
|
|
154
|
-
rule_ids = [r.strip() for r in args.rules.split(",")]
|
|
155
|
-
all_issues = [i for i in all_issues if i.get("type") in rule_ids]
|
|
156
|
-
|
|
157
|
-
fixer = AutoFixer(project_path, dry_run=not args.apply)
|
|
158
|
-
fixable = fixer.get_fixable_issues(all_issues)
|
|
159
|
-
|
|
160
|
-
if not fixable:
|
|
161
|
-
print("✅ No fixable issues found!")
|
|
162
|
-
return True
|
|
163
|
-
|
|
164
|
-
print(f"\n📋 Found {len(fixable)} fixable issue(s)")
|
|
165
|
-
if not args.apply:
|
|
166
|
-
print("\n⚠️ DRY RUN MODE (use --apply to execute changes)\n")
|
|
167
|
-
|
|
168
|
-
stats = fixer.apply_fixes(fixable, interactive=not args.auto_approve)
|
|
169
|
-
print(
|
|
170
|
-
f"\n📊 Summary: Applied: {stats['applied']}, Skipped: {stats['skipped']}, Failed: {stats['failed']}"
|
|
171
|
-
)
|
|
172
|
-
return True
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def _handle_analyze_command(args: argparse.Namespace) -> None:
|
|
176
|
-
"""Handles the execution of the 'analyze' command.
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
args: Parsed command line arguments.
|
|
180
|
-
"""
|
|
181
|
-
# Force generate_html based on flag, overriding profile if necessary for CLI usage
|
|
182
|
-
# We pass it via a temporary config override or modify the analyzer init
|
|
183
|
-
# For now, let's pass it to the analyzer constructor or modify config after init
|
|
184
|
-
|
|
185
|
-
analyzer = ProjectAnalyzer(args.project_path, args.output, args.profile)
|
|
186
|
-
|
|
187
|
-
# Override config based on CLI flag
|
|
188
|
-
if hasattr(args, "report") and args.report:
|
|
189
|
-
analyzer.config["generate_html"] = True
|
|
190
|
-
else:
|
|
191
|
-
analyzer.config["generate_html"] = False
|
|
192
|
-
|
|
193
|
-
success = analyzer.run()
|
|
194
|
-
|
|
195
|
-
# Always show terminal summary
|
|
196
|
-
from .reporters.summary_reporter import report_summary
|
|
197
|
-
|
|
198
|
-
# If we didn't generate reports, we might still want to show the summary
|
|
199
|
-
# using the in-memory data or the context file if it was saved.
|
|
200
|
-
# Engine saves json context by default? Let's check engine.py.
|
|
201
|
-
# Assuming engine saves project_context.json always or we need to access results directly.
|
|
202
|
-
# To keep it simple, we depend on the engine saving the context or returning it.
|
|
203
|
-
# Current engine.run retuns bool.
|
|
204
|
-
|
|
205
|
-
context_path = analyzer.output_dir / "project_context.json"
|
|
206
|
-
if context_path.exists():
|
|
207
|
-
report_summary(context_path)
|
|
208
|
-
|
|
209
|
-
if not success:
|
|
210
|
-
sys.exit(1)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def _handle_list_rules_command() -> None:
|
|
214
|
-
"""Handles the 'list-rules' command by displaying available audit rules."""
|
|
215
|
-
from .rules import get_qgis_audit_rules
|
|
216
|
-
|
|
217
|
-
rules = get_qgis_audit_rules()
|
|
218
|
-
print("\n📋 QGIS Audit Rules Catalog:")
|
|
219
|
-
print("=" * 30)
|
|
220
|
-
for r in rules:
|
|
221
|
-
print(f"- [{r['severity'].upper()}] {r['id']}: {r['message']}")
|
|
222
|
-
print(f"\nTotal: {len(rules)} rules.\n")
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _handle_init_command() -> None:
|
|
226
|
-
"""Handles the 'init' command by creating a default .analyzerignore file."""
|
|
227
|
-
from .utils import DEFAULT_EXCLUDE
|
|
228
|
-
|
|
229
|
-
ignore_file = pathlib.Path(".analyzerignore")
|
|
230
|
-
if ignore_file.exists():
|
|
231
|
-
print("⚠️ .analyzerignore already exists. Skipping.")
|
|
232
|
-
else:
|
|
233
|
-
with open(ignore_file, "w") as f:
|
|
234
|
-
f.write("# QGIS Plugin Analyzer Ignore File\n")
|
|
235
|
-
for p in DEFAULT_EXCLUDE:
|
|
236
|
-
f.write(f"{p}\n")
|
|
237
|
-
print("✅ Created .analyzerignore with default excludes.")
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def _handle_summary_command(args: argparse.Namespace) -> None:
|
|
241
|
-
"""Handles the 'summary' command by displaying a terminal report.
|
|
242
|
-
|
|
243
|
-
Args:
|
|
244
|
-
args: Parsed command line arguments.
|
|
245
|
-
"""
|
|
246
|
-
from .reporters.summary_reporter import report_summary
|
|
247
|
-
|
|
248
|
-
input_path = pathlib.Path(args.input).resolve()
|
|
249
|
-
report_summary(input_path, by=args.by)
|
|
250
|
-
|
|
251
|
-
|
|
252
150
|
def main() -> None:
|
|
253
151
|
"""Main entry point for the QGIS Plugin Analyzer CLI.
|
|
254
152
|
|
|
@@ -260,6 +158,8 @@ def main() -> None:
|
|
|
260
158
|
# Legacy support / default to analyze if no command provided
|
|
261
159
|
if len(sys.argv) > 1 and sys.argv[1] not in [
|
|
262
160
|
"analyze",
|
|
161
|
+
"security",
|
|
162
|
+
"version",
|
|
263
163
|
"fix",
|
|
264
164
|
"list-rules",
|
|
265
165
|
"init",
|
|
@@ -278,17 +178,20 @@ def main() -> None:
|
|
|
278
178
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
279
179
|
setup_logger(output_dir)
|
|
280
180
|
|
|
181
|
+
# Command Dispatcher
|
|
182
|
+
dispatch = {
|
|
183
|
+
"fix": lambda: handle_fix(args),
|
|
184
|
+
"analyze": lambda: handle_analyze(args),
|
|
185
|
+
"list-rules": lambda: handle_list_rules(),
|
|
186
|
+
"init": lambda: handle_init(),
|
|
187
|
+
"summary": lambda: handle_summary(args),
|
|
188
|
+
"security": lambda: handle_security(args),
|
|
189
|
+
"version": lambda: print(f"qgis-analyzer {__version__}"),
|
|
190
|
+
}
|
|
191
|
+
|
|
281
192
|
try:
|
|
282
|
-
if args.command
|
|
283
|
-
|
|
284
|
-
elif args.command == "analyze":
|
|
285
|
-
_handle_analyze_command(args)
|
|
286
|
-
elif args.command == "list-rules":
|
|
287
|
-
_handle_list_rules_command()
|
|
288
|
-
elif args.command == "init":
|
|
289
|
-
_handle_init_command()
|
|
290
|
-
elif args.command == "summary":
|
|
291
|
-
_handle_summary_command(args)
|
|
193
|
+
if args.command in dispatch:
|
|
194
|
+
dispatch[args.command]()
|
|
292
195
|
else:
|
|
293
196
|
parser.print_help()
|
|
294
197
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Command handlers for the QGIS Plugin Analyzer CLI.
|
|
2
|
+
|
|
3
|
+
This module contains the implementation of individual CLI commands to separate
|
|
4
|
+
interface definition (cli.py) from execution logic.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import pathlib
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from .engine import ProjectAnalyzer
|
|
13
|
+
from .fixer import AutoFixer
|
|
14
|
+
from .reporters.summary_reporter import report_summary
|
|
15
|
+
from .rules import get_qgis_audit_rules
|
|
16
|
+
from .utils import DEFAULT_EXCLUDE
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def handle_fix(args: argparse.Namespace) -> bool:
|
|
20
|
+
"""Handles the execution of the 'fix' command.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
args: Parsed command line arguments.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
True if the fix process completed successfully, False otherwise.
|
|
27
|
+
"""
|
|
28
|
+
project_path = pathlib.Path(args.path).resolve()
|
|
29
|
+
if not project_path.exists():
|
|
30
|
+
print(f"❌ Path not found: {project_path}")
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
# Run analysis first
|
|
34
|
+
print("🔍 Analyzing project for fixable issues...")
|
|
35
|
+
analyzer = ProjectAnalyzer(
|
|
36
|
+
str(project_path),
|
|
37
|
+
args.output if hasattr(args, "output") else "./analysis_results",
|
|
38
|
+
args.profile if hasattr(args, "profile") else "default",
|
|
39
|
+
)
|
|
40
|
+
analyzer.run()
|
|
41
|
+
|
|
42
|
+
# Load issues
|
|
43
|
+
context_file = analyzer.output_dir / "project_context.json"
|
|
44
|
+
if not context_file.exists():
|
|
45
|
+
print("❌ Analysis failed to generate context file.")
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
with open(context_file) as f:
|
|
49
|
+
context = json.load(f)
|
|
50
|
+
|
|
51
|
+
all_issues = []
|
|
52
|
+
for module in context.get("modules", []):
|
|
53
|
+
all_issues.extend(module.get("ast_issues", []))
|
|
54
|
+
|
|
55
|
+
if args.rules:
|
|
56
|
+
rule_ids = [r.strip() for r in args.rules.split(",")]
|
|
57
|
+
all_issues = [i for i in all_issues if i.get("type") in rule_ids]
|
|
58
|
+
|
|
59
|
+
fixer = AutoFixer(project_path, dry_run=not args.apply)
|
|
60
|
+
fixable = fixer.get_fixable_issues(all_issues)
|
|
61
|
+
|
|
62
|
+
if not fixable:
|
|
63
|
+
print("✅ No fixable issues found!")
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
print(f"\n📋 Found {len(fixable)} fixable issue(s)")
|
|
67
|
+
if not args.apply:
|
|
68
|
+
print("\n⚠️ DRY RUN MODE (use --apply to execute changes)\n")
|
|
69
|
+
|
|
70
|
+
stats = fixer.apply_fixes(fixable, interactive=not args.auto_approve)
|
|
71
|
+
print(
|
|
72
|
+
f"\n📊 Summary: Applied: {stats['applied']}, Skipped: {stats['skipped']}, Failed: {stats['failed']}"
|
|
73
|
+
)
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def handle_analyze(args: argparse.Namespace) -> None:
|
|
78
|
+
"""Handles the execution of the 'analyze' command.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
args: Parsed command line arguments.
|
|
82
|
+
"""
|
|
83
|
+
analyzer = ProjectAnalyzer(args.project_path, args.output, args.profile)
|
|
84
|
+
|
|
85
|
+
# Override config based on CLI flag
|
|
86
|
+
if hasattr(args, "report") and args.report:
|
|
87
|
+
analyzer.config["generate_html"] = True
|
|
88
|
+
else:
|
|
89
|
+
analyzer.config["generate_html"] = False
|
|
90
|
+
|
|
91
|
+
success = analyzer.run()
|
|
92
|
+
|
|
93
|
+
context_path = analyzer.output_dir / "project_context.json"
|
|
94
|
+
if context_path.exists():
|
|
95
|
+
report_summary(context_path)
|
|
96
|
+
|
|
97
|
+
if not success:
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def handle_list_rules() -> None:
|
|
102
|
+
"""Handles the 'list-rules' command by displaying available audit rules."""
|
|
103
|
+
rules = get_qgis_audit_rules()
|
|
104
|
+
print("\n📋 QGIS Audit Rules Catalog:")
|
|
105
|
+
print("=" * 30)
|
|
106
|
+
for r in rules:
|
|
107
|
+
print(f"- [{r['severity'].upper()}] {r['id']}: {r['message']}")
|
|
108
|
+
print(f"\nTotal: {len(rules)} rules.\n")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def handle_init() -> None:
|
|
112
|
+
"""Handles the 'init' command by creating a default .analyzerignore file."""
|
|
113
|
+
ignore_file = pathlib.Path(".analyzerignore")
|
|
114
|
+
if ignore_file.exists():
|
|
115
|
+
print("⚠️ .analyzerignore already exists. Skipping.")
|
|
116
|
+
else:
|
|
117
|
+
with open(ignore_file, "w") as f:
|
|
118
|
+
f.write("# QGIS Plugin Analyzer Ignore File\n")
|
|
119
|
+
for p in DEFAULT_EXCLUDE:
|
|
120
|
+
f.write(f"{p}\n")
|
|
121
|
+
print("✅ Created .analyzerignore with default excludes.")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def handle_summary(args: argparse.Namespace) -> None:
|
|
125
|
+
"""Handles the 'summary' command by displaying a terminal report.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
args: Parsed command line arguments.
|
|
129
|
+
"""
|
|
130
|
+
input_path = pathlib.Path(args.input).resolve()
|
|
131
|
+
report_summary(input_path, by=args.by)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def handle_security(args: argparse.Namespace) -> None:
|
|
135
|
+
"""Handles the execution of the 'security' command.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
args: Parsed command line arguments.
|
|
139
|
+
"""
|
|
140
|
+
project_path = pathlib.Path(args.project_path).resolve()
|
|
141
|
+
if not project_path.exists():
|
|
142
|
+
print(f"❌ Path not found: {project_path}")
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
|
|
145
|
+
print(f"🛡️ Starting focused security scan for: {project_path.name}...")
|
|
146
|
+
|
|
147
|
+
# Run analyzer with current profile
|
|
148
|
+
analyzer = ProjectAnalyzer(str(project_path), args.output, args.profile)
|
|
149
|
+
|
|
150
|
+
# We could potentially add a flag to 'deep' mode in the analyzer config
|
|
151
|
+
if args.deep:
|
|
152
|
+
analyzer.config["security_deep_scan"] = True
|
|
153
|
+
print("🔍 Deep scan enabled (Entropy analysis and full secret detection)")
|
|
154
|
+
|
|
155
|
+
success = analyzer.run()
|
|
156
|
+
|
|
157
|
+
context_path = analyzer.output_dir / "project_context.json"
|
|
158
|
+
if context_path.exists():
|
|
159
|
+
# Use the specialized security reporter
|
|
160
|
+
report_summary(context_path, by="security")
|
|
161
|
+
|
|
162
|
+
if not success:
|
|
163
|
+
sys.exit(1)
|