skylos 2.2.4__py3-none-any.whl → 2.4.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.
Potentially problematic release.
This version of skylos might be problematic. Click here for more details.
- skylos/__init__.py +1 -1
- skylos/analyzer.py +107 -76
- skylos/cli.py +156 -8
- skylos/rules/danger/__init__.py +0 -0
- skylos/rules/danger/danger.py +141 -0
- skylos/rules/danger/danger_cmd/__init__.py +0 -0
- skylos/rules/danger/danger_cmd/cmd_flow.py +208 -0
- skylos/rules/danger/danger_fs/__init__.py +0 -0
- skylos/rules/danger/danger_fs/path_flow.py +188 -0
- skylos/rules/danger/danger_net/__init__.py +0 -0
- skylos/rules/danger/danger_net/ssrf_flow.py +198 -0
- skylos/rules/danger/danger_sql/__init__.py +0 -0
- skylos/rules/danger/danger_sql/sql_flow.py +175 -0
- skylos/rules/danger/danger_sql/sql_raw_flow.py +202 -0
- skylos/rules/danger/danger_web/__init__.py +0 -0
- skylos/rules/danger/danger_web/xss_flow.py +279 -0
- {skylos-2.2.4.dist-info → skylos-2.4.0.dist-info}/METADATA +1 -1
- {skylos-2.2.4.dist-info → skylos-2.4.0.dist-info}/RECORD +26 -10
- test/test_cmd_injection.py +41 -0
- test/test_dangerous.py +32 -1
- test/test_path_traversal.py +40 -0
- test/test_sql_injection.py +54 -0
- test/test_ssrf.py +51 -0
- skylos/rules/dangerous.py +0 -135
- {skylos-2.2.4.dist-info → skylos-2.4.0.dist-info}/WHEEL +0 -0
- {skylos-2.2.4.dist-info → skylos-2.4.0.dist-info}/entry_points.txt +0 -0
- {skylos-2.2.4.dist-info → skylos-2.4.0.dist-info}/top_level.txt +0 -0
skylos/__init__.py
CHANGED
skylos/analyzer.py
CHANGED
|
@@ -9,7 +9,7 @@ from skylos.visitor import Visitor
|
|
|
9
9
|
from skylos.constants import ( PENALTIES, AUTO_CALLED )
|
|
10
10
|
from skylos.visitors.test_aware import TestAwareVisitor
|
|
11
11
|
from skylos.rules.secrets import scan_ctx as _secrets_scan_ctx
|
|
12
|
-
from skylos.rules.
|
|
12
|
+
from skylos.rules.danger.danger import scan_ctx as scan_danger
|
|
13
13
|
import os
|
|
14
14
|
import traceback
|
|
15
15
|
from skylos.visitors.framework_aware import FrameworkAwareVisitor, detect_framework_usage
|
|
@@ -239,7 +239,7 @@ class Skylos:
|
|
|
239
239
|
if method.simple_name == "format" and cls.endswith("Formatter"):
|
|
240
240
|
method.references += 1
|
|
241
241
|
|
|
242
|
-
def analyze(self, path, thr=60, exclude_folders= None, enable_secrets = False,
|
|
242
|
+
def analyze(self, path, thr=60, exclude_folders= None, enable_secrets = False, enable_danger = False):
|
|
243
243
|
files, root = self._get_python_files(path, exclude_folders)
|
|
244
244
|
|
|
245
245
|
if not files:
|
|
@@ -288,13 +288,15 @@ class Skylos:
|
|
|
288
288
|
except Exception:
|
|
289
289
|
pass
|
|
290
290
|
|
|
291
|
-
if
|
|
291
|
+
if enable_danger and scan_danger is not None:
|
|
292
292
|
try:
|
|
293
|
-
findings =
|
|
293
|
+
findings = scan_danger(root, [file])
|
|
294
294
|
if findings:
|
|
295
295
|
all_dangers.extend(findings)
|
|
296
|
-
except Exception:
|
|
297
|
-
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.error(f"Error scanning {file} for dangerous code: {e}")
|
|
298
|
+
if os.getenv("SKYLOS_DEBUG"):
|
|
299
|
+
logger.error(traceback.format_exc())
|
|
298
300
|
|
|
299
301
|
self._mark_refs()
|
|
300
302
|
self._apply_heuristics()
|
|
@@ -331,9 +333,9 @@ class Skylos:
|
|
|
331
333
|
result["secrets"] = all_secrets
|
|
332
334
|
result["analysis_summary"]["secrets_count"] = len(all_secrets)
|
|
333
335
|
|
|
334
|
-
if
|
|
335
|
-
result["
|
|
336
|
-
result["analysis_summary"]["
|
|
336
|
+
if enable_danger and all_dangers:
|
|
337
|
+
result["danger"] = all_dangers
|
|
338
|
+
result["analysis_summary"]["danger_count"] = len(all_dangers)
|
|
337
339
|
|
|
338
340
|
for u in unused:
|
|
339
341
|
if u["type"] in ("function", "method"):
|
|
@@ -386,75 +388,104 @@ def proc_file(file_or_args, mod=None):
|
|
|
386
388
|
|
|
387
389
|
return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
|
|
388
390
|
|
|
389
|
-
def analyze(path, conf=60, exclude_folders=None, enable_secrets=False,
|
|
390
|
-
return Skylos().analyze(path,conf, exclude_folders, enable_secrets,
|
|
391
|
+
def analyze(path, conf=60, exclude_folders=None, enable_secrets=False, enable_danger=False):
|
|
392
|
+
return Skylos().analyze(path,conf, exclude_folders, enable_secrets, enable_danger)
|
|
391
393
|
|
|
392
394
|
if __name__ == "__main__":
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if len(sys.argv) > 2:
|
|
397
|
-
confidence = int(sys.argv[2])
|
|
398
|
-
else:
|
|
399
|
-
confidence = 60
|
|
395
|
+
enable_secrets = ("--secrets" in sys.argv)
|
|
396
|
+
enable_danger = ("--danger" in sys.argv)
|
|
400
397
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
print("
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
print("Summary:")
|
|
413
|
-
if data["unused_functions"]:
|
|
414
|
-
print(f" * Unreachable functions: {len(data['unused_functions'])}")
|
|
415
|
-
if data["unused_imports"]:
|
|
416
|
-
print(f" * Unused imports: {len(data['unused_imports'])}")
|
|
417
|
-
if data["unused_classes"]:
|
|
418
|
-
print(f" * Unused classes: {len(data['unused_classes'])}")
|
|
419
|
-
if data["unused_variables"]:
|
|
420
|
-
print(f" * Unused variables: {len(data['unused_variables'])}")
|
|
421
|
-
|
|
422
|
-
if data["unused_functions"]:
|
|
423
|
-
print("\n - Unreachable Functions")
|
|
424
|
-
print("=======================")
|
|
425
|
-
for i, func in enumerate(data["unused_functions"], 1):
|
|
426
|
-
print(f" {i}. {func['name']}")
|
|
427
|
-
print(f" └─ {func['file']}:{func['line']}")
|
|
428
|
-
|
|
429
|
-
if data["unused_imports"]:
|
|
430
|
-
print("\n - Unused Imports")
|
|
431
|
-
print("================")
|
|
432
|
-
for i, imp in enumerate(data["unused_imports"], 1):
|
|
433
|
-
print(f" {i}. {imp['simple_name']}")
|
|
434
|
-
print(f" └─ {imp['file']}:{imp['line']}")
|
|
435
|
-
|
|
436
|
-
if data["unused_classes"]:
|
|
437
|
-
print("\n - Unused Classes")
|
|
438
|
-
print("=================")
|
|
439
|
-
for i, cls in enumerate(data["unused_classes"], 1):
|
|
440
|
-
print(f" {i}. {cls['name']}")
|
|
441
|
-
print(f" └─ {cls['file']}:{cls['line']}")
|
|
442
|
-
|
|
443
|
-
if data["unused_variables"]:
|
|
444
|
-
print("\n - Unused Variables")
|
|
445
|
-
print("==================")
|
|
446
|
-
for i, var in enumerate(data["unused_variables"], 1):
|
|
447
|
-
print(f" {i}. {var['name']}")
|
|
448
|
-
print(f" └─ {var['file']}:{var['line']}")
|
|
449
|
-
|
|
450
|
-
print("\n" + "─" * 50)
|
|
451
|
-
print(f"Found {total_items} dead code items. Add this badge to your README:")
|
|
452
|
-
print(f"```markdown")
|
|
453
|
-
print(f"")
|
|
454
|
-
print(f"```")
|
|
398
|
+
positional = [a for a in sys.argv[1:] if not a.startswith("--")]
|
|
399
|
+
|
|
400
|
+
if not positional:
|
|
401
|
+
print("Usage: python Skylos.py <path> [confidence_threshold] [--secrets] [--danger]")
|
|
402
|
+
sys.exit(2)
|
|
403
|
+
|
|
404
|
+
p = positional[0]
|
|
405
|
+
confidence = int(positional[1]) if len(positional) > 1 else 60
|
|
406
|
+
|
|
407
|
+
result = analyze(p, confidence, enable_secrets=enable_secrets, enable_danger=enable_danger)
|
|
455
408
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
409
|
+
data = json.loads(result)
|
|
410
|
+
print("\n Python Static Analysis Results")
|
|
411
|
+
print("===================================\n")
|
|
412
|
+
|
|
413
|
+
total_dead = 0
|
|
414
|
+
for key, items in data.items():
|
|
415
|
+
if key.startswith("unused_") and isinstance(items, list):
|
|
416
|
+
total_dead += len(items)
|
|
417
|
+
|
|
418
|
+
danger_count = data.get("analysis_summary", {}).get("danger_count", 0) if enable_danger else 0
|
|
419
|
+
secrets_count = data.get("analysis_summary", {}).get("secrets_count", 0) if enable_secrets else 0
|
|
420
|
+
|
|
421
|
+
print("Summary:")
|
|
422
|
+
if data["unused_functions"]:
|
|
423
|
+
print(f" * Unreachable functions: {len(data['unused_functions'])}")
|
|
424
|
+
if data["unused_imports"]:
|
|
425
|
+
print(f" * Unused imports: {len(data['unused_imports'])}")
|
|
426
|
+
if data["unused_classes"]:
|
|
427
|
+
print(f" * Unused classes: {len(data['unused_classes'])}")
|
|
428
|
+
if data["unused_variables"]:
|
|
429
|
+
print(f" * Unused variables: {len(data['unused_variables'])}")
|
|
430
|
+
if enable_danger:
|
|
431
|
+
print(f" * Security issues: {danger_count}")
|
|
432
|
+
if enable_secrets:
|
|
433
|
+
print(f" * Secrets found: {secrets_count}")
|
|
434
|
+
|
|
435
|
+
if data["unused_functions"]:
|
|
436
|
+
print("\n - Unreachable Functions")
|
|
437
|
+
print("=======================")
|
|
438
|
+
for i, func in enumerate(data["unused_functions"], 1):
|
|
439
|
+
print(f" {i}. {func['name']}")
|
|
440
|
+
print(f" └─ {func['file']}:{func['line']}")
|
|
441
|
+
|
|
442
|
+
if data["unused_imports"]:
|
|
443
|
+
print("\n - Unused Imports")
|
|
444
|
+
print("================")
|
|
445
|
+
for i, imp in enumerate(data["unused_imports"], 1):
|
|
446
|
+
print(f" {i}. {imp['simple_name']}")
|
|
447
|
+
print(f" └─ {imp['file']}:{imp['line']}")
|
|
448
|
+
|
|
449
|
+
if data["unused_classes"]:
|
|
450
|
+
print("\n - Unused Classes")
|
|
451
|
+
print("=================")
|
|
452
|
+
for i, cls in enumerate(data["unused_classes"], 1):
|
|
453
|
+
print(f" {i}. {cls['name']}")
|
|
454
|
+
print(f" └─ {cls['file']}:{cls['line']}")
|
|
455
|
+
|
|
456
|
+
if data["unused_variables"]:
|
|
457
|
+
print("\n - Unused Variables")
|
|
458
|
+
print("==================")
|
|
459
|
+
for i, var in enumerate(data["unused_variables"], 1):
|
|
460
|
+
print(f" {i}. {var['name']}")
|
|
461
|
+
print(f" └─ {var['file']}:{var['line']}")
|
|
462
|
+
|
|
463
|
+
if enable_danger and data.get("danger"):
|
|
464
|
+
print("\n - Security Issues")
|
|
465
|
+
print("================")
|
|
466
|
+
for i, f in enumerate(data["danger"], 1):
|
|
467
|
+
print(f" {i}. {f['message']} [{f['rule_id']}] ({f['file']}:{f['line']}) Severity: {f['severity']}")
|
|
468
|
+
|
|
469
|
+
if enable_secrets and data.get("secrets"):
|
|
470
|
+
print("\n - Secrets")
|
|
471
|
+
print("==========")
|
|
472
|
+
for i, s in enumerate(data["secrets"], 1):
|
|
473
|
+
rid = s.get("rule_id", "SECRET")
|
|
474
|
+
msg = s.get("message", "Potential secret")
|
|
475
|
+
file = s.get("file")
|
|
476
|
+
line = s.get("line", 1)
|
|
477
|
+
sev = s.get("severity", "HIGH")
|
|
478
|
+
print(f" {i}. {msg} [{rid}] ({file}:{line}) Severity: {sev}")
|
|
479
|
+
|
|
480
|
+
print("\n" + "─" * 50)
|
|
481
|
+
if enable_danger:
|
|
482
|
+
print(f"Found {total_dead} dead code items and {danger_count} security flaws. Add this badge to your README:")
|
|
459
483
|
else:
|
|
460
|
-
print("
|
|
484
|
+
print(f"Found {total_dead} dead code items. Add this badge to your README:")
|
|
485
|
+
print("```markdown")
|
|
486
|
+
print(f"")
|
|
487
|
+
print("```")
|
|
488
|
+
|
|
489
|
+
print("\nNext steps:")
|
|
490
|
+
print(" * Use --interactive to select specific items to remove")
|
|
491
|
+
print(" * Use --dry-run to preview changes before applying them")
|
skylos/cli.py
CHANGED
|
@@ -159,19 +159,25 @@ def interactive_selection(logger, unused_functions, unused_imports):
|
|
|
159
159
|
|
|
160
160
|
return selected_functions, selected_imports
|
|
161
161
|
|
|
162
|
-
def print_badge(dead_code_count
|
|
162
|
+
def print_badge(dead_code_count, logger, *, danger_enabled = False, danger_count = 0):
|
|
163
163
|
logger.info(f"\n{Colors.GRAY}{'─' * 50}{Colors.RESET}")
|
|
164
164
|
|
|
165
|
-
if dead_code_count == 0:
|
|
166
|
-
logger.info(
|
|
165
|
+
if dead_code_count == 0 and (not danger_enabled or danger_count == 0):
|
|
166
|
+
logger.info(" Your code is 100% dead code free! Add this badge to your README:")
|
|
167
167
|
logger.info("```markdown")
|
|
168
168
|
logger.info("")
|
|
169
169
|
logger.info("```")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
if danger_enabled:
|
|
173
|
+
logger.info(f"Found {dead_code_count} dead code items and {danger_count} security flaws. Add this badge to your README:")
|
|
170
174
|
else:
|
|
171
175
|
logger.info(f"Found {dead_code_count} dead code items. Add this badge to your README:")
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
|
|
177
|
+
logger.info("```markdown")
|
|
178
|
+
logger.info(f"")
|
|
179
|
+
logger.info("```")
|
|
180
|
+
|
|
175
181
|
|
|
176
182
|
def main():
|
|
177
183
|
if len(sys.argv) > 1 and sys.argv[1] == 'run':
|
|
@@ -188,6 +194,12 @@ def main():
|
|
|
188
194
|
)
|
|
189
195
|
parser.add_argument("path", help="Path to the Python project")
|
|
190
196
|
|
|
197
|
+
parser.add_argument(
|
|
198
|
+
"--table",
|
|
199
|
+
action="store_true",
|
|
200
|
+
help="Show findings in table"
|
|
201
|
+
)
|
|
202
|
+
|
|
191
203
|
parser.add_argument(
|
|
192
204
|
"--version",
|
|
193
205
|
action="version",
|
|
@@ -307,7 +319,8 @@ def main():
|
|
|
307
319
|
logger.info(f"{Colors.GREEN}📁 No folders excluded{Colors.RESET}")
|
|
308
320
|
|
|
309
321
|
try:
|
|
310
|
-
result_json = run_analyze(args.path, conf=args.confidence, enable_secrets=bool(args.secrets),
|
|
322
|
+
result_json = run_analyze(args.path, conf=args.confidence, enable_secrets=bool(args.secrets),
|
|
323
|
+
enable_danger=bool(args.danger), exclude_folders=list(final_exclude_folders))
|
|
311
324
|
|
|
312
325
|
if args.json:
|
|
313
326
|
print(result_json)
|
|
@@ -318,6 +331,121 @@ def main():
|
|
|
318
331
|
except Exception as e:
|
|
319
332
|
logger.error(f"Error during analysis: {e}")
|
|
320
333
|
sys.exit(1)
|
|
334
|
+
|
|
335
|
+
if args.table:
|
|
336
|
+
ELLIPSIS = "…"
|
|
337
|
+
|
|
338
|
+
def clip(text, max_length):
|
|
339
|
+
if not text:
|
|
340
|
+
text = ""
|
|
341
|
+
|
|
342
|
+
if len(text) <= max_length:
|
|
343
|
+
return text
|
|
344
|
+
|
|
345
|
+
truncated_end = max(0, max_length - 1)
|
|
346
|
+
return text[:truncated_end] + ELLIPSIS
|
|
347
|
+
|
|
348
|
+
def severity_color(severity):
|
|
349
|
+
severity_upper = (severity or "").upper()
|
|
350
|
+
|
|
351
|
+
if severity_upper in ("HIGH", "CRITICAL"):
|
|
352
|
+
return Colors.RED
|
|
353
|
+
elif severity_upper == "MEDIUM":
|
|
354
|
+
return Colors.YELLOW
|
|
355
|
+
else:
|
|
356
|
+
return Colors.GRAY
|
|
357
|
+
|
|
358
|
+
print(f"\n{Colors.CYAN}{Colors.BOLD}Unused {Colors.RESET}")
|
|
359
|
+
print(f"{Colors.CYAN}{'='*18}{Colors.RESET}")
|
|
360
|
+
|
|
361
|
+
print(f"{Colors.BOLD}{'kind':9} {'name':28} {'where'}{Colors.RESET}")
|
|
362
|
+
print(f"{'-'*9} {'-'*28} {'-'*36}")
|
|
363
|
+
|
|
364
|
+
unused_categories = [
|
|
365
|
+
("unused_functions", "function"),
|
|
366
|
+
("unused_imports", "import"),
|
|
367
|
+
("unused_classes", "class"),
|
|
368
|
+
("unused_variables", "variable"),
|
|
369
|
+
("unused_parameters", "parameter"),
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
for bucket, kind in unused_categories:
|
|
373
|
+
items = result.get(bucket, [])
|
|
374
|
+
for item in items:
|
|
375
|
+
name = item.get("name") or item.get("simple_name") or ""
|
|
376
|
+
file_path = item.get('file', '?')
|
|
377
|
+
line_num = item.get('line', item.get('lineno', '?'))
|
|
378
|
+
where = f"{file_path}:{line_num}"
|
|
379
|
+
|
|
380
|
+
clipped_name = clip(name, 28)
|
|
381
|
+
clipped_where = clip(where, 36)
|
|
382
|
+
print(f"{kind:9} {clipped_name:28} {clipped_where}")
|
|
383
|
+
|
|
384
|
+
secrets = result.get("secrets", []) or []
|
|
385
|
+
if secrets:
|
|
386
|
+
print(f"\n{Colors.RED}{Colors.BOLD}Secrets{Colors.RESET}")
|
|
387
|
+
print(f"{Colors.RED}{'=' * 7}{Colors.RESET}")
|
|
388
|
+
print(f"{Colors.BOLD}{'provider':12} {'message':22} {'preview':24} {'where'}{Colors.RESET}")
|
|
389
|
+
print(f"{'-' * 12} {'-' * 22} {'-' * 24} {'-' * 36}")
|
|
390
|
+
|
|
391
|
+
for secret in secrets[:100]:
|
|
392
|
+
provider = clip(secret.get("provider") or "generic", 12)
|
|
393
|
+
message = clip(secret.get("message") or "Secret detected", 22)
|
|
394
|
+
preview = clip(secret.get("preview") or "****", 24)
|
|
395
|
+
|
|
396
|
+
file_path = secret.get('file', '?')
|
|
397
|
+
line_num = secret.get('line', '?')
|
|
398
|
+
location = f"{file_path}:{line_num}"
|
|
399
|
+
clipped_location = clip(location, 36)
|
|
400
|
+
|
|
401
|
+
print(f"{Colors.MAGENTA}{provider:12}{Colors.RESET} {message:22} {preview:24} {clipped_location}")
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
security_issues = result.get("danger", [])
|
|
405
|
+
if security_issues:
|
|
406
|
+
print(f"\n{Colors.RED}{Colors.BOLD}Security issues{Colors.RESET}")
|
|
407
|
+
print(f"{Colors.RED}{'=' * 15}{Colors.RESET}")
|
|
408
|
+
print(f"{Colors.BOLD}{'rule_id':10} {'sev':5} {'message':38} {'where'}{Colors.RESET}")
|
|
409
|
+
print(f"{'-' * 10} {'-' * 5} {'-' * 38} {'-' * 36}")
|
|
410
|
+
|
|
411
|
+
for issue in security_issues[:100]:
|
|
412
|
+
rule_id = clip(issue.get("rule_id") or "", 10)
|
|
413
|
+
severity = (issue.get("severity") or "").upper()
|
|
414
|
+
message = clip(issue.get("message") or "", 38)
|
|
415
|
+
|
|
416
|
+
file_path = issue.get('file', '?')
|
|
417
|
+
line_num = issue.get('line', '?')
|
|
418
|
+
location = f"{file_path}:{line_num}"
|
|
419
|
+
clipped_location = clip(location, 36)
|
|
420
|
+
|
|
421
|
+
severity_color_code = severity_color(severity)
|
|
422
|
+
clipped_severity = clip(severity, 5)
|
|
423
|
+
|
|
424
|
+
print(f"{rule_id:10} {severity_color_code}{clipped_severity:5}{Colors.RESET} {message:38} {clipped_location}")
|
|
425
|
+
|
|
426
|
+
summ = result.get("analysis_summary", {})
|
|
427
|
+
unused_keys = [
|
|
428
|
+
"unused_functions",
|
|
429
|
+
"unused_imports",
|
|
430
|
+
"unused_classes",
|
|
431
|
+
"unused_variables",
|
|
432
|
+
"unused_parameters"
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
total_unused = 0
|
|
436
|
+
for k in unused_keys:
|
|
437
|
+
total_unused += len(result.get(k, []))
|
|
438
|
+
|
|
439
|
+
print(f"\n{Colors.BOLD}Summary{Colors.RESET}")
|
|
440
|
+
|
|
441
|
+
print("=======")
|
|
442
|
+
print(f"files analyzed : {summ.get('total_files','?')}")
|
|
443
|
+
print(f"unused items : {total_unused}")
|
|
444
|
+
if "secrets_count" in summ:
|
|
445
|
+
print(f"secrets : {summ['secrets_count']}")
|
|
446
|
+
if "danger_count" in summ:
|
|
447
|
+
print(f"security issues: {summ['danger_count']}")
|
|
448
|
+
return
|
|
321
449
|
|
|
322
450
|
if args.json:
|
|
323
451
|
lg = logging.getLogger('skylos')
|
|
@@ -335,6 +463,7 @@ def main():
|
|
|
335
463
|
unused_variables = result.get("unused_variables", [])
|
|
336
464
|
unused_classes = result.get("unused_classes", [])
|
|
337
465
|
secrets_findings = result.get("secrets", [])
|
|
466
|
+
danger_findings = result.get("danger", [])
|
|
338
467
|
|
|
339
468
|
logger.info(f"{Colors.CYAN}{Colors.BOLD} Python Static Analysis Results{Colors.RESET}")
|
|
340
469
|
logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
|
|
@@ -347,6 +476,8 @@ def main():
|
|
|
347
476
|
logger.info(f" * Unused classes: {Colors.YELLOW}{len(unused_classes)}{Colors.RESET}")
|
|
348
477
|
if secrets_findings:
|
|
349
478
|
logger.info(f" * Secrets: {Colors.RED}{len(secrets_findings)}{Colors.RESET}")
|
|
479
|
+
if danger_findings:
|
|
480
|
+
logger.info(f" * Security issues: {Colors.RED}{len(danger_findings)}{Colors.RESET}")
|
|
350
481
|
|
|
351
482
|
if args.interactive and (unused_functions or unused_imports):
|
|
352
483
|
logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
|
|
@@ -477,10 +608,27 @@ def main():
|
|
|
477
608
|
prev = s.get("preview", "****")
|
|
478
609
|
msg = s.get("message", "Secret detected")
|
|
479
610
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{msg} [{provider}] {Colors.GRAY}({where}){Colors.RESET} -> {prev}")
|
|
611
|
+
|
|
612
|
+
if danger_findings:
|
|
613
|
+
logger.info(f"\n{Colors.RED}{Colors.BOLD} - Security Issues{Colors.RESET}")
|
|
614
|
+
logger.info(f"{Colors.RED}{'=' * 16}{Colors.RESET}")
|
|
615
|
+
for i, d in enumerate(danger_findings[:20], 1):
|
|
616
|
+
rule_id = d.get("rule_id", "unknown_rule")
|
|
617
|
+
severity = d.get("severity", "UNKNOWN").upper()
|
|
618
|
+
message = d.get("message", "Issue detected")
|
|
619
|
+
file = d.get("file", "?")
|
|
620
|
+
line = d.get("line", "?")
|
|
621
|
+
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{message} [{rule_id}] {Colors.GRAY}({file}:{line}){Colors.RESET} Severity: {severity}")
|
|
480
622
|
|
|
481
623
|
dead_code_count = len(unused_functions) + len(unused_imports) + len(unused_variables) + len(unused_classes) + len(unused_parameters)
|
|
482
624
|
|
|
483
|
-
|
|
625
|
+
danger_count = len(danger_findings) if args.danger else 0
|
|
626
|
+
print_badge(
|
|
627
|
+
dead_code_count,
|
|
628
|
+
logger,
|
|
629
|
+
danger_enabled=bool(args.danger),
|
|
630
|
+
danger_count=danger_count,
|
|
631
|
+
)
|
|
484
632
|
|
|
485
633
|
if unused_functions or unused_imports:
|
|
486
634
|
logger.info(f"\n{Colors.BOLD}Next steps:{Colors.RESET}")
|
|
File without changes
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .danger_sql.sql_flow import scan as scan_sql
|
|
6
|
+
from .danger_cmd.cmd_flow import scan as scan_cmd
|
|
7
|
+
from .danger_sql.sql_raw_flow import scan as scan_sql_raw
|
|
8
|
+
from .danger_net.ssrf_flow import scan as scan_ssrf
|
|
9
|
+
from .danger_fs.path_flow import scan as scan_path
|
|
10
|
+
from .danger_web.xss_flow import scan as scan_xss
|
|
11
|
+
|
|
12
|
+
ALLOWED_SUFFIXES = (".py", ".pyi", ".pyw")
|
|
13
|
+
|
|
14
|
+
DANGEROUS_CALLS = {
|
|
15
|
+
"eval": ("SKY-D201", "HIGH", "Use of eval()"),
|
|
16
|
+
"exec": ("SKY-D202", "HIGH", "Use of exec()"),
|
|
17
|
+
"pickle.load": ("SKY-D203", "CRITICAL", "Untrusted deserialization via pickle.load"),
|
|
18
|
+
"pickle.loads": ("SKY-D204", "CRITICAL", "Untrusted deserialization via pickle.loads"),
|
|
19
|
+
"yaml.load": ("SKY-D205", "HIGH", "yaml.load without SafeLoader"),
|
|
20
|
+
"hashlib.md5": ("SKY-D206", "MEDIUM", "Weak hash (MD5)"),
|
|
21
|
+
"hashlib.sha1": ("SKY-D207", "MEDIUM", "Weak hash (SHA1)"),
|
|
22
|
+
"requests.*": ("SKY-D208", "HIGH", "requests call with verify=False",
|
|
23
|
+
{"kw_equals": {"verify": False}}),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def _matches_rule(name, rule_key):
|
|
27
|
+
if not name:
|
|
28
|
+
return False
|
|
29
|
+
if rule_key.endswith(".*"):
|
|
30
|
+
return name.startswith(rule_key[:-2] + ".")
|
|
31
|
+
return name == rule_key
|
|
32
|
+
|
|
33
|
+
def _kw_equals(node: ast.Call, requirements):
|
|
34
|
+
if not requirements:
|
|
35
|
+
return True
|
|
36
|
+
kw_map = {}
|
|
37
|
+
for kw in (node.keywords or []):
|
|
38
|
+
if kw.arg:
|
|
39
|
+
kw_map[kw.arg] = kw.value
|
|
40
|
+
|
|
41
|
+
for key, expected in requirements.items():
|
|
42
|
+
val = kw_map.get(key)
|
|
43
|
+
if not isinstance(val, ast.Constant):
|
|
44
|
+
return False
|
|
45
|
+
if val.value != expected:
|
|
46
|
+
return False
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
def qualified_name_from_call(node: ast.Call):
|
|
50
|
+
func = node.func
|
|
51
|
+
parts = []
|
|
52
|
+
while isinstance(func, ast.Attribute):
|
|
53
|
+
parts.append(func.attr)
|
|
54
|
+
func = func.value
|
|
55
|
+
if isinstance(func, ast.Name):
|
|
56
|
+
parts.append(func.id)
|
|
57
|
+
parts.reverse()
|
|
58
|
+
return ".".join(parts)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def _yaml_load_without_safeloader(node: ast.Call):
|
|
62
|
+
name = qualified_name_from_call(node)
|
|
63
|
+
if name != "yaml.load":
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
for kw in (node.keywords or []):
|
|
67
|
+
if kw.arg == "Loader":
|
|
68
|
+
text = ast.unparse(kw.value)
|
|
69
|
+
return "SafeLoader" not in text
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
def _add_finding(findings,
|
|
73
|
+
file_path: Path,
|
|
74
|
+
node: ast.AST,
|
|
75
|
+
rule_id,
|
|
76
|
+
severity,
|
|
77
|
+
message):
|
|
78
|
+
findings.append({
|
|
79
|
+
"rule_id": rule_id,
|
|
80
|
+
"severity": severity,
|
|
81
|
+
"message": message,
|
|
82
|
+
"file": str(file_path),
|
|
83
|
+
"line": getattr(node, "lineno", 1),
|
|
84
|
+
"col": getattr(node, "col_offset", 0),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
def _scan_file(file_path: Path, findings):
|
|
88
|
+
src = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
89
|
+
tree = ast.parse(src)
|
|
90
|
+
|
|
91
|
+
scan_sql(tree, file_path, findings)
|
|
92
|
+
scan_cmd(tree, file_path, findings)
|
|
93
|
+
scan_sql_raw(tree, file_path, findings)
|
|
94
|
+
scan_ssrf(tree, file_path, findings)
|
|
95
|
+
scan_path(tree, file_path, findings)
|
|
96
|
+
scan_xss(tree, file_path, findings)
|
|
97
|
+
|
|
98
|
+
for node in ast.walk(tree):
|
|
99
|
+
if not isinstance(node, ast.Call):
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
name = qualified_name_from_call(node)
|
|
103
|
+
if not name:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
for rule_key, tup in DANGEROUS_CALLS.items():
|
|
107
|
+
rule_id = tup[0]
|
|
108
|
+
severity = tup[1]
|
|
109
|
+
message = tup[2]
|
|
110
|
+
|
|
111
|
+
if len(tup) > 3:
|
|
112
|
+
opts = tup[3]
|
|
113
|
+
else:
|
|
114
|
+
opts = None
|
|
115
|
+
|
|
116
|
+
if not _matches_rule(name, rule_key):
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
if rule_key == "yaml.load":
|
|
120
|
+
if not _yaml_load_without_safeloader(node):
|
|
121
|
+
continue
|
|
122
|
+
if opts and "kw_equals" in opts:
|
|
123
|
+
if not _kw_equals(node, opts["kw_equals"]):
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
_add_finding(findings, file_path, node, rule_id, severity, message)
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
def scan_ctx( _, files):
|
|
130
|
+
findings = []
|
|
131
|
+
|
|
132
|
+
for file_path in files:
|
|
133
|
+
if file_path.suffix.lower() not in ALLOWED_SUFFIXES:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
_scan_file(file_path, findings)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(f"Scan failed for {file_path}: {e}", file=sys.stderr)
|
|
140
|
+
|
|
141
|
+
return findings
|
|
File without changes
|