skylos 2.2.4__tar.gz → 2.4.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.

Potentially problematic release.


This version of skylos might be problematic. Click here for more details.

Files changed (65) hide show
  1. {skylos-2.2.4 → skylos-2.4.0}/PKG-INFO +1 -1
  2. {skylos-2.2.4 → skylos-2.4.0}/README.md +68 -5
  3. {skylos-2.2.4 → skylos-2.4.0}/pyproject.toml +1 -1
  4. {skylos-2.2.4 → skylos-2.4.0}/setup.py +1 -1
  5. {skylos-2.2.4 → skylos-2.4.0}/skylos/__init__.py +1 -1
  6. {skylos-2.2.4 → skylos-2.4.0}/skylos/analyzer.py +107 -76
  7. {skylos-2.2.4 → skylos-2.4.0}/skylos/cli.py +156 -8
  8. skylos-2.4.0/skylos/rules/danger/danger.py +141 -0
  9. skylos-2.4.0/skylos/rules/danger/danger_cmd/cmd_flow.py +208 -0
  10. skylos-2.4.0/skylos/rules/danger/danger_fs/path_flow.py +188 -0
  11. skylos-2.4.0/skylos/rules/danger/danger_net/ssrf_flow.py +198 -0
  12. skylos-2.4.0/skylos/rules/danger/danger_sql/__init__.py +0 -0
  13. skylos-2.4.0/skylos/rules/danger/danger_sql/sql_flow.py +175 -0
  14. skylos-2.4.0/skylos/rules/danger/danger_sql/sql_raw_flow.py +202 -0
  15. skylos-2.4.0/skylos/rules/danger/danger_web/__init__.py +0 -0
  16. skylos-2.4.0/skylos/rules/danger/danger_web/xss_flow.py +279 -0
  17. skylos-2.4.0/skylos/visitors/__init__.py +0 -0
  18. {skylos-2.2.4 → skylos-2.4.0}/skylos.egg-info/PKG-INFO +1 -1
  19. {skylos-2.2.4 → skylos-2.4.0}/skylos.egg-info/SOURCES.txt +17 -1
  20. skylos-2.4.0/test/__init__.py +0 -0
  21. skylos-2.4.0/test/sample_repo/__init__.py +0 -0
  22. skylos-2.4.0/test/sample_repo/sample_repo/__init__.py +0 -0
  23. skylos-2.4.0/test/test_cmd_injection.py +41 -0
  24. {skylos-2.2.4 → skylos-2.4.0}/test/test_dangerous.py +32 -1
  25. skylos-2.4.0/test/test_path_traversal.py +40 -0
  26. skylos-2.4.0/test/test_sql_injection.py +54 -0
  27. skylos-2.4.0/test/test_ssrf.py +51 -0
  28. skylos-2.2.4/skylos/rules/dangerous.py +0 -135
  29. {skylos-2.2.4 → skylos-2.4.0}/setup.cfg +0 -0
  30. {skylos-2.2.4 → skylos-2.4.0}/skylos/codemods.py +0 -0
  31. {skylos-2.2.4 → skylos-2.4.0}/skylos/constants.py +0 -0
  32. {skylos-2.2.4 → skylos-2.4.0}/skylos/rules/__init__.py +0 -0
  33. {skylos-2.2.4/skylos/visitors → skylos-2.4.0/skylos/rules/danger}/__init__.py +0 -0
  34. {skylos-2.2.4/test → skylos-2.4.0/skylos/rules/danger/danger_cmd}/__init__.py +0 -0
  35. {skylos-2.2.4/test/sample_repo → skylos-2.4.0/skylos/rules/danger/danger_fs}/__init__.py +0 -0
  36. {skylos-2.2.4/test/sample_repo/sample_repo → skylos-2.4.0/skylos/rules/danger/danger_net}/__init__.py +0 -0
  37. {skylos-2.2.4 → skylos-2.4.0}/skylos/rules/secrets.py +0 -0
  38. {skylos-2.2.4 → skylos-2.4.0}/skylos/server.py +0 -0
  39. {skylos-2.2.4 → skylos-2.4.0}/skylos/visitor.py +0 -0
  40. {skylos-2.2.4 → skylos-2.4.0}/skylos/visitors/framework_aware.py +0 -0
  41. {skylos-2.2.4 → skylos-2.4.0}/skylos/visitors/test_aware.py +0 -0
  42. {skylos-2.2.4 → skylos-2.4.0}/skylos.egg-info/dependency_links.txt +0 -0
  43. {skylos-2.2.4 → skylos-2.4.0}/skylos.egg-info/entry_points.txt +0 -0
  44. {skylos-2.2.4 → skylos-2.4.0}/skylos.egg-info/requires.txt +0 -0
  45. {skylos-2.2.4 → skylos-2.4.0}/skylos.egg-info/top_level.txt +0 -0
  46. {skylos-2.2.4 → skylos-2.4.0}/test/compare_tools.py +0 -0
  47. {skylos-2.2.4 → skylos-2.4.0}/test/conftest.py +0 -0
  48. {skylos-2.2.4 → skylos-2.4.0}/test/diagnostics.py +0 -0
  49. {skylos-2.2.4 → skylos-2.4.0}/test/sample_repo/app.py +0 -0
  50. {skylos-2.2.4 → skylos-2.4.0}/test/sample_repo/sample_repo/commands.py +0 -0
  51. {skylos-2.2.4 → skylos-2.4.0}/test/sample_repo/sample_repo/models.py +0 -0
  52. {skylos-2.2.4 → skylos-2.4.0}/test/sample_repo/sample_repo/routes.py +0 -0
  53. {skylos-2.2.4 → skylos-2.4.0}/test/sample_repo/sample_repo/utils.py +0 -0
  54. {skylos-2.2.4 → skylos-2.4.0}/test/test_analyzer.py +0 -0
  55. {skylos-2.2.4 → skylos-2.4.0}/test/test_changes_analyzer.py +0 -0
  56. {skylos-2.2.4 → skylos-2.4.0}/test/test_cli.py +0 -0
  57. {skylos-2.2.4 → skylos-2.4.0}/test/test_codemods.py +0 -0
  58. {skylos-2.2.4 → skylos-2.4.0}/test/test_constants.py +0 -0
  59. {skylos-2.2.4 → skylos-2.4.0}/test/test_framework_aware.py +0 -0
  60. {skylos-2.2.4 → skylos-2.4.0}/test/test_integration.py +0 -0
  61. {skylos-2.2.4 → skylos-2.4.0}/test/test_new_behaviours.py +0 -0
  62. {skylos-2.2.4 → skylos-2.4.0}/test/test_secrets.py +0 -0
  63. {skylos-2.2.4 → skylos-2.4.0}/test/test_skylos.py +0 -0
  64. {skylos-2.2.4 → skylos-2.4.0}/test/test_test_aware.py +0 -0
  65. {skylos-2.2.4 → skylos-2.4.0}/test/test_visitor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skylos
