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.
- {skylos-2.2.4 → skylos-2.4.0}/PKG-INFO +1 -1
- {skylos-2.2.4 → skylos-2.4.0}/README.md +68 -5
- {skylos-2.2.4 → skylos-2.4.0}/pyproject.toml +1 -1
- {skylos-2.2.4 → skylos-2.4.0}/setup.py +1 -1
- {skylos-2.2.4 → skylos-2.4.0}/skylos/__init__.py +1 -1
- {skylos-2.2.4 → skylos-2.4.0}/skylos/analyzer.py +107 -76
- {skylos-2.2.4 → skylos-2.4.0}/skylos/cli.py +156 -8
- skylos-2.4.0/skylos/rules/danger/danger.py +141 -0
- skylos-2.4.0/skylos/rules/danger/danger_cmd/cmd_flow.py +208 -0
- skylos-2.4.0/skylos/rules/danger/danger_fs/path_flow.py +188 -0
- skylos-2.4.0/skylos/rules/danger/danger_net/ssrf_flow.py +198 -0
- skylos-2.4.0/skylos/rules/danger/danger_sql/__init__.py +0 -0
- skylos-2.4.0/skylos/rules/danger/danger_sql/sql_flow.py +175 -0
- skylos-2.4.0/skylos/rules/danger/danger_sql/sql_raw_flow.py +202 -0
- skylos-2.4.0/skylos/rules/danger/danger_web/__init__.py +0 -0
- skylos-2.4.0/skylos/rules/danger/danger_web/xss_flow.py +279 -0
- skylos-2.4.0/skylos/visitors/__init__.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos.egg-info/PKG-INFO +1 -1
- {skylos-2.2.4 → skylos-2.4.0}/skylos.egg-info/SOURCES.txt +17 -1
- skylos-2.4.0/test/__init__.py +0 -0
- skylos-2.4.0/test/sample_repo/__init__.py +0 -0
- skylos-2.4.0/test/sample_repo/sample_repo/__init__.py +0 -0
- skylos-2.4.0/test/test_cmd_injection.py +41 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_dangerous.py +32 -1
- skylos-2.4.0/test/test_path_traversal.py +40 -0
- skylos-2.4.0/test/test_sql_injection.py +54 -0
- skylos-2.4.0/test/test_ssrf.py +51 -0
- skylos-2.2.4/skylos/rules/dangerous.py +0 -135
- {skylos-2.2.4 → skylos-2.4.0}/setup.cfg +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos/codemods.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos/constants.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos/rules/__init__.py +0 -0
- {skylos-2.2.4/skylos/visitors → skylos-2.4.0/skylos/rules/danger}/__init__.py +0 -0
- {skylos-2.2.4/test → skylos-2.4.0/skylos/rules/danger/danger_cmd}/__init__.py +0 -0
- {skylos-2.2.4/test/sample_repo → skylos-2.4.0/skylos/rules/danger/danger_fs}/__init__.py +0 -0
- {skylos-2.2.4/test/sample_repo/sample_repo → skylos-2.4.0/skylos/rules/danger/danger_net}/__init__.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos/rules/secrets.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos/server.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos/visitor.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos/visitors/framework_aware.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos/visitors/test_aware.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos.egg-info/dependency_links.txt +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos.egg-info/entry_points.txt +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos.egg-info/requires.txt +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/skylos.egg-info/top_level.txt +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/compare_tools.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/conftest.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/diagnostics.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/sample_repo/app.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/sample_repo/sample_repo/commands.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/sample_repo/sample_repo/models.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/sample_repo/sample_repo/routes.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/sample_repo/sample_repo/utils.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_analyzer.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_changes_analyzer.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_cli.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_codemods.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_constants.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_framework_aware.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_integration.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_new_behaviours.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_secrets.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_skylos.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_test_aware.py +0 -0
- {skylos-2.2.4 → skylos-2.4.0}/test/test_visitor.py +0 -0
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|

|
|
5
|
+

|
|
5
6
|

|
|
7
|
+

|
|
6
8
|

|
|
9
|
+

|
|
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
|
-
- [
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -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")
|
|
@@ -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}")
|