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 +3 -0
- shield/cli.py +632 -0
- shield/config.py +116 -0
- shield/layer2_grasp.py +347 -0
- shield/layer3_fdsp.py +369 -0
- shield/layer5_runtime.py +480 -0
- shield/reporter.py +672 -0
- shield/rules/__init__.py +3 -0
- shield/rules/custom_rules.py +1190 -0
- shield_security-0.5.0.dist-info/METADATA +323 -0
- shield_security-0.5.0.dist-info/RECORD +15 -0
- shield_security-0.5.0.dist-info/WHEEL +5 -0
- shield_security-0.5.0.dist-info/entry_points.txt +2 -0
- shield_security-0.5.0.dist-info/licenses/LICENSE +21 -0
- shield_security-0.5.0.dist-info/top_level.txt +1 -0
shield/__init__.py
ADDED
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()
|