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.
Files changed (49) hide show
  1. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/PKG-INFO +20 -7
  2. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/README.md +19 -6
  3. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/pyproject.toml +2 -1
  4. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/__init__.py +2 -1
  5. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/cli.py +49 -146
  6. qgis_plugin_analyzer-1.5.0/src/analyzer/commands.py +163 -0
  7. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/engine.py +121 -58
  8. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/reporters/markdown_reporter.py +41 -0
  9. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/reporters/summary_reporter.py +67 -3
  10. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/rules/qgis_rules.py +3 -1
  11. qgis_plugin_analyzer-1.5.0/src/analyzer/scanner.py +222 -0
  12. qgis_plugin_analyzer-1.5.0/src/analyzer/secrets.py +84 -0
  13. qgis_plugin_analyzer-1.5.0/src/analyzer/security_checker.py +85 -0
  14. qgis_plugin_analyzer-1.5.0/src/analyzer/security_rules.py +127 -0
  15. qgis_plugin_analyzer-1.5.0/src/analyzer/visitors.py +455 -0
  16. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/qgis_plugin_analyzer.egg-info/PKG-INFO +20 -7
  17. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/qgis_plugin_analyzer.egg-info/SOURCES.txt +5 -0
  18. qgis_plugin_analyzer-1.4.0/src/analyzer/scanner.py +0 -794
  19. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/LICENSE +0 -0
  20. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/setup.cfg +0 -0
  21. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/__init__.py +0 -0
  22. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/fixer.py +0 -0
  23. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/models/__init__.py +0 -0
  24. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/models/analysis_models.py +0 -0
  25. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/reporters/__init__.py +0 -0
  26. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/reporters/html_reporter.py +0 -0
  27. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/rules/__init__.py +0 -0
  28. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/rules/modernization_rules.py +0 -0
  29. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/semantic.py +0 -0
  30. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/transformers.py +0 -0
  31. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/utils/__init__.py +0 -0
  32. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/utils/ast_utils.py +0 -0
  33. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/utils/config_utils.py +0 -0
  34. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/utils/logging_utils.py +0 -0
  35. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/utils/path_utils.py +0 -0
  36. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/utils/performance_utils.py +0 -0
  37. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/analyzer/validators.py +0 -0
  38. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/qgis_plugin_analyzer.egg-info/dependency_links.txt +0 -0
  39. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/qgis_plugin_analyzer.egg-info/entry_points.txt +0 -0
  40. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/src/qgis_plugin_analyzer.egg-info/top_level.txt +0 -0
  41. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_analyzer.py +0 -0
  42. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_fixer.py +0 -0
  43. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_high_complexity.py +0 -0
  44. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_safety.py +0 -0
  45. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_scanner.py +0 -0
  46. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_security.py +0 -0
  47. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_semantic.py +0 -0
  48. {qgis_plugin_analyzer-1.4.0 → qgis_plugin_analyzer-1.5.0}/tests/test_validators.py +0 -0
  49. {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.4.0
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
  ![License](https://img.shields.io/badge/License-GPLv3-blue.svg)
35
35
  ![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)
36
36
  ![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?logo=git)
37
- ![Quality Score](https://img.shields.io/badge/Module%20Stability-55.6%2F100-yellow)
38
- ![Maintainability](https://img.shields.io/badge/Maintainability-100.0%2F100-brightgreen)
37
+ ![Quality Score](https://img.shields.io/badge/Module%20Stability-92.3%2F100-brightgreen)
38
+ ![Maintainability](https://img.shields.io/badge/Maintainability-84.1%2F100-green)
39
+ ![Security Score](https://img.shields.io/badge/Security--Bandit-98.7%2F100-brightgreen)
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
- | **Compliance Checks** | ✅ | ❌ | ❌ | |
67
- | **i18n / API Audit** | ✅ | ❌ | ❌ | |
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.4.0 # Use the latest tag
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
  ![License](https://img.shields.io/badge/License-GPLv3-blue.svg)
7
7
  ![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)
8
8
  ![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?logo=git)
9
- ![Quality Score](https://img.shields.io/badge/Module%20Stability-55.6%2F100-yellow)
10
- ![Maintainability](https://img.shields.io/badge/Maintainability-100.0%2F100-brightgreen)
9
+ ![Quality Score](https://img.shields.io/badge/Module%20Stability-92.3%2F100-brightgreen)
10
+ ![Maintainability](https://img.shields.io/badge/Maintainability-84.1%2F100-green)
11
+ ![Security Score](https://img.shields.io/badge/Security--Bandit-98.7%2F100-brightgreen)
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
- | **Compliance Checks** | ✅ | ❌ | ❌ | |
39
- | **i18n / API Audit** | ✅ | ❌ | ❌ | |
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.4.0 # Use the latest tag
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.4.0"
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]
@@ -16,4 +16,5 @@
16
16
  # * the Free Software Foundation; either version 2 of the License, or *
17
17
  # * (at your option) any later version. *
18
18
  # * *
19
- # ***************************************************************************/
19
+ #
20
+ __version__ = "1.5.0"
@@ -23,7 +23,15 @@ import argparse
23
23
  import pathlib
24
24
  import sys
25
25
 
26
- from .engine import ProjectAnalyzer
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 == "fix":
283
- _handle_fix_command(args)
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)