shield-security 0.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.
shield/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ __version__ = "0.5.0"
2
+ __author__ = "Ali Yasin Idrees"
3
+ __name__ = "SHIELD Security"
shield/cli.py ADDED
@@ -0,0 +1,632 @@
1
+ """
2
+ SHIELD Security — CLI Entry Point
3
+ ===================================
4
+ Secure Hybrid Integration and Enforcement Layer
5
+ for LLM-assisted Development
6
+
7
+ Author: Ali Yasin Idrees
8
+ Version: 0.3.0
9
+
10
+ Commands:
11
+ shield scan <path> Scan for vulnerabilities
12
+ shield fix <path> Scan and auto-fix
13
+ shield fix <path> --inplace Fix original files directly
14
+ shield compare before.json after.json Compare two scans
15
+ shield version Show version
16
+ """
17
+
18
+ import sys
19
+ import argparse
20
+ from pathlib import Path
21
+ from colorama import init, Fore, Style
22
+
23
+ # Initialize colorama for Windows color support
24
+ init(autoreset=True)
25
+
26
+ # ── BANNER ────────────────────────────────────────────────────────────────────
27
+
28
+ BANNER = f"""
29
+ {Fore.WHITE}╔══════════════════════════════════════════════════════════════╗
30
+ ║ ║
31
+ ║ {Fore.WHITE}███████╗██╗ ██╗██╗███████╗██╗ ██████╗{Fore.WHITE} ║
32
+ ║ {Fore.WHITE}██╔════╝██║ ██║██║██╔════╝██║ ██╔══██╗{Fore.WHITE} ║
33
+ ║ {Fore.WHITE}███████╗███████║██║█████╗ ██║ ██║ ██║{Fore.WHITE} ║
34
+ ║ {Fore.WHITE}╚════██║██╔══██║██║██╔══╝ ██║ ██║ ██║{Fore.WHITE} ║
35
+ ║ {Fore.WHITE}███████║██║ ██║██║███████╗███████╗██████╔╝{Fore.WHITE} ║
36
+ ║ {Fore.WHITE}╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚═════╝{Fore.WHITE} ║
37
+ ║ ║
38
+ ║ {Fore.WHITE}Security Layer for LLM-assisted Development{Fore.WHITE} ║
39
+ ║ {Fore.WHITE}v0.4.0 · Ali Yasin Idrees · 2026{Fore.WHITE} ║
40
+ ╚══════════════════════════════════════════════════════════════╝
41
+ {Style.RESET_ALL}"""
42
+
43
+ # ── HELPERS ───────────────────────────────────────────────────────────────────
44
+
45
+ def print_banner():
46
+ if sys.stdout.isatty():
47
+ print(BANNER)
48
+
49
+ def success(msg): print(f"{Fore.GREEN} ✅ {msg}{Style.RESET_ALL}")
50
+ def warning(msg): print(f"{Fore.YELLOW} ⚠️ {msg}{Style.RESET_ALL}")
51
+ def error(msg): print(f"{Fore.RED} ❌ {msg}{Style.RESET_ALL}")
52
+ def info(msg): print(f"{Fore.WHITE} → {msg}{Style.RESET_ALL}")
53
+ def section(msg): print(f"\n{Fore.WHITE}{'─'*60}\n {msg}\n{'─'*60}{Style.RESET_ALL}")
54
+
55
+ # ── LANGUAGE DETECTION ────────────────────────────────────────────────────────
56
+
57
+ def detect_languages(path: str) -> list[str]:
58
+ """Auto-detect programming languages in directory."""
59
+ extensions = {
60
+ '.py': 'python',
61
+ '.js': 'javascript',
62
+ '.ts': 'typescript',
63
+ '.java': 'java',
64
+ '.php': 'php',
65
+ '.go': 'go',
66
+ '.rb': 'ruby',
67
+ }
68
+ found = set()
69
+ for f in Path(path).rglob('*'):
70
+ if f.suffix in extensions:
71
+ found.add(extensions[f.suffix])
72
+ return sorted(list(found))
73
+
74
+ # ── SCAN COMMAND ──────────────────────────────────────────────────────────────
75
+
76
+ def cmd_scan(args):
77
+ """shield scan <path> [--report report.html] [--json]"""
78
+ import time
79
+ import subprocess # nosec B404 — hardcoded commands only
80
+ import json as jsonlib
81
+ from shield.reporter import (
82
+ ScanResult, Finding,
83
+ generate_report,
84
+ save_json_report,
85
+ parse_bandit_to_result,
86
+ parse_semgrep_to_result
87
+ )
88
+ from shield.config import load_config
89
+ cfg = load_config(str(args.path))
90
+ scan_start = time.time()
91
+
92
+ # Apply config defaults where args not specified
93
+ if not args.report and cfg.get('report'):
94
+ args.report = cfg['report']
95
+
96
+ if sys.stdout.isatty():
97
+ print_banner()
98
+
99
+ section(f"SCANNING {args.path}")
100
+
101
+ path = Path(args.path)
102
+ if not path.exists():
103
+ error(f"Path not found: {args.path}")
104
+ sys.exit(1)
105
+
106
+ languages = detect_languages(args.path)
107
+ if not languages:
108
+ warning("No supported language files found.")
109
+ sys.exit(0)
110
+
111
+ info(f"Languages detected: {', '.join(languages)}")
112
+
113
+ # Count lines of code
114
+ loc = sum(
115
+ len(f.read_text(encoding='utf-8', errors='ignore').splitlines())
116
+ for f in path.rglob('*.py')
117
+ )
118
+
119
+ # Create scan result
120
+ result = ScanResult(str(args.path), loc=loc)
121
+ result.languages = languages
122
+
123
+ total_high = 0
124
+ total_medium = 0
125
+ total_low = 0
126
+
127
+ # ── Python scan via Bandit ─────────────────────────────────────────────
128
+ if 'python' in languages:
129
+ section("Python — Bandit 1.9.4")
130
+ t0 = time.time()
131
+ cmd = [
132
+ sys.executable, '-m', 'bandit',
133
+ '-r', str(path),
134
+ '-f', 'json', '-q'
135
+ ]
136
+ res = subprocess.run(cmd, capture_output=True, text=True) # nosec B603
137
+ elapsed = round(time.time() - t0, 2)
138
+ info(f"Scan completed in {elapsed}s")
139
+ output = res.stdout or res.stderr
140
+ json_start = output.find('{')
141
+
142
+ if json_start >= 0:
143
+ data = jsonlib.loads(output[json_start:])
144
+ result = parse_bandit_to_result(data, str(args.path), loc)
145
+ result.languages = languages
146
+
147
+ high = result.high
148
+ medium = result.medium
149
+ low = result.low
150
+
151
+ total_high += len(high)
152
+ total_medium += len(medium)
153
+ total_low += len(low)
154
+
155
+ if high:
156
+ for f in high:
157
+ error(f"[{f.rule_id}] {f.message}")
158
+ info(f" File: {f.filename}")
159
+ info(f" Line: {f.line}")
160
+ info(f" CWE: {f.cwe}")
161
+ if f.code:
162
+ info(f" Code: {f.code.strip()[:80]}")
163
+ print()
164
+
165
+ if medium:
166
+ for f in medium:
167
+ warning(f"[{f.rule_id}] {f.message}")
168
+ info(f" File: {f.filename}")
169
+ info(f" Line: {f.line}")
170
+ print()
171
+
172
+ if low and not (hasattr(args, 'quiet') and args.quiet):
173
+ print(f"\n{Fore.CYAN} ── Low Severity Findings ──{Style.RESET_ALL}")
174
+ for f in low:
175
+ print(f"{Fore.CYAN} ℹ️ [{f.rule_id}] {f.message}{Style.RESET_ALL}")
176
+ info(f" File: {f.filename}")
177
+ info(f" Line: {f.line}")
178
+ info(f" CWE: {f.cwe}")
179
+ if f.code:
180
+ info(f" Code: {f.code.strip()[:80]}")
181
+ print()
182
+
183
+ if not high and not medium and not low:
184
+ success("Zero findings — completely clean")
185
+ elif not high and not medium:
186
+ success("No High or Medium findings detected")
187
+
188
+ info(f"Python total — High: {len(high)} "
189
+ f"Medium: {len(medium)} Low: {len(low)}")
190
+
191
+ # ── JavaScript scan via Semgrep ────────────────────────────────────────
192
+ if 'javascript' in languages or 'typescript' in languages:
193
+ section("JavaScript / TypeScript — Semgrep p/nodejs")
194
+ t0 = time.time()
195
+ try:
196
+ cmd = ['semgrep', '--config=p/nodejs', '--json', str(path)]
197
+ res = subprocess.run( # nosec B603
198
+ cmd, capture_output=True, text=True, timeout=60,
199
+ encoding='utf-8', errors='replace'
200
+ )
201
+ elapsed = round(time.time() - t0, 2)
202
+ info(f"Scan completed in {elapsed}s")
203
+ output = res.stdout
204
+ json_start = output.find('{')
205
+
206
+ if json_start >= 0:
207
+ data = jsonlib.loads(output[json_start:])
208
+ result = parse_semgrep_to_result(data, result)
209
+ findings = [f for f in result.findings if f.source == 'semgrep']
210
+ total_high += len(findings)
211
+
212
+ if findings:
213
+ for f in findings:
214
+ error(f"[SEMGREP] {f.rule_id}")
215
+ info(f" File: {f.filename} — Line {f.line}")
216
+ else:
217
+ success("No findings in JavaScript/TypeScript files")
218
+
219
+ info(f"JS/TS total — Blocking: {len(findings)}")
220
+
221
+ except FileNotFoundError:
222
+ warning("Semgrep not installed — skipping JS/TS scan")
223
+
224
+ # ── Final verdict ──────────────────────────────────────────────────────
225
+ section("SHIELD SCAN RESULT")
226
+
227
+ if total_high > 0:
228
+ print(f" {Fore.RED}High: {total_high} ← must fix before deployment{Style.RESET_ALL}")
229
+ else:
230
+ print(f" {Fore.GREEN}High: {total_high}{Style.RESET_ALL}")
231
+
232
+ if total_medium > 0:
233
+ print(f" {Fore.YELLOW}Medium: {total_medium} ← review recommended{Style.RESET_ALL}")
234
+ else:
235
+ print(f" {Fore.GREEN}Medium: {total_medium}{Style.RESET_ALL}")
236
+
237
+ if total_low > 0:
238
+ print(f" {Fore.CYAN}Low: {total_low} ← best practice violations{Style.RESET_ALL}")
239
+ else:
240
+ print(f" {Fore.GREEN}Low: {total_low}{Style.RESET_ALL}")
241
+
242
+ # ── Grade ──────────────────────────────────────────────────────────────
243
+ print()
244
+ grade_display = {
245
+ "A+": Fore.GREEN, "A": Fore.GREEN,
246
+ "B": Fore.YELLOW, "C": Fore.YELLOW,
247
+ "D": Fore.RED, "F": Fore.RED
248
+ }.get(result.grade, Fore.WHITE)
249
+ print(f" {grade_display}Security Grade: {result.grade} "
250
+ f"· I/100L: {result.i100l}{Style.RESET_ALL}")
251
+
252
+ # ── JSON terminal output ───────────────────────────────────────────────
253
+ if hasattr(args, 'json') and args.json:
254
+ print(jsonlib.dumps(
255
+ [f.to_dict() for f in result.findings], indent=2
256
+ ))
257
+
258
+ # ── HTML Report + JSON file ────────────────────────────────────────────
259
+ if hasattr(args, 'report') and args.report:
260
+ report_path = generate_report(result, args.report)
261
+ json_path = args.report.replace('.html', '.json')
262
+ save_json_report(result, json_path)
263
+ print()
264
+ success(f"Report saved: {report_path}")
265
+ info(f"JSON saved: {json_path}")
266
+
267
+ print()
268
+ if total_high > 0:
269
+ error(f"BLOCKED — {total_high} High-severity finding(s) detected")
270
+ error("Run 'shield fix <path>' to auto-remediate")
271
+ total_time = round(time.time() - scan_start, 2)
272
+ info(f"Total scan time: {total_time}s")
273
+ sys.exit(1)
274
+ elif total_medium > 0:
275
+ warning(f"WARNING — {total_medium} Medium finding(s) — review recommended")
276
+ total_time = round(time.time() - scan_start, 2)
277
+ info(f"Total scan time: {total_time}s")
278
+ sys.exit(0)
279
+ else:
280
+ success("CLEAN — No High or Medium findings detected")
281
+ total_time = round(time.time() - scan_start, 2)
282
+ info(f"Total scan time: {total_time}s")
283
+ sys.exit(0)
284
+
285
+
286
+ # ── FIX COMMAND ───────────────────────────────────────────────────────────────
287
+
288
+ def cmd_fix(args):
289
+ """shield fix <path>"""
290
+ import subprocess # nosec B404 — hardcoded commands only
291
+ import json as jsonlib
292
+ from shield.reporter import (
293
+ generate_report,
294
+ save_json_report,
295
+ parse_bandit_to_result
296
+ )
297
+
298
+ if sys.stdout.isatty():
299
+ print_banner()
300
+
301
+ section(f"FIXING {args.path}")
302
+
303
+ path = Path(args.path)
304
+ if not path.exists():
305
+ error(f"Path not found: {args.path}")
306
+ sys.exit(1)
307
+
308
+ if args.inplace:
309
+ warning("INPLACE MODE — original files will be modified directly")
310
+ else:
311
+ info(f"Safe mode — fixed files will be saved to: {args.output}")
312
+
313
+ try:
314
+ from shield.layer3_fdsp import fdsp_loop
315
+ results = fdsp_loop(
316
+ corpus_dir=str(path),
317
+ output_dir=args.output,
318
+ max_k=args.k,
319
+ inplace=args.inplace
320
+ )
321
+
322
+ section("FIX COMPLETE")
323
+ summary = results.get('summary', {})
324
+ baseline_high = summary.get('baseline_high', 0)
325
+ final_high = summary.get('final_high', 0)
326
+ baseline_i100l = summary.get('baseline_I100L', 0)
327
+ final_i100l = summary.get('final_I100L', 0)
328
+ total_fixes = sum(
329
+ v for k, v in results.get('total_fixes', {}).items()
330
+ if not k.endswith(('_strategy', '_error'))
331
+ )
332
+
333
+ info(f"Baseline High: {baseline_high}")
334
+ info(f"Final High: {final_high}")
335
+ info(f"Baseline I/100L: {baseline_i100l}")
336
+ info(f"Final I/100L: {final_i100l}")
337
+ info(f"Auto-fixes applied: {total_fixes}")
338
+ print()
339
+
340
+ # Fixes breakdown
341
+ if results.get('total_fixes'):
342
+ section("FIXES APPLIED")
343
+ for rule, count in results['total_fixes'].items():
344
+ if not rule.endswith(('_strategy', '_error')):
345
+ success(f"[{rule}] {count} fix(es) applied")
346
+
347
+ # Grade improvement
348
+ if final_high == 0:
349
+ success("Zero High-severity findings remaining")
350
+ if baseline_high > 0:
351
+ reduction = round(
352
+ (baseline_high - final_high) / baseline_high * 100
353
+ )
354
+ success(f"High-severity reduction: {reduction}%")
355
+ else:
356
+ warning(f"{final_high} High finding(s) remain — manual review needed")
357
+
358
+ # ── Auto-scan fixed files → save HTML + JSON ───────────────────────
359
+ fixed_path = Path(args.output) / 'working_copy'
360
+ if fixed_path.exists():
361
+ cmd = [
362
+ sys.executable, '-m', 'bandit',
363
+ '-r', str(fixed_path),
364
+ '-f', 'json', '-q'
365
+ ]
366
+ res = subprocess.run( # nosec B603
367
+ cmd, capture_output=True, text=True
368
+ )
369
+ output = res.stdout or res.stderr
370
+ json_start = output.find('{')
371
+
372
+ if json_start >= 0:
373
+ data = jsonlib.loads(output[json_start:])
374
+ loc = sum(
375
+ len(f.read_text(encoding='utf-8', errors='ignore').splitlines())
376
+ for f in fixed_path.rglob('*.py')
377
+ )
378
+ after_result = parse_bandit_to_result(
379
+ data, str(args.path), loc
380
+ )
381
+
382
+ # Save after.json automatically
383
+ after_json = str(Path(args.output) / 'after.json')
384
+ save_json_report(after_result, after_json)
385
+ success(f"After JSON saved: {after_json}")
386
+
387
+ # Save HTML report if requested
388
+ if hasattr(args, 'report') and args.report:
389
+ report_path = generate_report(after_result, args.report)
390
+ print()
391
+ success(f"Fix report saved: {report_path}")
392
+
393
+ # Save FDSP results JSON if requested
394
+ if hasattr(args, 'output_json') and args.output_json:
395
+ Path(args.output_json).write_text(
396
+ jsonlib.dumps(results, indent=2),
397
+ encoding='utf-8'
398
+ )
399
+ success(f"FDSP results saved: {args.output_json}")
400
+
401
+ except Exception as e:
402
+ error(f"Fix failed: {e}")
403
+ sys.exit(1)
404
+
405
+ # ── COMPARE COMMAND ───────────────────────────────────────────────────────────
406
+
407
+ def cmd_compare(args):
408
+ """shield compare before.json after.json --report comparison.html"""
409
+ import json as jsonlib
410
+ from shield.reporter import (
411
+ ScanResult, Finding,
412
+ generate_comparison_report
413
+ )
414
+
415
+ if sys.stdout.isatty():
416
+ print_banner()
417
+
418
+ section(f"COMPARING {args.before} vs {args.after}")
419
+
420
+ def load_result(json_path: str) -> ScanResult:
421
+ data = jsonlib.loads(Path(json_path).read_text(encoding='utf-8'))
422
+ result = ScanResult(
423
+ data.get('corpus', json_path),
424
+ loc=data.get('loc', 0)
425
+ )
426
+ result.languages = data.get('languages', [])
427
+ for f in data.get('findings', []):
428
+ result.add_finding(Finding(
429
+ rule_id = f.get('rule_id', ''),
430
+ severity = f.get('severity', 'LOW'),
431
+ cwe = f.get('cwe', 'N/A'),
432
+ message = f.get('message', ''),
433
+ filename = f.get('filename', ''),
434
+ line = f.get('line', 0),
435
+ code = f.get('code', ''),
436
+ source = f.get('source', 'bandit'),
437
+ ))
438
+ return result
439
+
440
+ try:
441
+ before_result = load_result(args.before)
442
+ after_result = load_result(args.after)
443
+
444
+ info(f"Before — Grade: {before_result.grade} "
445
+ f"Findings: {len(before_result.findings)} "
446
+ f"I/100L: {before_result.i100l}")
447
+ info(f"After — Grade: {after_result.grade} "
448
+ f"Findings: {len(after_result.findings)} "
449
+ f"I/100L: {after_result.i100l}")
450
+
451
+ report_path = generate_comparison_report(
452
+ before_result, after_result, args.report
453
+ )
454
+ print()
455
+ success(f"Comparison report saved: {report_path}")
456
+
457
+ except Exception as e:
458
+ error(f"Compare failed: {e}")
459
+ sys.exit(1)
460
+
461
+ # ── VERSION COMMAND ───────────────────────────────────────────────────────────
462
+
463
+ def cmd_version(args):
464
+ print_banner()
465
+ info("SHIELD Security v0.4.0")
466
+ info("Author: Ali Yasin Idrees")
467
+ info("GitHub: github.com/Aliidrees1234/llm-security-research")
468
+ info("Research: LLM Security in Web Applications — 2026")
469
+ info("Engine: Full AST engine — 11 rules — 91 tests — HTML/JSON reports")
470
+
471
+
472
+ # ── INIT COMMAND ──────────────────────────────────────────────────────────────
473
+ def cmd_init(args):
474
+ """shield init — create .shieldrc"""
475
+ from shield.config import create_default_config
476
+
477
+ if sys.stdout.isatty():
478
+ print_banner()
479
+
480
+ section("SHIELD INIT")
481
+
482
+ rc_path = Path(".shieldrc")
483
+ if rc_path.exists():
484
+ warning(".shieldrc already exists — not overwriting")
485
+ info("Delete it first if you want a fresh config")
486
+ return
487
+
488
+ # Detect project type
489
+ has_flask = any(Path(".").rglob("*.py")) and any(
490
+ "flask" in f.read_text(encoding='utf-8', errors='ignore').lower()
491
+ for f in list(Path(".").rglob("*.py"))[:10]
492
+ )
493
+ has_node = Path("package.json").exists()
494
+ has_tests = Path("tests").exists() or Path("test").exists()
495
+
496
+ # Create config
497
+ create_default_config(".shieldrc")
498
+ success(".shieldrc created")
499
+
500
+ # Show what was detected
501
+ print()
502
+ info("Project detected:")
503
+ if has_flask: info(" ✅ Python / Flask")
504
+ if has_node: info(" ✅ Node.js / Express")
505
+ if has_tests: info(" ✅ Tests folder found")
506
+
507
+ # Offer to add to .gitignore
508
+ gitignore = Path(".gitignore")
509
+ if gitignore.exists():
510
+ content = gitignore.read_text(encoding='utf-8')
511
+ if '.shieldrc' not in content:
512
+ print()
513
+ warning(".shieldrc may contain sensitive paths — add to .gitignore?")
514
+ info("Run: echo '.shieldrc' >> .gitignore")
515
+
516
+ print()
517
+ info("Next steps:")
518
+ info(" 1. Edit .shieldrc to customize for this project")
519
+ info(" 2. Run: shield scan .")
520
+ info(" 3. Run: shield fix . --output ./fixed")
521
+
522
+ # ── MAIN ──────────────────────────────────────────────────────────────────────
523
+
524
+ def main():
525
+ parser = argparse.ArgumentParser(
526
+ prog="shield",
527
+ description="SHIELD Security — Automated security layer for LLM-assisted development",
528
+ formatter_class=argparse.RawDescriptionHelpFormatter,
529
+ epilog="""
530
+ Examples:
531
+ shield scan ./my_project
532
+ shield scan ./my_project --report report.html
533
+ shield fix ./my_project
534
+ shield fix ./my_project --inplace
535
+ shield fix ./my_project --output ./fixed --k 3 --report after.html
536
+ shield compare before.json after.json --report comparison.html
537
+ shield version
538
+ """
539
+ )
540
+
541
+ subparsers = parser.add_subparsers(dest="command")
542
+
543
+ # ── scan ──────────────────────────────────────────────────────────────
544
+ scan_parser = subparsers.add_parser(
545
+ "scan",
546
+ help="Scan a directory for security vulnerabilities"
547
+ )
548
+ scan_parser.add_argument("path", help="Directory to scan")
549
+ scan_parser.add_argument(
550
+ "--report", default=None,
551
+ help="Generate HTML report (e.g. --report report.html)"
552
+ )
553
+ scan_parser.add_argument(
554
+ "--json", action="store_true", default=False,
555
+ help="Output results as JSON to terminal"
556
+ )
557
+ scan_parser.add_argument(
558
+ "--quiet", "-q",
559
+ action="store_true",
560
+ default=False,
561
+ help="Only find High and Medium findings"
562
+ )
563
+
564
+ # ── fix ───────────────────────────────────────────────────────────────
565
+ fix_parser = subparsers.add_parser(
566
+ "fix",
567
+ help="Scan and automatically fix security vulnerabilities"
568
+ )
569
+ fix_parser.add_argument("path", help="Directory to fix")
570
+ fix_parser.add_argument(
571
+ "--inplace", action="store_true", default=False,
572
+ help="Fix original files directly (WARNING: modifies your files)"
573
+ )
574
+ fix_parser.add_argument(
575
+ "--output", default="shield_results",
576
+ help="Output directory for fixed files (default: shield_results)"
577
+ )
578
+ fix_parser.add_argument(
579
+ "--k", type=int, default=3,
580
+ help="Maximum FDSP iterations (default: 3)"
581
+ )
582
+ fix_parser.add_argument(
583
+ "--report", default=None,
584
+ help="Generate HTML report of fixed code"
585
+ )
586
+ fix_parser.add_argument(
587
+ "--output-json", default=None, dest="output_json",
588
+ help="Save FDSP results as JSON"
589
+ )
590
+
591
+ # ── compare ───────────────────────────────────────────────────────────
592
+ compare_parser = subparsers.add_parser(
593
+ "compare",
594
+ help="Compare two JSON scan reports (before vs after)"
595
+ )
596
+ compare_parser.add_argument("before", help="Before JSON report path")
597
+ compare_parser.add_argument("after", help="After JSON report path")
598
+ compare_parser.add_argument(
599
+ "--report", default="comparison.html",
600
+ help="Output comparison HTML (default: comparison.html)"
601
+ )
602
+
603
+ # ── version ───────────────────────────────────────────────────────────
604
+ subparsers.add_parser("version", help="Show SHIELD version and author info")
605
+
606
+ # ── init ──────────────────────────────────────────────────────────────────
607
+ subparsers.add_parser(
608
+ "init",
609
+ help="Create a .shieldrc config file in current directory"
610
+ )
611
+
612
+ # ── parse and dispatch ─────────────────────────────────────────────────
613
+ args = parser.parse_args()
614
+
615
+ if args.command == "scan":
616
+ cmd_scan(args)
617
+ elif args.command == "fix":
618
+ cmd_fix(args)
619
+ elif args.command == "compare":
620
+ cmd_compare(args)
621
+ elif args.command == "version":
622
+ cmd_version(args)
623
+ elif args.command == "init":
624
+ cmd_init(args)
625
+ else:
626
+ print_banner()
627
+ parser.print_help()
628
+
629
+
630
+
631
+ if __name__ == "__main__":
632
+ main()