skylos 2.1.2__tar.gz → 2.2.3__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.1.2 → skylos-2.2.3}/PKG-INFO +1 -1
- {skylos-2.1.2 → skylos-2.2.3}/README.md +223 -3
- {skylos-2.1.2 → skylos-2.2.3}/pyproject.toml +1 -1
- {skylos-2.1.2 → skylos-2.2.3}/setup.py +1 -1
- skylos-2.2.3/skylos/__init__.py +10 -0
- {skylos-2.1.2 → skylos-2.2.3}/skylos/analyzer.py +20 -5
- {skylos-2.1.2 → skylos-2.2.3}/skylos/cli.py +94 -19
- skylos-2.2.3/skylos/codemods.py +247 -0
- skylos-2.2.3/skylos/rules/secrets.py +267 -0
- {skylos-2.1.2 → skylos-2.2.3}/skylos/server.py +1 -12
- {skylos-2.1.2 → skylos-2.2.3}/skylos.egg-info/PKG-INFO +1 -1
- {skylos-2.1.2 → skylos-2.2.3}/skylos.egg-info/SOURCES.txt +6 -2
- skylos-2.2.3/test/sample_repo/__init__.py +0 -0
- skylos-2.2.3/test/sample_repo/sample_repo/__init__.py +0 -0
- skylos-2.2.3/test/test_secrets.py +179 -0
- skylos-2.1.2/skylos/__init__.py +0 -8
- skylos-2.1.2/skylos/codemods.py +0 -89
- {skylos-2.1.2 → skylos-2.2.3}/setup.cfg +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/skylos/constants.py +0 -0
- {skylos-2.1.2/test → skylos-2.2.3/skylos/rules}/__init__.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/skylos/visitor.py +0 -0
- {skylos-2.1.2/test/sample_repo → skylos-2.2.3/skylos/visitors}/__init__.py +0 -0
- {skylos-2.1.2/skylos → skylos-2.2.3/skylos/visitors}/framework_aware.py +0 -0
- {skylos-2.1.2/skylos → skylos-2.2.3/skylos/visitors}/test_aware.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/skylos.egg-info/dependency_links.txt +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/skylos.egg-info/entry_points.txt +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/skylos.egg-info/requires.txt +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/skylos.egg-info/top_level.txt +0 -0
- {skylos-2.1.2/test/sample_repo/sample_repo → skylos-2.2.3/test}/__init__.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/compare_tools.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/conftest.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/diagnostics.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/sample_repo/app.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/sample_repo/sample_repo/commands.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/sample_repo/sample_repo/models.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/sample_repo/sample_repo/routes.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/sample_repo/sample_repo/utils.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/test_analyzer.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/test_changes_analyzer.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/test_cli.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/test_codemods.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/test_constants.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/test_framework_aware.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/test_integration.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/test_new_behaviours.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/test_skylos.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/test_test_aware.py +0 -0
- {skylos-2.1.2 → skylos-2.2.3}/test/test_visitor.py +0 -0
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
- [Example Output](#example-output)
|
|
33
33
|
- [Interactive Mode](#interactive-mode)
|
|
34
34
|
- [Development](#development)
|
|
35
|
+
- [CI/CD (Pre-commit & GitHub Actions)](#cicd-pre-commit--github-actions)
|
|
35
36
|
- [FAQ](#faq)
|
|
36
37
|
- [Limitations](#limitations)
|
|
37
38
|
- [Troubleshooting](#troubleshooting)
|
|
@@ -51,6 +52,8 @@
|
|
|
51
52
|
* **Unused Imports**: Identifies imports that are not used
|
|
52
53
|
* **Folder Management**: Inclusion/exclusion of directories
|
|
53
54
|
* **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
|
+
|
|
54
57
|
|
|
55
58
|
## Benchmark (You can find this benchmark test in `test` folder)
|
|
56
59
|
|
|
@@ -94,12 +97,17 @@ pip install .
|
|
|
94
97
|
```bash
|
|
95
98
|
skylos /path/to/your/project
|
|
96
99
|
|
|
100
|
+
skylos /path/to/your/project --secrets ## include api key scan
|
|
101
|
+
|
|
97
102
|
# To launch the front end
|
|
98
103
|
skylos run
|
|
99
104
|
|
|
100
105
|
# Interactive mode - select items to remove
|
|
101
106
|
skylos --interactive /path/to/your/project
|
|
102
107
|
|
|
108
|
+
# Comment out items
|
|
109
|
+
skylos . --interactive --comment-out
|
|
110
|
+
|
|
103
111
|
# Dry run - see what would be removed
|
|
104
112
|
skylos --interactive --dry-run /path/to/your/project
|
|
105
113
|
|
|
@@ -193,7 +201,7 @@ When Skylos detects a test file, it by default, will apply a confidence penalty
|
|
|
193
201
|
|
|
194
202
|
## Ignoring Pragmas
|
|
195
203
|
|
|
196
|
-
To ignore any warning, indicate `# pragma: no skylos` **ON THE SAME LINE** as the function/class you want to ignore
|
|
204
|
+
1. To ignore any warning, indicate `# pragma: no skylos` **ON THE SAME LINE** as the function/class you want to ignore
|
|
197
205
|
|
|
198
206
|
Example
|
|
199
207
|
|
|
@@ -203,6 +211,14 @@ Example
|
|
|
203
211
|
return new_path
|
|
204
212
|
```
|
|
205
213
|
|
|
214
|
+
2. To suppress a **secret** on a line, add: `# skylos: ignore[SKY-S101]`
|
|
215
|
+
|
|
216
|
+
Example
|
|
217
|
+
```python
|
|
218
|
+
|
|
219
|
+
API_KEY = "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # skylos: ignore[SKY-S101]
|
|
220
|
+
```
|
|
221
|
+
|
|
206
222
|
## Including & Excluding Files
|
|
207
223
|
|
|
208
224
|
### Default Exclusions
|
|
@@ -237,6 +253,7 @@ Options:
|
|
|
237
253
|
--json Output raw JSON instead of formatted text
|
|
238
254
|
-o, --output FILE Write output to file instead of stdout
|
|
239
255
|
-v, --verbose Enable verbose output
|
|
256
|
+
--version Checks version
|
|
240
257
|
-i, --interactive Interactively select items to remove
|
|
241
258
|
--dry-run Show what would be removed without modifying files
|
|
242
259
|
--exclude-folder FOLDER Exclude a folder from analysis (can be used multiple times)
|
|
@@ -244,6 +261,7 @@ Options:
|
|
|
244
261
|
--no-default-excludes Don't exclude default folders (__pycache__, .git, venv, etc.)
|
|
245
262
|
--list-default-excludes List the default excluded folders and
|
|
246
263
|
-c, --confidence LEVEL Confidence threshold (0-100). Lower values will show more items.
|
|
264
|
+
-- secrets Scan for api keys/secrets
|
|
247
265
|
```
|
|
248
266
|
|
|
249
267
|
## Interactive Mode
|
|
@@ -254,6 +272,206 @@ The interactive mode lets you select specific functions and imports to remove:
|
|
|
254
272
|
2. **Confirm changes**: Review selected items before applying
|
|
255
273
|
3. **Auto-cleanup**: Files are automatically updated
|
|
256
274
|
|
|
275
|
+
## CI/CD (Pre-commit & GitHub Actions)
|
|
276
|
+
|
|
277
|
+
Pick **one** (or use **both**)
|
|
278
|
+
|
|
279
|
+
1. Pre-commit (local + CI): runs Skylos before commits/PRs.
|
|
280
|
+
- You must install pre-commit locally once. Skylos gets installed automatically by the hook.
|
|
281
|
+
|
|
282
|
+
2. GitHub Actions: runs Skylos on pushes/PRs in CI.
|
|
283
|
+
- No local install needed
|
|
284
|
+
|
|
285
|
+
### Option A — Pre-commit (local + CI)
|
|
286
|
+
|
|
287
|
+
1. Create or edit `.pre-commit-config.yaml` at the repo root:
|
|
288
|
+
|
|
289
|
+
**A: Skylos hook repo**
|
|
290
|
+
```yaml
|
|
291
|
+
## .pre-commit-config.yaml
|
|
292
|
+
repos:
|
|
293
|
+
- repo: https://github.com/duriantaco/skylos
|
|
294
|
+
rev: v2.2.3
|
|
295
|
+
hooks:
|
|
296
|
+
- id: skylos-scan
|
|
297
|
+
name: skylos report
|
|
298
|
+
entry: python -m skylos.cli
|
|
299
|
+
language: python
|
|
300
|
+
types_or: [python]
|
|
301
|
+
pass_filenames: false
|
|
302
|
+
require_serial: true
|
|
303
|
+
args: [".", "--output", "report.json", "--confidence", "70"]
|
|
304
|
+
|
|
305
|
+
- repo: local
|
|
306
|
+
hooks:
|
|
307
|
+
- id: skylos-fail-on-findings
|
|
308
|
+
name: skylos
|
|
309
|
+
env:
|
|
310
|
+
SKYLOS_SOFT: "1"
|
|
311
|
+
language: python
|
|
312
|
+
language_version: python3
|
|
313
|
+
pass_filenames: false
|
|
314
|
+
require_serial: true
|
|
315
|
+
entry: >
|
|
316
|
+
python -c "import os, json, sys, pathlib;
|
|
317
|
+
p=pathlib.Path('report.json');
|
|
318
|
+
|
|
319
|
+
if not p.exists():
|
|
320
|
+
sys.exit(0);
|
|
321
|
+
|
|
322
|
+
data=json.loads(p.read_text(encoding='utf-8'));
|
|
323
|
+
|
|
324
|
+
count = 0
|
|
325
|
+
for v in data.values():
|
|
326
|
+
if isinstance(v, list):
|
|
327
|
+
count += len(v)
|
|
328
|
+
|
|
329
|
+
print(f'[skylos] findings: {count}');
|
|
330
|
+
sys.exit(0 if os.getenv('SKYLOS_SOFT') or count==0 else 1)"
|
|
331
|
+
```
|
|
332
|
+
**B: self-contained local hook**
|
|
333
|
+
|
|
334
|
+
```yaml
|
|
335
|
+
repos:
|
|
336
|
+
- repo: local
|
|
337
|
+
hooks:
|
|
338
|
+
- id: skylos-scan
|
|
339
|
+
name: skylos report
|
|
340
|
+
language: python
|
|
341
|
+
entry: python -m skylos.cli
|
|
342
|
+
pass_filenames: false
|
|
343
|
+
require_serial: true
|
|
344
|
+
additional_dependencies: [skylos==2.2.3]
|
|
345
|
+
args: [".", "--output", "report.json", "--confidence", "70"]
|
|
346
|
+
|
|
347
|
+
- id: skylos-fail-on-findings
|
|
348
|
+
name: skylos (soft)
|
|
349
|
+
language: python
|
|
350
|
+
language_version: python3
|
|
351
|
+
pass_filenames: false
|
|
352
|
+
require_serial: true
|
|
353
|
+
entry: >
|
|
354
|
+
python -c "import os, json, sys, pathlib;
|
|
355
|
+
p=pathlib.Path('report.json');
|
|
356
|
+
|
|
357
|
+
if not p.exists():
|
|
358
|
+
sys.exit(0);
|
|
359
|
+
|
|
360
|
+
data=json.loads(p.read_text(encoding='utf-8'));
|
|
361
|
+
|
|
362
|
+
count = 0
|
|
363
|
+
for v in data.values():
|
|
364
|
+
if isinstance(v, list):
|
|
365
|
+
count += len(v)
|
|
366
|
+
|
|
367
|
+
print(f'[skylos] findings: {count}');
|
|
368
|
+
sys.exit(0 if os.getenv('SKYLOS_SOFT') or count==0 else 1)"
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Install requirements:**
|
|
372
|
+
|
|
373
|
+
You must install pre-commit locally once:
|
|
374
|
+
```bash
|
|
375
|
+
pip install pre-commit
|
|
376
|
+
pre-commit install
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
2. pre-commit run --all-files
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
3. Run the same hooks in CI (GitHub Actions): create .github/workflows/pre-commit.yml:
|
|
383
|
+
|
|
384
|
+
```yaml
|
|
385
|
+
name: pre-commit
|
|
386
|
+
on: [push, pull_request]
|
|
387
|
+
jobs:
|
|
388
|
+
run:
|
|
389
|
+
runs-on: ubuntu-latest
|
|
390
|
+
steps:
|
|
391
|
+
- uses: actions/checkout@v4
|
|
392
|
+
- uses: actions/setup-python@v5
|
|
393
|
+
with: { python-version: "3.11", cache: "pip" }
|
|
394
|
+
- uses: pre-commit/action@v3.0.1
|
|
395
|
+
with: { extra_args: --all-files }
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**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
|
|
399
|
+
|
|
400
|
+
### Option B — Github Actions
|
|
401
|
+
|
|
402
|
+
1. Create .github/workflows/skylos.yml:
|
|
403
|
+
|
|
404
|
+
```yaml
|
|
405
|
+
name: Skylos Deadcode Scan
|
|
406
|
+
|
|
407
|
+
on:
|
|
408
|
+
pull_request:
|
|
409
|
+
push:
|
|
410
|
+
branches: [ main, master ]
|
|
411
|
+
workflow_dispatch:
|
|
412
|
+
|
|
413
|
+
jobs:
|
|
414
|
+
scan:
|
|
415
|
+
runs-on: ubuntu-latest
|
|
416
|
+
env:
|
|
417
|
+
SKYLOS_STRICT: ${{ vars.SKYLOS_STRICT || 'false' }}
|
|
418
|
+
steps:
|
|
419
|
+
- uses: actions/checkout@v4
|
|
420
|
+
|
|
421
|
+
- uses: actions/setup-python@v5
|
|
422
|
+
with:
|
|
423
|
+
python-version: '3.11'
|
|
424
|
+
cache: 'pip'
|
|
425
|
+
|
|
426
|
+
- name: Install Skylos
|
|
427
|
+
run: pip install skylos
|
|
428
|
+
|
|
429
|
+
- name: Run Skylos
|
|
430
|
+
env:
|
|
431
|
+
REPORT: skylos_${{ github.run_number }}_${{ github.sha }}.json
|
|
432
|
+
run: |
|
|
433
|
+
echo "REPORT=$REPORT" >> "$GITHUB_OUTPUT"
|
|
434
|
+
skylos . --json > "$REPORT"
|
|
435
|
+
id: scan
|
|
436
|
+
|
|
437
|
+
- name: Fail if there are findings
|
|
438
|
+
continue-on-error: ${{ env.SKYLOS_STRICT != 'true' }}
|
|
439
|
+
env:
|
|
440
|
+
REPORT: ${{ steps.scan.outputs.REPORT }}
|
|
441
|
+
run: |
|
|
442
|
+
python - << 'PY'
|
|
443
|
+
import json, sys, os
|
|
444
|
+
report = os.environ["REPORT"]
|
|
445
|
+
data = json.load(open(report, "r", encoding="utf-8"))
|
|
446
|
+
count = 0
|
|
447
|
+
for value in data.values():
|
|
448
|
+
if isinstance(value, list):
|
|
449
|
+
count += len(value)
|
|
450
|
+
print(f"Findings: {count}")
|
|
451
|
+
if count > 0:
|
|
452
|
+
print(f"::warning title=Skylos findings::{count} potential issues found. See {report}")
|
|
453
|
+
sys.exit(1 if count > 0 else 0)
|
|
454
|
+
PY
|
|
455
|
+
|
|
456
|
+
- name: Upload report artifact
|
|
457
|
+
if: always()
|
|
458
|
+
uses: actions/upload-artifact@v4
|
|
459
|
+
with:
|
|
460
|
+
name: ${{ steps.scan.outputs.REPORT }}
|
|
461
|
+
path: ${{ steps.scan.outputs.REPORT }}
|
|
462
|
+
|
|
463
|
+
- name: Summarize in job log
|
|
464
|
+
if: always()
|
|
465
|
+
run: |
|
|
466
|
+
echo "Skylos report: ${{ steps.scan.outputs.REPORT }}" >> $GITHUB_STEP_SUMMARY
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**To make the job fail on findings (strict mode)**:
|
|
470
|
+
|
|
471
|
+
1. Go to GitHub -> Settings -> Secrets and variables -> Actions -> Variables
|
|
472
|
+
|
|
473
|
+
2. Add variable SKYLOS_STRICT with value true
|
|
474
|
+
|
|
257
475
|
## Development
|
|
258
476
|
|
|
259
477
|
### Prerequisites
|
|
@@ -307,6 +525,7 @@ A: Start with 60 (default) for safe cleanup. Use 30 for framework applications.
|
|
|
307
525
|
- **Frameworks**: Django models, Flask, FastAPI routes may appear unused but aren't
|
|
308
526
|
- **Test data**: Limited scenarios, your mileage may vary
|
|
309
527
|
- **False positives**: Always manually review before deleting code
|
|
528
|
+
- **Secrets PoC**: May emit both a provider hit and a generic high-entropy hit for the same token. All tokens are detected only in py files (`.py`, `.pyi`, `.pyw`).
|
|
310
529
|
|
|
311
530
|
## Troubleshooting
|
|
312
531
|
|
|
@@ -339,9 +558,10 @@ We welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING
|
|
|
339
558
|
## Roadmap
|
|
340
559
|
- [x] Expand our test cases
|
|
341
560
|
- [ ] Configuration file support
|
|
342
|
-
- [
|
|
343
|
-
- [
|
|
561
|
+
- [x] Git hooks integration
|
|
562
|
+
- [x] CI/CD integration examples
|
|
344
563
|
- [ ] Further optimization
|
|
564
|
+
- [ ] Add new rules
|
|
345
565
|
|
|
346
566
|
## License
|
|
347
567
|
|
|
@@ -7,10 +7,11 @@ from pathlib import Path
|
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
from skylos.visitor import Visitor
|
|
9
9
|
from skylos.constants import ( PENALTIES, AUTO_CALLED )
|
|
10
|
-
from skylos.test_aware import TestAwareVisitor
|
|
10
|
+
from skylos.visitors.test_aware import TestAwareVisitor
|
|
11
|
+
from skylos.rules.secrets import scan_ctx as _secrets_scan_ctx
|
|
11
12
|
import os
|
|
12
13
|
import traceback
|
|
13
|
-
from skylos.framework_aware import FrameworkAwareVisitor, detect_framework_usage
|
|
14
|
+
from skylos.visitors.framework_aware import FrameworkAwareVisitor, detect_framework_usage
|
|
14
15
|
|
|
15
16
|
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s')
|
|
16
17
|
logger=logging.getLogger('Skylos')
|
|
@@ -237,7 +238,7 @@ class Skylos:
|
|
|
237
238
|
if method.simple_name == "format" and cls.endswith("Formatter"):
|
|
238
239
|
method.references += 1
|
|
239
240
|
|
|
240
|
-
def analyze(self, path, thr=60, exclude_folders=None):
|
|
241
|
+
def analyze(self, path, thr=60, exclude_folders= None, enable_secrets = False):
|
|
241
242
|
files, root = self._get_python_files(path, exclude_folders)
|
|
242
243
|
|
|
243
244
|
if not files:
|
|
@@ -260,6 +261,7 @@ class Skylos:
|
|
|
260
261
|
for f in files:
|
|
261
262
|
modmap[f] = self._module(root, f)
|
|
262
263
|
|
|
264
|
+
all_secrets = []
|
|
263
265
|
for file in files:
|
|
264
266
|
mod = modmap[file]
|
|
265
267
|
defs, refs, dyn, exports, test_flags, framework_flags = proc_file(file, mod)
|
|
@@ -272,6 +274,16 @@ class Skylos:
|
|
|
272
274
|
self.dynamic.update(dyn)
|
|
273
275
|
self.exports[mod].update(exports)
|
|
274
276
|
|
|
277
|
+
if enable_secrets and _secrets_scan_ctx is not None:
|
|
278
|
+
try:
|
|
279
|
+
src_lines = Path(file).read_text(encoding="utf-8", errors="ignore").splitlines(True)
|
|
280
|
+
ctx = {"relpath": str(file), "lines": src_lines, "tree": None}
|
|
281
|
+
findings = list(_secrets_scan_ctx(ctx))
|
|
282
|
+
if findings:
|
|
283
|
+
all_secrets.extend(findings)
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
275
287
|
self._mark_refs()
|
|
276
288
|
self._apply_heuristics()
|
|
277
289
|
self._mark_exports()
|
|
@@ -303,6 +315,9 @@ class Skylos:
|
|
|
303
315
|
"excluded_folders": exclude_folders or [],
|
|
304
316
|
}
|
|
305
317
|
}
|
|
318
|
+
|
|
319
|
+
if enable_secrets and all_secrets:
|
|
320
|
+
result["secrets"] = all_secrets
|
|
306
321
|
|
|
307
322
|
for u in unused:
|
|
308
323
|
if u["type"] in ("function", "method"):
|
|
@@ -355,8 +370,8 @@ def proc_file(file_or_args, mod=None):
|
|
|
355
370
|
|
|
356
371
|
return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
|
|
357
372
|
|
|
358
|
-
def analyze(path,conf=60, exclude_folders=None):
|
|
359
|
-
return Skylos().analyze(path,conf, exclude_folders)
|
|
373
|
+
def analyze(path, conf=60, exclude_folders=None, enable_secrets=False):
|
|
374
|
+
return Skylos().analyze(path,conf, exclude_folders, enable_secrets)
|
|
360
375
|
|
|
361
376
|
if __name__ == "__main__":
|
|
362
377
|
if len(sys.argv)>1:
|
|
@@ -8,6 +8,8 @@ from skylos.analyzer import analyze as run_analyze
|
|
|
8
8
|
from skylos.codemods import (
|
|
9
9
|
remove_unused_import_cst,
|
|
10
10
|
remove_unused_function_cst,
|
|
11
|
+
comment_out_unused_import_cst,
|
|
12
|
+
comment_out_unused_function_cst,
|
|
11
13
|
)
|
|
12
14
|
import pathlib
|
|
13
15
|
import skylos
|
|
@@ -80,6 +82,32 @@ def remove_unused_function(file_path, function_name, line_number):
|
|
|
80
82
|
except Exception as e:
|
|
81
83
|
logging.error(f"Failed to remove function {function_name} from {file_path}: {e}")
|
|
82
84
|
return False
|
|
85
|
+
|
|
86
|
+
def comment_out_unused_import(file_path, import_name, line_number, marker="SKYLOS DEADCODE"):
|
|
87
|
+
path = pathlib.Path(file_path)
|
|
88
|
+
try:
|
|
89
|
+
src = path.read_text(encoding="utf-8")
|
|
90
|
+
new_code, changed = comment_out_unused_import_cst(src, import_name, line_number, marker=marker)
|
|
91
|
+
if not changed:
|
|
92
|
+
return False
|
|
93
|
+
path.write_text(new_code, encoding="utf-8")
|
|
94
|
+
return True
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logging.error(f"Failed to comment out import {import_name} from {file_path}: {e}")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def comment_out_unused_function(file_path, function_name, line_number, marker="SKYLOS DEADCODE"):
|
|
100
|
+
path = pathlib.Path(file_path)
|
|
101
|
+
try:
|
|
102
|
+
src = path.read_text(encoding="utf-8")
|
|
103
|
+
new_code, changed = comment_out_unused_function_cst(src, function_name, line_number, marker=marker)
|
|
104
|
+
if not changed:
|
|
105
|
+
return False
|
|
106
|
+
path.write_text(new_code, encoding="utf-8")
|
|
107
|
+
return True
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logging.error(f"Failed to comment out function {function_name} from {file_path}: {e}")
|
|
110
|
+
return False
|
|
83
111
|
|
|
84
112
|
def interactive_selection(logger, unused_functions, unused_imports):
|
|
85
113
|
if not INTERACTIVE_AVAILABLE:
|
|
@@ -110,7 +138,7 @@ def interactive_selection(logger, unused_functions, unused_imports):
|
|
|
110
138
|
selected_functions = answers['functions']
|
|
111
139
|
|
|
112
140
|
if unused_imports:
|
|
113
|
-
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to
|
|
141
|
+
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to act on (hit spacebar to select):{Colors.RESET}")
|
|
114
142
|
|
|
115
143
|
import_choices = []
|
|
116
144
|
|
|
@@ -172,6 +200,13 @@ def main():
|
|
|
172
200
|
action="store_true",
|
|
173
201
|
help="Output raw JSON",
|
|
174
202
|
)
|
|
203
|
+
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
"--comment-out",
|
|
206
|
+
action="store_true",
|
|
207
|
+
help="Comment out selected dead code instead of deleting it",
|
|
208
|
+
)
|
|
209
|
+
|
|
175
210
|
parser.add_argument(
|
|
176
211
|
"--output",
|
|
177
212
|
"-o",
|
|
@@ -232,12 +267,15 @@ def main():
|
|
|
232
267
|
help="List the default excluded folders and exit."
|
|
233
268
|
)
|
|
234
269
|
|
|
270
|
+
parser.add_argument("--secrets", action="store_true",
|
|
271
|
+
help="Scan for API keys. Off by default.")
|
|
272
|
+
|
|
235
273
|
args = parser.parse_args()
|
|
236
274
|
|
|
237
275
|
if args.list_default_excludes:
|
|
238
276
|
print("Default excluded folders:")
|
|
239
277
|
for folder in sorted(DEFAULT_EXCLUDE_FOLDERS):
|
|
240
|
-
print(f"
|
|
278
|
+
print(f" {folder}")
|
|
241
279
|
print(f"\nTotal: {len(DEFAULT_EXCLUDE_FOLDERS)} folders")
|
|
242
280
|
print("\nUse --no-default-excludes to disable these exclusions")
|
|
243
281
|
print("Use --include-folder <folder> to force include specific folders")
|
|
@@ -266,7 +304,7 @@ def main():
|
|
|
266
304
|
logger.info(f"{Colors.GREEN}📁 No folders excluded{Colors.RESET}")
|
|
267
305
|
|
|
268
306
|
try:
|
|
269
|
-
result_json = run_analyze(args.path, conf=args.confidence, exclude_folders=list(final_exclude_folders))
|
|
307
|
+
result_json = run_analyze(args.path, conf=args.confidence, enable_secrets=bool(args.secrets), exclude_folders=list(final_exclude_folders))
|
|
270
308
|
result = json.loads(result_json)
|
|
271
309
|
|
|
272
310
|
except Exception as e:
|
|
@@ -282,6 +320,7 @@ def main():
|
|
|
282
320
|
unused_parameters = result.get("unused_parameters", [])
|
|
283
321
|
unused_variables = result.get("unused_variables", [])
|
|
284
322
|
unused_classes = result.get("unused_classes", [])
|
|
323
|
+
secrets_findings = result.get("secrets", [])
|
|
285
324
|
|
|
286
325
|
logger.info(f"{Colors.CYAN}{Colors.BOLD} Python Static Analysis Results{Colors.RESET}")
|
|
287
326
|
logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
|
|
@@ -292,49 +331,75 @@ def main():
|
|
|
292
331
|
logger.info(f" * Unused parameters: {Colors.YELLOW}{len(unused_parameters)}{Colors.RESET}")
|
|
293
332
|
logger.info(f" * Unused variables: {Colors.YELLOW}{len(unused_variables)}{Colors.RESET}")
|
|
294
333
|
logger.info(f" * Unused classes: {Colors.YELLOW}{len(unused_classes)}{Colors.RESET}")
|
|
334
|
+
if secrets_findings:
|
|
335
|
+
logger.info(f" * Secrets: {Colors.RED}{len(secrets_findings)}{Colors.RESET}")
|
|
295
336
|
|
|
296
337
|
if args.interactive and (unused_functions or unused_imports):
|
|
297
338
|
logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
|
|
298
339
|
selected_functions, selected_imports = interactive_selection(logger, unused_functions, unused_imports)
|
|
299
340
|
|
|
300
341
|
if selected_functions or selected_imports:
|
|
301
|
-
logger.info(f"\n{Colors.BOLD}Selected items to
|
|
342
|
+
logger.info(f"\n{Colors.BOLD}Selected items to process:{Colors.RESET}")
|
|
302
343
|
|
|
303
344
|
if selected_functions:
|
|
304
|
-
logger.info(f"
|
|
345
|
+
logger.info(f" Functions: {len(selected_functions)}")
|
|
305
346
|
for func in selected_functions:
|
|
306
|
-
logger.info(f"
|
|
347
|
+
logger.info(f" - {func['name']} ({func['file']}: {func['line']})")
|
|
307
348
|
|
|
308
349
|
if selected_imports:
|
|
309
|
-
logger.info(f"
|
|
350
|
+
logger.info(f" Imports: {len(selected_imports)}")
|
|
310
351
|
for imp in selected_imports:
|
|
311
|
-
logger.info(f"
|
|
352
|
+
logger.info(f" - {imp['name']} ({imp['file']}: {imp['line']})")
|
|
312
353
|
|
|
313
354
|
if not args.dry_run:
|
|
355
|
+
if args.comment_out:
|
|
356
|
+
confirm_verb = "comment out"
|
|
357
|
+
else:
|
|
358
|
+
confirm_verb = "remove"
|
|
359
|
+
|
|
314
360
|
questions = [
|
|
315
361
|
inquirer.Confirm('confirm',
|
|
316
|
-
message="Are you sure you want to
|
|
362
|
+
message="Are you sure you want to process these items?",
|
|
317
363
|
default=False)
|
|
318
364
|
]
|
|
319
365
|
answers = inquirer.prompt(questions)
|
|
320
366
|
|
|
321
367
|
if answers and answers['confirm']:
|
|
322
|
-
|
|
323
|
-
|
|
368
|
+
action = "Commenting out" if args.comment_out else "Removing"
|
|
369
|
+
logger.info(f"\n{Colors.YELLOW}{action} selected items...{Colors.RESET}")
|
|
370
|
+
|
|
371
|
+
action_func = comment_out_unused_function if args.comment_out else remove_unused_function
|
|
372
|
+
if args.comment_out:
|
|
373
|
+
action_past = "Commented out"
|
|
374
|
+
action_verb = "comment out"
|
|
375
|
+
else:
|
|
376
|
+
action_past = "Removed"
|
|
377
|
+
action_verb = "remove"
|
|
378
|
+
|
|
324
379
|
for func in selected_functions:
|
|
325
|
-
success =
|
|
380
|
+
success = action_func(func['file'], func['name'], func['line'])
|
|
381
|
+
|
|
326
382
|
if success:
|
|
327
|
-
logger.info(f" {Colors.GREEN} {Colors.RESET}
|
|
383
|
+
logger.info(f" {Colors.GREEN} ✓ {Colors.RESET} {action_past} function: {func['name']}")
|
|
328
384
|
else:
|
|
329
|
-
logger.error(f" {Colors.RED} x {Colors.RESET} Failed to
|
|
330
|
-
|
|
385
|
+
logger.error(f" {Colors.RED} x {Colors.RESET} Failed to {action_verb}: {func['name']}")
|
|
386
|
+
|
|
387
|
+
import_func = comment_out_unused_import if args.comment_out else remove_unused_import
|
|
388
|
+
if args.comment_out:
|
|
389
|
+
action_past = "Commented out"
|
|
390
|
+
action_verb = "comment out"
|
|
391
|
+
else:
|
|
392
|
+
action_past = "Removed"
|
|
393
|
+
action_verb = "remove"
|
|
394
|
+
|
|
331
395
|
for imp in selected_imports:
|
|
332
|
-
success =
|
|
396
|
+
success = import_func(imp['file'], imp['name'], imp['line'])
|
|
397
|
+
|
|
333
398
|
if success:
|
|
334
|
-
logger.info(f" {Colors.GREEN} {Colors.RESET}
|
|
399
|
+
logger.info(f" {Colors.GREEN} ✓ {Colors.RESET} {action_past} import: {imp['name']}")
|
|
335
400
|
else:
|
|
336
|
-
logger.error(f" {Colors.RED} x {Colors.RESET} Failed to
|
|
337
|
-
|
|
401
|
+
logger.error(f" {Colors.RED} x {Colors.RESET} Failed to {action_verb}: {imp['name']}")
|
|
402
|
+
|
|
338
403
|
logger.info(f"\n{Colors.GREEN}Cleanup complete!{Colors.RESET}")
|
|
339
404
|
else:
|
|
340
405
|
logger.info(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}")
|
|
@@ -389,6 +454,16 @@ def main():
|
|
|
389
454
|
else:
|
|
390
455
|
logger.info(f"\n{Colors.GREEN}✓ All classes are being used!{Colors.RESET}")
|
|
391
456
|
|
|
457
|
+
if secrets_findings:
|
|
458
|
+
logger.info(f"\n{Colors.RED}{Colors.BOLD} - Secrets{Colors.RESET}")
|
|
459
|
+
logger.info(f"{Colors.RED}{'=' * 9}{Colors.RESET}")
|
|
460
|
+
for i, s in enumerate(secrets_findings[:20], 1):
|
|
461
|
+
provider = s.get("provider", "generic")
|
|
462
|
+
where = f"{s.get('file','?')}:{s.get('line','?')}"
|
|
463
|
+
prev = s.get("preview", "****")
|
|
464
|
+
msg = s.get("message", "Secret detected")
|
|
465
|
+
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{msg} [{provider}] {Colors.GRAY}({where}){Colors.RESET} -> {prev}")
|
|
466
|
+
|
|
392
467
|
dead_code_count = len(unused_functions) + len(unused_imports) + len(unused_variables) + len(unused_classes) + len(unused_parameters)
|
|
393
468
|
|
|
394
469
|
print_badge(dead_code_count, logger)
|