3
- Version: 2.2.4
3
+ Version: 2.4.0
4
4
  Summary: A static analysis tool for Python codebases
5
5
  Author-email: oha <aaronoh2015@gmail.com>
6
6
  Requires-Python: >=3.9
@@ -2,8 +2,11 @@
2
2
 
3
3
  ![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)
4
4
  ![100% Local](https://img.shields.io/badge/privacy-100%25%20local-brightgreen)
5
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/skylos)
5
6
  ![PyPI version](https://img.shields.io/pypi/v/skylos)
7
+ ![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/oha.skylos-vscode-extension)
6
8
  ![Security Policy](https://img.shields.io/badge/security-policy-brightgreen)
9
+ ![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)
7
10
 
8
11
  <div align="center">
9
12
  <img src="assets/SKYLOS.png" alt="Skylos Logo" width="200">
@@ -25,7 +28,7 @@
25
28
  - [Web Interface](#web-interface)
26
29
  - [Design](#design)
27
30
  - [Test File Detection](#test-file-detection)
28
- - [Folder Management](#folder-management)
31
+ - [Vibe Coding](#vibe-coding)
29
32
  - [Ignoring Pragmas](#ignoring-pragmas)
30
33
  - [Including & Excluding Files](#including--excluding-files)
31
34
  - [CLI Options](#cli-options)
@@ -33,6 +36,7 @@
33
36
  - [Interactive Mode](#interactive-mode)
34
37
  - [Development](#development)
35
38
  - [CI/CD (Pre-commit & GitHub Actions)](#cicd-pre-commit--github-actions)
39
+ - [VS Code extension](#vsc-extension)
36
40
  - [FAQ](#faq)
37
41
  - [Limitations](#limitations)
38
42
  - [Troubleshooting](#troubleshooting)
@@ -52,8 +56,8 @@
52
56
  * **Unused Imports**: Identifies imports that are not used
53
57
  * **Folder Management**: Inclusion/exclusion of directories
54
58
  * **Ignore Pragmas**: Skip lines tagged with `# pragma: no skylos`, `# pragma: no cover`, or `# noqa`
55
- **NEW** **Secrets Scanning (PoC, opt-in)**: Detects API keys & secrets (GitHub, GitLab, Slack, Stripe, AWS, Google, SendGrid, Twilio, private key blocks)
56
- **NEW** **Dangerous Patterns**: Flags risky code such as `eval/exec`, `os.system`, `subprocess(shell=True)`, `pickle.load/loads`, `yaml.load` without SafeLoader, hashlib.md5/sha1. Refer to `DANGEROUS_CODE.md` for the whole list.
59
+ * **NEW** **Secrets Scanning (PoC, opt-in)**: Detects API keys & secrets (GitHub, GitLab, Slack, Stripe, AWS, Google, SendGrid, Twilio, private key blocks)
60
+ * **NEW** **Dangerous Patterns**: Flags risky code such as `eval/exec`, `os.system`, `subprocess(shell=True)`, `pickle.load/loads`, `yaml.load` without SafeLoader, hashlib.md5/sha1. Refer to `DANGEROUS_CODE.md` for the whole list. This includes SQL injection, path traversal and any other security flaws that may arise from the practise of vibe-coding.
57
61
 
58
62
  ## Benchmark (You can find this benchmark test in `test` folder)
59
63
 
@@ -112,8 +116,12 @@ skylos . --interactive --comment-out
112
116
  # Dry run - see what would be removed
113
117
  skylos --interactive --dry-run /path/to/your/project
114
118
 
119
+ # Load the results in json format
115
120
  skylos --json /path/to/your/project
116
121
 
122
+ # Load the results in table format
123
+ skylos --table /path/to/your/project
124
+
117
125
  # With confidence
118
126
  skylos path/to/your/file --confidence 20 ## or whatever value u wanna set
119
127
  ```
@@ -200,6 +208,49 @@ When Skylos detects a test file, it by default, will apply a confidence penalty
200
208
  /project/test_data.py
201
209
  ```
202
210
 
211
+ ## Vibe-Coding
212
+
213
+ We are aware that vibe coding has created a lot of vulnerabilities. To an AI, it's job is to spit out a list of tokens with the highest probability, whether it's right or not. This may introduce vulnerabilities in their applications. We have expanded Skylos to catch the most important problems first.
214
+
215
+ - SQL injection (cursor)
216
+ - SQL injection (raw-API)
217
+ - Command injection
218
+ - SSRF
219
+ - Path traversal
220
+ - eval/exec
221
+ - pickle.load/loads
222
+ - yaml.load w/o SafeLoader
223
+ - weak hashes MD5/SHA1
224
+ - subprocess(..., shell=True)
225
+ - requests(..., verify=False)
226
+
227
+ ### Quick Start
228
+
229
+ ```bash
230
+ skylos /path/to/your/project --danger
231
+ ```
232
+
233
+ ### Examples that will be flagged
234
+
235
+ ```
236
+ # SQLi (cursor)
237
+ cur.execute(f"SELECT * FROM users WHERE name='{name}'")
238
+
239
+ # SQLi (raw-api)
240
+ pd.read_sql("SELECT * FROM users WHERE name='" + q + "'", conn)
241
+
242
+ # Command injection
243
+ os.system("zip -r out.zip " + folder)
244
+
245
+ # SSRF
246
+ requests.get(request.args["url"], timeout=3)
247
+
248
+ # Path traversal
249
+ with open(request.args.get("p"), "r") as f: ...
250
+ ```
251
+
252
+ This list will be expanded in the near future. For more information, refer to `DANGEROUS_CODE.md`
253
+
203
254
  ## Ignoring Pragmas
204
255
 
205
256
  1. To ignore any warning, indicate `# pragma: no skylos` **ON THE SAME LINE** as the function/class you want to ignore
@@ -252,6 +303,7 @@ Arguments:
252
303
  Options:
253
304
  -h, --help Show this help message and exit
254
305
  --json Output raw JSON instead of formatted text
306
+ --table Output results in table format via the CLI
255
307
  -o, --output FILE Write output to file instead of stdout
256
308
  -v, --verbose Enable verbose output
257
309
  --version Checks version
@@ -368,7 +420,7 @@ jobs:
368
420
  ## .pre-commit-config.yaml
369
421
  repos:
370
422
  - repo: https://github.com/duriantaco/skylos
371
- rev: v2.2.4
423
+ rev: v2.4.0
372
424
  hooks:
373
425
  - id: skylos-scan
374
426
  name: skylos report
@@ -418,7 +470,7 @@ repos:
418
470
  entry: python -m skylos.cli
419
471
  pass_filenames: false
420
472
  require_serial: true
421
- additional_dependencies: [skylos==2.2.4]
473
+ additional_dependencies: [skylos==2.4.0]
422
474
  args: [".", "--output", "report.json", "--confidence", "70"]
423
475
 
424
476
  - id: skylos-fail-on-findings
@@ -474,6 +526,16 @@ jobs:
474
526
 
475
527
  **Pre commit behavior:** the second hook is soft by default (SKYLOS_SOFT=1). This means that it prints findings and passes. You can remove the env/logic if you want pre-commit to block commits on finding
476
528
 
529
+ ## VS Code extension
530
+
531
+ Skylos has a VS Code extension that runs on save like a linter. Runs automatically on save of Python files. You will ll see squiggles + a popup like "Skylos found N items." For more information, refer to the VSC `README.md` inside the marketplace.
532
+
533
+ ### Install
534
+
535
+ From VS Code Marketplace: "Skylos" (publisher: oha)
536
+
537
+ Version: `0.1.0`
538
+
477
539
  ## Development
478
540
 
479
541
  ### Prerequisites
@@ -568,6 +630,7 @@ We welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING
568
630
  - [ ] Further optimization
569
631
  - [ ] Add new rules
570
632
  - [ ] Expanding on the `dangerous.py` list
633
+ - [ ] Porting to uv
571
634
 
572
635
  ## License
573
636
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skylos"
7
- version = "2.2.4"
7
+ version = "2.4.0"
8
8
  requires-python = ">=3.9"
9
9
  description = "A static analysis tool for Python codebases"
10
10
  authors = [{name = "oha", email = "aaronoh2015@gmail.com"}]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="skylos",
5
- version="2.2.4",
5
+ version="2.4.0",
6
6
  packages=find_packages(),
7
7
  python_requires=">=3.9",
8
8
  install_requires=[
@@ -1,4 +1,4 @@
1
- __version__ = "2.2.4"
1
+ __version__ = "2.4.0"
2
2
 
3
3
  def analyze(*args, **kwargs):
4
4
  from .analyzer import analyze as _analyze
@@ -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.dangerous import scan_ctx as scan_dangerous
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, enable_dangerous = 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 enable_dangerous and scan_dangerous is not None:
291
+ if enable_danger and scan_danger is not None:
292
292
  try:
293
- findings = scan_dangerous(root, [file])
293
+ findings = scan_danger(root, [file])
294
294
  if findings:
295
295
  all_dangers.extend(findings)
296
- except Exception:
297
- pass
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 enable_dangerous and all_dangers:
335
- result["dangerous"] = all_dangers
336
- result["analysis_summary"]["dangerous_count"] = len(all_dangers)
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, enable_dangerous=False):
390
- return Skylos().analyze(path,conf, exclude_folders, enable_secrets, enable_dangerous)
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
- if len(sys.argv)>1:
394
- p = sys.argv[1]
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
- result = analyze(p,confidence)
402
-
403
- data = json.loads(result)
404
- print("\n Python Static Analysis Results")
405
- print("===================================\n")
406
-
407
- total_items = 0
408
- for key, items in data.items():
409
- if key.startswith("unused_") and isinstance(items, list):
410
- total_items += len(items)
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"![Dead Code: {total_items}](https://img.shields.io/badge/Dead_Code-{total_items}_detected-orange?logo=codacy&logoColor=red)")
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
- print("\nNext steps:")
457
- print(" * Use --interactive to select specific items to remove")
458
- print(" * Use --dry-run to preview changes before applying them")
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("Usage: python Skylos.py <path> [confidence_threshold]")
484
+ print(f"Found {total_dead} dead code items. Add this badge to your README:")
485
+ print("```markdown")
486
+ print(f"![Dead Code: {total_dead}](https://img.shields.io/badge/Dead_Code-{total_dead}_detected-orange?logo=codacy&logoColor=red)")
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")
@@ -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: int, logger):
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(f" Your code is 100% dead code free! Add this badge to your README:")
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("![Dead Code Free](https://img.shields.io/badge/Dead_Code-Free-brightgreen?logo=moleculer&logoColor=white)")
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
- logger.info("```markdown")
173
- logger.info(f"![Dead Code: {dead_code_count}](https://img.shields.io/badge/Dead_Code-{dead_code_count}_detected-orange?logo=codacy&logoColor=red)")
174
- logger.info("```")
176
+
177
+ logger.info("```markdown")
178
+ logger.info(f"![Dead Code: {dead_code_count}](https://img.shields.io/badge/Dead_Code-{dead_code_count}_detected-orange?logo=codacy&logoColor=red)")
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), exclude_folders=list(final_exclude_folders))
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
- print_badge(dead_code_count, logger)
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}")