skylos 2.1.2__py3-none-any.whl → 2.2.2__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 +5 -3
- skylos/analyzer.py +20 -5
- skylos/cli.py +94 -19
- skylos/codemods.py +154 -5
- skylos/rules/__init__.py +0 -0
- skylos/rules/secrets.py +268 -0
- skylos/server.py +1 -12
- skylos/visitors/__init__.py +0 -0
- {skylos-2.1.2.dist-info → skylos-2.2.2.dist-info}/METADATA +1 -1
- {skylos-2.1.2.dist-info → skylos-2.2.2.dist-info}/RECORD +16 -12
- test/test_secrets.py +179 -0
- /skylos/{framework_aware.py → visitors/framework_aware.py} +0 -0
- /skylos/{test_aware.py → visitors/test_aware.py} +0 -0
- {skylos-2.1.2.dist-info → skylos-2.2.2.dist-info}/WHEEL +0 -0
- {skylos-2.1.2.dist-info → skylos-2.2.2.dist-info}/entry_points.txt +0 -0
- {skylos-2.1.2.dist-info → skylos-2.2.2.dist-info}/top_level.txt +0 -0
skylos/__init__.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
__version__ = "2.2.2"
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
def analyze(*args, **kwargs):
|
|
4
|
+
from .analyzer import analyze as _analyze
|
|
5
|
+
return _analyze(*args, **kwargs)
|
|
4
6
|
|
|
5
7
|
def debug_test():
|
|
6
8
|
return "debug-ok"
|
|
7
9
|
|
|
8
|
-
__all__ = ["analyze", "debug_test", "__version__"]
|
|
10
|
+
__all__ = ["analyze", "debug_test", "__version__"]
|
skylos/analyzer.py
CHANGED
|
@@ -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:
|
skylos/cli.py
CHANGED
|
@@ -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)
|
skylos/codemods.py
CHANGED
|
@@ -2,6 +2,155 @@ from __future__ import annotations
|
|
|
2
2
|
import libcst as cst
|
|
3
3
|
from libcst.metadata import PositionProvider
|
|
4
4
|
|
|
5
|
+
class _CommentOutBlock(cst.CSTTransformer):
|
|
6
|
+
|
|
7
|
+
METADATA_DEPENDENCIES = (PositionProvider,)
|
|
8
|
+
|
|
9
|
+
def __init__(self, module_code, marker = "SKYLOS DEADCODE"):
|
|
10
|
+
self.module_code = module_code.splitlines(True)
|
|
11
|
+
self.marker = marker
|
|
12
|
+
|
|
13
|
+
def _comment_block(self, start_line, end_line):
|
|
14
|
+
lines = self.module_code[start_line - 1:end_line]
|
|
15
|
+
out = []
|
|
16
|
+
out.append(cst.EmptyLine(comment=cst.Comment(f"# {self.marker} START (lines {start_line}-{end_line})")))
|
|
17
|
+
for raw in lines:
|
|
18
|
+
out.append(cst.EmptyLine(comment=cst.Comment("# " + raw.rstrip("\n"))))
|
|
19
|
+
out.append(cst.EmptyLine(comment=cst.Comment(f"# {self.marker} END")))
|
|
20
|
+
return out
|
|
21
|
+
|
|
22
|
+
class _CommentOutFunctionAtLine(_CommentOutBlock):
|
|
23
|
+
def __init__(self, func_name, target_line, module_code, marker):
|
|
24
|
+
super().__init__(module_code, marker)
|
|
25
|
+
self.func_name = func_name
|
|
26
|
+
self.target_line = target_line
|
|
27
|
+
self.changed = False
|
|
28
|
+
|
|
29
|
+
def _is_target(self, node: cst.CSTNode):
|
|
30
|
+
pos = self.get_metadata(PositionProvider, node, None)
|
|
31
|
+
return pos and pos.start.line == self.target_line
|
|
32
|
+
|
|
33
|
+
def leave_FunctionDef(self, orig: cst.FunctionDef, updated: cst.FunctionDef):
|
|
34
|
+
if self._is_target(orig) and (orig.name.value == self.func_name):
|
|
35
|
+
self.changed = True
|
|
36
|
+
pos = self.get_metadata(PositionProvider, orig)
|
|
37
|
+
return cst.FlattenSentinel(self._comment_block(pos.start.line, pos.end.line))
|
|
38
|
+
return updated
|
|
39
|
+
|
|
40
|
+
def leave_AsyncFunctionDef(self, orig: cst.AsyncFunctionDef, updated: cst.AsyncFunctionDef):
|
|
41
|
+
if self._is_target(orig) and (orig.name.value == self.func_name):
|
|
42
|
+
self.changed = True
|
|
43
|
+
pos = self.get_metadata(PositionProvider, orig)
|
|
44
|
+
return cst.FlattenSentinel(self._comment_block(pos.start.line, pos.end.line))
|
|
45
|
+
return updated
|
|
46
|
+
|
|
47
|
+
class _CommentOutImportAtLine(_CommentOutBlock):
|
|
48
|
+
|
|
49
|
+
def __init__(self, target_name, target_line, module_code, marker):
|
|
50
|
+
super().__init__(module_code, marker)
|
|
51
|
+
self.target_name = target_name
|
|
52
|
+
self.target_line = target_line
|
|
53
|
+
self.changed = False
|
|
54
|
+
|
|
55
|
+
def _is_target_line(self, node: cst.CSTNode):
|
|
56
|
+
pos = self.get_metadata(PositionProvider, node, None)
|
|
57
|
+
return bool(pos and (pos.start.line <= self.target_line <= pos.end.line))
|
|
58
|
+
|
|
59
|
+
def _render_single_alias_text(self, head, alias: cst.ImportAlias, is_from):
|
|
60
|
+
if is_from:
|
|
61
|
+
alias_txt = alias.name.code
|
|
62
|
+
if alias.asname:
|
|
63
|
+
alias_txt += f" as {alias.asname.name.value}"
|
|
64
|
+
return f"from {head} import {alias_txt}"
|
|
65
|
+
else:
|
|
66
|
+
alias_txt = alias.name.code
|
|
67
|
+
if alias.asname:
|
|
68
|
+
alias_txt += f" as {alias.asname.name.value}"
|
|
69
|
+
return f"import {alias_txt}"
|
|
70
|
+
|
|
71
|
+
def _split_aliases(self, aliases, head, is_from):
|
|
72
|
+
kept = []
|
|
73
|
+
removed_for_comment= []
|
|
74
|
+
for alias in list(aliases):
|
|
75
|
+
bound = _bound_name_for_import_alias(alias)
|
|
76
|
+
if bound == self.target_name:
|
|
77
|
+
self.changed = True
|
|
78
|
+
removed_for_comment.append(self._render_single_alias_text(head, alias, is_from))
|
|
79
|
+
else:
|
|
80
|
+
kept.append(alias)
|
|
81
|
+
return kept, removed_for_comment
|
|
82
|
+
|
|
83
|
+
def leave_Import(self, orig: cst.Import, updated: cst.Import):
|
|
84
|
+
if not self._is_target_line(orig):
|
|
85
|
+
return updated
|
|
86
|
+
|
|
87
|
+
head = ""
|
|
88
|
+
kept, removed = self._split_aliases(updated.names, head, is_from=False)
|
|
89
|
+
|
|
90
|
+
if not removed:
|
|
91
|
+
return updated
|
|
92
|
+
|
|
93
|
+
pos = self.get_metadata(PositionProvider, orig)
|
|
94
|
+
if not kept:
|
|
95
|
+
return cst.FlattenSentinel(self._comment_block(pos.start.line, pos.end.line))
|
|
96
|
+
|
|
97
|
+
commented = []
|
|
98
|
+
for txt in removed:
|
|
99
|
+
comment = cst.Comment(f"# {self.marker}: {txt}")
|
|
100
|
+
commented.append(cst.EmptyLine(comment=comment))
|
|
101
|
+
|
|
102
|
+
kept_import = updated.with_changes(names=tuple(kept))
|
|
103
|
+
all_nodes = [kept_import] + commented
|
|
104
|
+
return cst.FlattenSentinel(all_nodes)
|
|
105
|
+
|
|
106
|
+
def leave_ImportFrom(self, orig: cst.ImportFrom, updated: cst.ImportFrom):
|
|
107
|
+
if not self._is_target_line(orig) or isinstance(updated.names, cst.ImportStar):
|
|
108
|
+
return updated
|
|
109
|
+
|
|
110
|
+
if updated.relative:
|
|
111
|
+
dots = "." * len(updated.relative)
|
|
112
|
+
else:
|
|
113
|
+
dots = ""
|
|
114
|
+
|
|
115
|
+
if updated.module is not None:
|
|
116
|
+
modname = updated.module.code
|
|
117
|
+
else:
|
|
118
|
+
modname = ""
|
|
119
|
+
|
|
120
|
+
mod = f"{dots}{modname}"
|
|
121
|
+
|
|
122
|
+
kept, removed = self._split_aliases(list(updated.names), mod, is_from=True)
|
|
123
|
+
|
|
124
|
+
if not removed:
|
|
125
|
+
return updated
|
|
126
|
+
pos = self.get_metadata(PositionProvider, orig)
|
|
127
|
+
|
|
128
|
+
if not kept:
|
|
129
|
+
comment_block = self._comment_block(pos.start.line, pos.end.line)
|
|
130
|
+
return cst.FlattenSentinel(comment_block)
|
|
131
|
+
|
|
132
|
+
commented = []
|
|
133
|
+
for txt in removed:
|
|
134
|
+
comment = cst.Comment(f"# {self.marker}: {txt}")
|
|
135
|
+
commented.append(cst.EmptyLine(comment=comment))
|
|
136
|
+
|
|
137
|
+
updated_import = updated.with_changes(names=tuple(kept))
|
|
138
|
+
all_nodes = [updated_import] + commented
|
|
139
|
+
|
|
140
|
+
return cst.FlattenSentinel(all_nodes)
|
|
141
|
+
|
|
142
|
+
def comment_out_unused_function_cst(code, func_name, line_number, marker = "SKYLOS DEADCODE"):
|
|
143
|
+
wrapper = cst.MetadataWrapper(cst.parse_module(code))
|
|
144
|
+
tx = _CommentOutFunctionAtLine(func_name, line_number, code, marker)
|
|
145
|
+
new_mod = wrapper.visit(tx)
|
|
146
|
+
return new_mod.code, tx.changed
|
|
147
|
+
|
|
148
|
+
def comment_out_unused_import_cst(code, import_name, line_number, marker = "SKYLOS DEADCODE"):
|
|
149
|
+
wrapper = cst.MetadataWrapper(cst.parse_module(code))
|
|
150
|
+
tx = _CommentOutImportAtLine(import_name, line_number, code, marker)
|
|
151
|
+
new_mod = wrapper.visit(tx)
|
|
152
|
+
return new_mod.code, tx.changed
|
|
153
|
+
|
|
5
154
|
def _bound_name_for_import_alias(alias: cst.ImportAlias):
|
|
6
155
|
if alias.asname:
|
|
7
156
|
return alias.asname.name.value
|
|
@@ -20,8 +169,8 @@ class _RemoveImportAtLine(cst.CSTTransformer):
|
|
|
20
169
|
|
|
21
170
|
def _is_target_line(self, node: cst.CSTNode):
|
|
22
171
|
pos = self.get_metadata(PositionProvider, node, None)
|
|
23
|
-
return bool(pos and pos.start.line
|
|
24
|
-
|
|
172
|
+
return bool(pos and (pos.start.line <= self.target_line <= pos.end.line))
|
|
173
|
+
|
|
25
174
|
def _filter_aliases(self, aliases):
|
|
26
175
|
kept = []
|
|
27
176
|
for alias in aliases:
|
|
@@ -61,16 +210,16 @@ class _RemoveFunctionAtLine(cst.CSTTransformer):
|
|
|
61
210
|
|
|
62
211
|
def _is_target(self, node: cst.CSTNode):
|
|
63
212
|
pos = self.get_metadata(PositionProvider, node, None)
|
|
64
|
-
return
|
|
213
|
+
return pos and pos.start.line == self.target_line
|
|
65
214
|
|
|
66
215
|
def leave_FunctionDef(self, orig: cst.FunctionDef, updated: cst.FunctionDef):
|
|
67
|
-
if self._is_target(orig) and (orig.name.value
|
|
216
|
+
if self._is_target(orig) and (orig.name.value == self.func_name):
|
|
68
217
|
self.changed = True
|
|
69
218
|
return cst.RemoveFromParent()
|
|
70
219
|
return updated
|
|
71
220
|
|
|
72
221
|
def leave_AsyncFunctionDef(self, orig: cst.AsyncFunctionDef, updated: cst.AsyncFunctionDef):
|
|
73
|
-
if self._is_target(orig) and (orig.name.value
|
|
222
|
+
if self._is_target(orig) and (orig.name.value == self.func_name):
|
|
74
223
|
self.changed = True
|
|
75
224
|
return cst.RemoveFromParent()
|
|
76
225
|
|
skylos/rules/__init__.py
ADDED
|
File without changes
|
skylos/rules/secrets.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import re, ast
|
|
3
|
+
from math import log2
|
|
4
|
+
from typing import Dict, Any, Iterable, List, Optional
|
|
5
|
+
|
|
6
|
+
__all__ = ["scan_ctx"]
|
|
7
|
+
|
|
8
|
+
ALLOWED_FILE_SUFFIXES = (".py", ".pyi", ".pyw")
|
|
9
|
+
|
|
10
|
+
PROVIDER_PATTERNS = [
|
|
11
|
+
("github", re.compile(r"(ghp|gho|ghu|ghs|ghr|gpat)_[A-Za-z0-9]{36,}")),
|
|
12
|
+
("gitlab", re.compile(r"glpat-[A-Za-z0-9_-]{20,}")),
|
|
13
|
+
("slack", re.compile(r"xox[abprs]-[A-Za-z0-9-]{10,48}")),
|
|
14
|
+
("stripe", re.compile(r"sk_(live|test)_[A-Za-z0-9]{16,}")),
|
|
15
|
+
("aws_access_key_id", re.compile(r"\b(AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[0-9A-Z]{16}\b")),
|
|
16
|
+
("google_api_key", re.compile(r"\bAIza[0-9A-Za-z\-_]{35}\b")),
|
|
17
|
+
("sendgrid", re.compile(r"\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b")),
|
|
18
|
+
("twilio", re.compile(r"\bSK[0-9a-fA-F]{32}\b")),
|
|
19
|
+
("private_key_block", re.compile(r"-----BEGIN (?:RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY-----")),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
GENERIC_VALUE = re.compile(r"""(?ix)
|
|
23
|
+
(?:
|
|
24
|
+
(token|api[_-]?key|secret|password|passwd|pwd|bearer|auth[_-]?token|access[_-]?token)
|
|
25
|
+
\s*[:=]\s*(?P<q>['"])(?P<val>[^'"]{16,})(?P=q)
|
|
26
|
+
)|(?P<bare>[A-Za-z0-9_\-]{24,})
|
|
27
|
+
""")
|
|
28
|
+
|
|
29
|
+
SAFE_TEST_HINTS = {
|
|
30
|
+
"example", "sample", "fake", "placeholder", "dummy", "test_", "_test", "test_test_",
|
|
31
|
+
"changeme", "password", "secret", "not_a_real", "do_not_use",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
IGNORE_DIRECTIVE = "skylos: ignore[SKY-S101]"
|
|
35
|
+
DEFAULT_MIN_ENTROPY = 3.6
|
|
36
|
+
|
|
37
|
+
def _entropy(s):
|
|
38
|
+
if len(s) == 0:
|
|
39
|
+
return 0.0
|
|
40
|
+
|
|
41
|
+
char_counts = {}
|
|
42
|
+
for character in s:
|
|
43
|
+
if character in char_counts:
|
|
44
|
+
char_counts[character] += 1
|
|
45
|
+
else:
|
|
46
|
+
char_counts[character] = 1
|
|
47
|
+
|
|
48
|
+
total_chars = len(s)
|
|
49
|
+
entropy = 0.0
|
|
50
|
+
|
|
51
|
+
for count in char_counts.values():
|
|
52
|
+
probability = count / total_chars
|
|
53
|
+
entropy -= probability * log2(probability)
|
|
54
|
+
|
|
55
|
+
return entropy
|
|
56
|
+
|
|
57
|
+
def _mask(tok):
|
|
58
|
+
token_length = len(tok)
|
|
59
|
+
|
|
60
|
+
if token_length <= 8:
|
|
61
|
+
return "*" * token_length
|
|
62
|
+
|
|
63
|
+
else:
|
|
64
|
+
first_part = tok[:4]
|
|
65
|
+
last_part = tok[-4:]
|
|
66
|
+
return first_part + "…" + last_part
|
|
67
|
+
|
|
68
|
+
def _docstring_lines(tree):
|
|
69
|
+
if tree is None:
|
|
70
|
+
return set()
|
|
71
|
+
|
|
72
|
+
docstring_line_numbers = set()
|
|
73
|
+
|
|
74
|
+
def find_docstring_lines(node):
|
|
75
|
+
if not hasattr(node, "body") or not node.body:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
first_statement = node.body[0]
|
|
79
|
+
|
|
80
|
+
is_expression = isinstance(first_statement, ast.Expr)
|
|
81
|
+
if not is_expression:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
value = getattr(first_statement, "value", None)
|
|
85
|
+
if not isinstance(value, ast.Constant):
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if not isinstance(value.value, str):
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
start_line = getattr(first_statement, "lineno", None)
|
|
92
|
+
end_line = getattr(first_statement, "end_lineno", start_line)
|
|
93
|
+
|
|
94
|
+
if start_line is not None:
|
|
95
|
+
if end_line is None:
|
|
96
|
+
end_line = start_line
|
|
97
|
+
|
|
98
|
+
for line_num in range(start_line, end_line + 1):
|
|
99
|
+
docstring_line_numbers.add(line_num)
|
|
100
|
+
|
|
101
|
+
if isinstance(tree, ast.Module):
|
|
102
|
+
find_docstring_lines(tree)
|
|
103
|
+
|
|
104
|
+
for node in ast.walk(tree):
|
|
105
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
106
|
+
find_docstring_lines(node)
|
|
107
|
+
|
|
108
|
+
return docstring_line_numbers
|
|
109
|
+
|
|
110
|
+
def scan_ctx(ctx, *, min_entropy= DEFAULT_MIN_ENTROPY, scan_comments= True,
|
|
111
|
+
scan_docstrings= True, allowlist_patterns= None, ignore_path_substrings= None):
|
|
112
|
+
|
|
113
|
+
rel_path = ctx.get("relpath", "")
|
|
114
|
+
if not rel_path.endswith(ALLOWED_FILE_SUFFIXES):
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
if ignore_path_substrings:
|
|
118
|
+
for substring in ignore_path_substrings:
|
|
119
|
+
if substring and substring in rel_path:
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
file_lines = ctx.get("lines") or []
|
|
123
|
+
syntax_tree = ctx.get("tree")
|
|
124
|
+
|
|
125
|
+
allowlist_regexes = []
|
|
126
|
+
if allowlist_patterns:
|
|
127
|
+
for pattern in allowlist_patterns:
|
|
128
|
+
compiled_regex = re.compile(pattern)
|
|
129
|
+
allowlist_regexes.append(compiled_regex)
|
|
130
|
+
|
|
131
|
+
if scan_docstrings:
|
|
132
|
+
docstring_lines = set()
|
|
133
|
+
else:
|
|
134
|
+
docstring_lines = _docstring_lines(syntax_tree)
|
|
135
|
+
|
|
136
|
+
findings = []
|
|
137
|
+
|
|
138
|
+
for line_number, raw_line in enumerate(file_lines, start=1):
|
|
139
|
+
line_content = raw_line.rstrip("\n")
|
|
140
|
+
|
|
141
|
+
if IGNORE_DIRECTIVE in line_content:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
stripped_line = line_content.lstrip()
|
|
145
|
+
if not scan_comments and stripped_line.startswith("#"):
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
if not scan_docstrings and line_number in docstring_lines:
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
should_skip_line = False
|
|
152
|
+
for regex_pattern in allowlist_regexes:
|
|
153
|
+
if regex_pattern.search(line_content):
|
|
154
|
+
should_skip_line = True
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
if should_skip_line:
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
for provider_name, pattern_regex in PROVIDER_PATTERNS:
|
|
161
|
+
pattern_matches = pattern_regex.finditer(line_content)
|
|
162
|
+
|
|
163
|
+
for regex_match in pattern_matches:
|
|
164
|
+
potential_secret = regex_match.group(0)
|
|
165
|
+
|
|
166
|
+
token_lowercase = potential_secret.lower()
|
|
167
|
+
has_safe_hint = False
|
|
168
|
+
|
|
169
|
+
for safe_hint in SAFE_TEST_HINTS:
|
|
170
|
+
if safe_hint in token_lowercase:
|
|
171
|
+
has_safe_hint = True
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
if has_safe_hint:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
col_pos = line_content.find(potential_secret)
|
|
178
|
+
|
|
179
|
+
finding = {
|
|
180
|
+
"rule_id": "SKY-S101",
|
|
181
|
+
"severity": "CRITICAL",
|
|
182
|
+
"provider": provider_name,
|
|
183
|
+
"message": f"Potential {provider_name} secret detected",
|
|
184
|
+
"file": rel_path,
|
|
185
|
+
"line": line_number,
|
|
186
|
+
"col": max(0, col_pos),
|
|
187
|
+
"end_col": max(1, col_pos + len(potential_secret)),
|
|
188
|
+
"preview": _mask(potential_secret),
|
|
189
|
+
}
|
|
190
|
+
findings.append(finding)
|
|
191
|
+
|
|
192
|
+
aws_key_indicators = ["AWS_SECRET_ACCESS_KEY", "aws_secret_access_key"]
|
|
193
|
+
line_has_aws_key = False
|
|
194
|
+
|
|
195
|
+
for indicator in aws_key_indicators:
|
|
196
|
+
if indicator in line_content or indicator in line_content.lower():
|
|
197
|
+
line_has_aws_key = True
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
if line_has_aws_key:
|
|
201
|
+
aws_secret_pattern = r"['\"]?([A-Za-z0-9/+=]{40})['\"]?"
|
|
202
|
+
aws_match = re.search(aws_secret_pattern, line_content)
|
|
203
|
+
|
|
204
|
+
if aws_match:
|
|
205
|
+
aws_token = aws_match.group(1)
|
|
206
|
+
tok_entropy = _entropy(aws_token)
|
|
207
|
+
|
|
208
|
+
if tok_entropy >= min_entropy:
|
|
209
|
+
col_pos = line_content.find(aws_token)
|
|
210
|
+
|
|
211
|
+
aws_finding = {
|
|
212
|
+
"rule_id": "SKY-S101",
|
|
213
|
+
"severity": "CRITICAL",
|
|
214
|
+
"provider": "aws_secret_access_key",
|
|
215
|
+
"message": "Potential AWS secret access key detected",
|
|
216
|
+
"file": rel_path,
|
|
217
|
+
"line": line_number,
|
|
218
|
+
"col": max(0, col_pos),
|
|
219
|
+
"end_col": max(1, col_pos + len(aws_token)),
|
|
220
|
+
"preview": _mask(aws_token),
|
|
221
|
+
"entropy": round(tok_entropy, 2),
|
|
222
|
+
}
|
|
223
|
+
findings.append(aws_finding)
|
|
224
|
+
|
|
225
|
+
generic_match = GENERIC_VALUE.search(line_content)
|
|
226
|
+
if generic_match:
|
|
227
|
+
val_group = generic_match.group("val")
|
|
228
|
+
bare_group = generic_match.group("bare")
|
|
229
|
+
|
|
230
|
+
if val_group:
|
|
231
|
+
extracted_token = val_group
|
|
232
|
+
elif bare_group:
|
|
233
|
+
extracted_token = bare_group
|
|
234
|
+
else:
|
|
235
|
+
extracted_token = ""
|
|
236
|
+
|
|
237
|
+
clean_token = extracted_token.strip()
|
|
238
|
+
|
|
239
|
+
if clean_token:
|
|
240
|
+
token_lowercase = clean_token.lower()
|
|
241
|
+
has_safe_hint = False
|
|
242
|
+
|
|
243
|
+
for safe_hint in SAFE_TEST_HINTS:
|
|
244
|
+
if safe_hint in token_lowercase:
|
|
245
|
+
has_safe_hint = True
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
if not has_safe_hint:
|
|
249
|
+
tok_entropy = _entropy(clean_token)
|
|
250
|
+
|
|
251
|
+
if tok_entropy >= min_entropy and len(clean_token) >= 20:
|
|
252
|
+
col_pos = line_content.find(clean_token)
|
|
253
|
+
|
|
254
|
+
generic_finding = {
|
|
255
|
+
"rule_id": "SKY-S101",
|
|
256
|
+
"severity": "CRITICAL",
|
|
257
|
+
"provider": "generic",
|
|
258
|
+
"message": f"High-entropy value detected (entropy={tok_entropy:.2f})",
|
|
259
|
+
"file": rel_path,
|
|
260
|
+
"line": line_number,
|
|
261
|
+
"col": max(0, col_pos),
|
|
262
|
+
"end_col": max(1, col_pos + len(clean_token)),
|
|
263
|
+
"preview": _mask(clean_token),
|
|
264
|
+
"entropy": round(tok_entropy, 2),
|
|
265
|
+
}
|
|
266
|
+
findings.append(generic_finding)
|
|
267
|
+
|
|
268
|
+
return findings
|
skylos/server.py
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Skylos Web Server
|
|
4
|
-
Serves the frontend and provides API to analyze projects using skylos
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
1
|
from flask import Flask, request, jsonify
|
|
8
2
|
from flask_cors import CORS
|
|
9
3
|
import skylos
|
|
@@ -17,7 +11,6 @@ CORS(app)
|
|
|
17
11
|
|
|
18
12
|
@app.route('/')
|
|
19
13
|
def serve_frontend():
|
|
20
|
-
"""Serve the frontend HTML"""
|
|
21
14
|
return """<!DOCTYPE html>
|
|
22
15
|
<html lang="en">
|
|
23
16
|
<head>
|
|
@@ -461,7 +454,6 @@ def serve_frontend():
|
|
|
461
454
|
classes: analysisData.unused_classes || []
|
|
462
455
|
};
|
|
463
456
|
|
|
464
|
-
// Filter by confidence threshold
|
|
465
457
|
Object.keys(data).forEach(key => {
|
|
466
458
|
data[key] = data[key].filter(item => item.confidence >= confidenceThreshold);
|
|
467
459
|
});
|
|
@@ -481,12 +473,11 @@ def serve_frontend():
|
|
|
481
473
|
const listElement = document.getElementById('deadCodeList');
|
|
482
474
|
const allItems = [];
|
|
483
475
|
|
|
484
|
-
// Combine all items with their categories
|
|
485
476
|
Object.keys(data).forEach(category => {
|
|
486
477
|
data[category].forEach(item => {
|
|
487
478
|
allItems.push({
|
|
488
479
|
...item,
|
|
489
|
-
category: category.slice(0, -1)
|
|
480
|
+
category: category.slice(0, -1)
|
|
490
481
|
});
|
|
491
482
|
});
|
|
492
483
|
});
|
|
@@ -500,7 +491,6 @@ def serve_frontend():
|
|
|
500
491
|
return;
|
|
501
492
|
}
|
|
502
493
|
|
|
503
|
-
// Sort by confidence (highest first)
|
|
504
494
|
allItems.sort((a, b) => b.confidence - a.confidence);
|
|
505
495
|
|
|
506
496
|
listElement.innerHTML = allItems.map(item => `
|
|
@@ -551,7 +541,6 @@ def start_server():
|
|
|
551
541
|
print(" Starting Skylos Web Interface...")
|
|
552
542
|
print("Opening browser at: http://localhost:5090")
|
|
553
543
|
|
|
554
|
-
# Open browser after a short delay
|
|
555
544
|
Timer(1.5, open_browser).start()
|
|
556
545
|
|
|
557
546
|
app.run(debug=False, host='0.0.0.0', port=5090, use_reloader=False)
|
|
File without changes
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
skylos/__init__.py,sha256=
|
|
2
|
-
skylos/analyzer.py,sha256=
|
|
3
|
-
skylos/cli.py,sha256=
|
|
4
|
-
skylos/codemods.py,sha256=
|
|
1
|
+
skylos/__init__.py,sha256=C4oz_FAjQTq_0XiSSxpDnngdL9YlWLpuam7NNvB_vPs,229
|
|
2
|
+
skylos/analyzer.py,sha256=G_8pw7GmChATc5h5XXij2pcHirhh_5G9Y8dlAC1dx38,16735
|
|
3
|
+
skylos/cli.py,sha256=DOV6nwPbi5zh-OJ3wXjIaPxCfXRtiPMNqo9Zp2nvDBA,19475
|
|
4
|
+
skylos/codemods.py,sha256=QdOwtbE2PpLsgCqpeScFg-pCfcpfHw9hFu-0WKKKBQg,9084
|
|
5
5
|
skylos/constants.py,sha256=kU-2FKQAV-ju4rYw62Tw25uRvqauzjZFUqtvGWaI6Es,1571
|
|
6
|
-
skylos/
|
|
7
|
-
skylos/server.py,sha256=5Rlgy3LdE8I5TWRJJh0n19JqhVYaAOc9fUtRjL-PpX8,16270
|
|
8
|
-
skylos/test_aware.py,sha256=kxYoMG2m02kbMlxtFOM-MWJO8qqxHviP9HgAMbKRfvU,2304
|
|
6
|
+
skylos/server.py,sha256=oHuevjdDFvJVbvpTtCDjSLOJ6Zy1jL4BYLYV4VFNMXs,15903
|
|
9
7
|
skylos/visitor.py,sha256=o_2JxjXKXAcWLQr8nmoatGAz2EhBk225qE_piHf3hDg,20458
|
|
8
|
+
skylos/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
skylos/rules/secrets.py,sha256=FD7cXZ4_Zfg_Si1qFXLVK-5OIX1HXtCr_yJM0OlRsbI,9425
|
|
10
|
+
skylos/visitors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
skylos/visitors/framework_aware.py,sha256=eX4oU0jQwDWejkkW4kjRNctre27sVLHK1CTDDiqPqRw,13054
|
|
12
|
+
skylos/visitors/test_aware.py,sha256=kxYoMG2m02kbMlxtFOM-MWJO8qqxHviP9HgAMbKRfvU,2304
|
|
10
13
|
test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
14
|
test/compare_tools.py,sha256=0g9PDeJlbst-7hOaQzrL4MiJFQKpqM8q8VeBGzpPczg,22738
|
|
12
15
|
test/conftest.py,sha256=57sTF6vLL5U0CVKwGQFJcRs6n7t1dEHIriQoSluNmAI,6418
|
|
@@ -19,6 +22,7 @@ test/test_constants.py,sha256=pMuDy0UpC81zENMDCeK6Bqmm3BR_HHZQSlMG-9TgOm0,12602
|
|
|
19
22
|
test/test_framework_aware.py,sha256=G9va1dEQ31wsvd4X7ROf_1YhhAQG5CogB7v0hYCojQ8,8802
|
|
20
23
|
test/test_integration.py,sha256=bNKGUe-w0xEZEdnoQNHbssvKMGs9u9fmFQTOz1lX9_k,12398
|
|
21
24
|
test/test_new_behaviours.py,sha256=vAON8rR09RXawZT1knYtZHseyRcECKz5bdOspJoMjUA,1472
|
|
25
|
+
test/test_secrets.py,sha256=rjC-iQD9s8U296PyHP0vMEjC6CC9mJwKiFj7Sm-xsuo,5711
|
|
22
26
|
test/test_skylos.py,sha256=kz77STrS4k3Eez5RDYwGxOg2WH3e7zNZPUYEaTLbGTs,15608
|
|
23
27
|
test/test_test_aware.py,sha256=VmbR_MQY0m941CAxxux8OxJHIr7l8crfWRouSeBMhIo,9390
|
|
24
28
|
test/test_visitor.py,sha256=xAbGv-XaozKm_0WJJhr0hMb6mLaJcbPz57G9-SWkxFU,22764
|
|
@@ -29,8 +33,8 @@ test/sample_repo/sample_repo/commands.py,sha256=b6gQ9YDabt2yyfqGbOpLo0osF7wya8O4
|
|
|
29
33
|
test/sample_repo/sample_repo/models.py,sha256=xXIg3pToEZwKuUCmKX2vTlCF_VeFA0yZlvlBVPIy5Qw,3320
|
|
30
34
|
test/sample_repo/sample_repo/routes.py,sha256=8yITrt55BwS01G7nWdESdx8LuxmReqop1zrGUKPeLi8,2475
|
|
31
35
|
test/sample_repo/sample_repo/utils.py,sha256=S56hEYh8wkzwsD260MvQcmUFOkw2EjFU27nMLFE6G2k,1103
|
|
32
|
-
skylos-2.
|
|
33
|
-
skylos-2.
|
|
34
|
-
skylos-2.
|
|
35
|
-
skylos-2.
|
|
36
|
-
skylos-2.
|
|
36
|
+
skylos-2.2.2.dist-info/METADATA,sha256=LfA3i21DFqm9HGGJLXEkSwkK1Adv-58LPnIAmwguvwQ,314
|
|
37
|
+
skylos-2.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
38
|
+
skylos-2.2.2.dist-info/entry_points.txt,sha256=zzRpN2ByznlQoLeuLolS_TFNYSQxUGBL1EXQsAd6bIA,43
|
|
39
|
+
skylos-2.2.2.dist-info/top_level.txt,sha256=f8GA_7KwfaEopPMP8-EXDQXaqd4IbsOQPakZy01LkdQ,12
|
|
40
|
+
skylos-2.2.2.dist-info/RECORD,,
|
test/test_secrets.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import pytest
|
|
3
|
+
from skylos.rules.secrets import scan_ctx
|
|
4
|
+
|
|
5
|
+
ELLIPSIS = "…"
|
|
6
|
+
|
|
7
|
+
def _ctx_from_source(src, rel="app.py", with_ast=False):
|
|
8
|
+
if with_ast:
|
|
9
|
+
tree = ast.parse(src)
|
|
10
|
+
else:
|
|
11
|
+
tree = None
|
|
12
|
+
|
|
13
|
+
lines = src.splitlines(True)
|
|
14
|
+
|
|
15
|
+
context = {
|
|
16
|
+
"relpath": rel,
|
|
17
|
+
"lines": lines,
|
|
18
|
+
"tree": tree
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return context
|
|
22
|
+
|
|
23
|
+
def test_github_and_generic_both_fire_on_token_assignment():
|
|
24
|
+
src = 'GITHUB_TOKEN = "ghp_1234567890abcdef1234567890abcdef1234"\n'
|
|
25
|
+
|
|
26
|
+
findings = list(scan_ctx(_ctx_from_source(src)))
|
|
27
|
+
|
|
28
|
+
providers = set()
|
|
29
|
+
for finding in findings:
|
|
30
|
+
provider_name = finding["provider"]
|
|
31
|
+
providers.add(provider_name)
|
|
32
|
+
|
|
33
|
+
assert "github" in providers
|
|
34
|
+
assert "generic" in providers
|
|
35
|
+
|
|
36
|
+
github_previews = []
|
|
37
|
+
for finding in findings:
|
|
38
|
+
if finding["provider"] == "github":
|
|
39
|
+
preview = finding["preview"]
|
|
40
|
+
github_previews.append(preview)
|
|
41
|
+
|
|
42
|
+
assert len(github_previews) > 0
|
|
43
|
+
first_preview = github_previews[0]
|
|
44
|
+
assert first_preview.startswith("ghp_")
|
|
45
|
+
assert ELLIPSIS in first_preview
|
|
46
|
+
|
|
47
|
+
@pytest.mark.parametrize(
|
|
48
|
+
"line,provider",
|
|
49
|
+
[
|
|
50
|
+
('GITLAB_PAT = "glpat-A1b2C3d4E5f6G7h8I9j0"\n', "gitlab"),
|
|
51
|
+
('SLACK_BOT = "xoxb-1234567890ABCDEF12"\n', "slack"),
|
|
52
|
+
('STRIPE = "sk_live_a1B2c3D4e5F6g7H8"\n', "stripe"),
|
|
53
|
+
('GOOGLE = "AIzaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"\n', "google_api_key"),
|
|
54
|
+
('SENDGRID = "SG.AAAAABBBBBCCCCCC.DDDDDEEEEEFFFFFFF"\n', "sendgrid"),
|
|
55
|
+
('TWILIO = "SK0123456789abcdef0123456789abcdef"\n', "twilio"),
|
|
56
|
+
('PK = "-----BEGIN RSA PRIVATE KEY-----"\n', "private_key_block"),
|
|
57
|
+
('AWS_ACCESS_KEY_ID = "AKIAABCDEFGHIJKLMNOP"\n', "aws_access_key_id"),
|
|
58
|
+
],
|
|
59
|
+
)
|
|
60
|
+
def test_provider_patterns(line, provider):
|
|
61
|
+
findings = list(scan_ctx(_ctx_from_source(line)))
|
|
62
|
+
assert any(f["provider"] == provider for f in findings)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_aws_secret_access_key_special_case():
|
|
66
|
+
src = (
|
|
67
|
+
'AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"\n'
|
|
68
|
+
)
|
|
69
|
+
findings = list(scan_ctx(_ctx_from_source(src)))
|
|
70
|
+
hit = None
|
|
71
|
+
for finding in findings:
|
|
72
|
+
if finding["provider"] == "aws_secret_access_key":
|
|
73
|
+
hit = finding
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
assert hit is not None
|
|
77
|
+
assert "entropy" in hit and isinstance(hit["entropy"], float)
|
|
78
|
+
assert ELLIPSIS in hit["preview"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_generic_entropy_detection_and_threshold():
|
|
82
|
+
src = 'X = "o2uV7Ew1kZ9Q3nR8sT5yU6pX4cJ2mL7a"\n'
|
|
83
|
+
findings = list(scan_ctx(_ctx_from_source(src)))
|
|
84
|
+
assert any(f["provider"] == "generic" for f in findings)
|
|
85
|
+
|
|
86
|
+
findings_high_thr = list(scan_ctx(_ctx_from_source(src), min_entropy=8.0))
|
|
87
|
+
assert not any(f["provider"] == "generic" for f in findings_high_thr)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_ignore_directive_suppresses_matches():
|
|
91
|
+
src = 'GITHUB_TOKEN = "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # skylos: ignore[SKY-S101]\n'
|
|
92
|
+
findings = list(scan_ctx(_ctx_from_source(src)))
|
|
93
|
+
assert findings == []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_allowlist_patterns_suppresses_line():
|
|
97
|
+
src = 'TWILIO = "SKabcdefabcdefabcdefabcdefabcdefabcd"\n'
|
|
98
|
+
allow = [r"TWILIO\s*="]
|
|
99
|
+
findings = list(scan_ctx(_ctx_from_source(src), allowlist_patterns=allow))
|
|
100
|
+
assert findings == []
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_scan_comments_toggle():
|
|
104
|
+
line = '# cred: xoxb-1234567890ABCDEF12 appears only in comment\n'
|
|
105
|
+
findings_default = list(scan_ctx(_ctx_from_source(line)))
|
|
106
|
+
|
|
107
|
+
found_slack = False
|
|
108
|
+
for finding in findings_default:
|
|
109
|
+
if finding["provider"] == "slack":
|
|
110
|
+
found_slack = True
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
assert found_slack
|
|
114
|
+
|
|
115
|
+
findings_off = list(scan_ctx(_ctx_from_source(line), scan_comments=False))
|
|
116
|
+
assert findings_off == []
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_scan_docstrings_toggle_with_ast():
|
|
120
|
+
src = '''"""
|
|
121
|
+
module docstring with a GITHUB token: ghp_1234567890abcdef1234567890abcdef1234
|
|
122
|
+
"""
|
|
123
|
+
def f():
|
|
124
|
+
"""Function docstring with AWS AKIAABCDEFGHIJKLMNOP key."""
|
|
125
|
+
return 1
|
|
126
|
+
'''
|
|
127
|
+
f1 = list(scan_ctx(_ctx_from_source(src, with_ast=True)))
|
|
128
|
+
providers_in_f1 = set()
|
|
129
|
+
for finding in f1:
|
|
130
|
+
provider_name = finding["provider"]
|
|
131
|
+
providers_in_f1.add(provider_name)
|
|
132
|
+
assert "github" in providers_in_f1 or "aws_access_key_id" in providers_in_f1
|
|
133
|
+
|
|
134
|
+
f2 = list(scan_ctx(_ctx_from_source(src, with_ast=True), scan_docstrings=False))
|
|
135
|
+
assert f2 == []
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_suffix_and_path_filters():
|
|
139
|
+
ctx_txt = _ctx_from_source('X="ghp_1234567890abcdef1234567890abcdef1234"\n', rel="notes.txt")
|
|
140
|
+
assert list(scan_ctx(ctx_txt)) == []
|
|
141
|
+
|
|
142
|
+
ctx_vendor = _ctx_from_source('X="AKIAABCDEFGHIJKLMNOP"\n', rel="vendor/app.py")
|
|
143
|
+
out = list(scan_ctx(ctx_vendor, ignore_path_substrings=["vendor"]))
|
|
144
|
+
assert out == []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_masking_behavior_short_and_long():
|
|
148
|
+
short = 'X = "ABCDEFGH"\n'
|
|
149
|
+
long = 'token = "ABCDEFGHIJKLMNOPKLMN"\n'
|
|
150
|
+
|
|
151
|
+
short_findings = scan_ctx(_ctx_from_source(short))
|
|
152
|
+
|
|
153
|
+
f_short = None
|
|
154
|
+
for finding in short_findings:
|
|
155
|
+
if finding["provider"] == "generic":
|
|
156
|
+
f_short = finding
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
long_findings = scan_ctx(_ctx_from_source(long))
|
|
160
|
+
|
|
161
|
+
f_long = None
|
|
162
|
+
for finding in long_findings:
|
|
163
|
+
if finding["provider"] == "generic":
|
|
164
|
+
f_long = finding
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
assert f_short is None
|
|
168
|
+
|
|
169
|
+
long_preview = f_long["preview"]
|
|
170
|
+
starts_with_abcd = long_preview.startswith("ABCD")
|
|
171
|
+
ends_with_klmn = long_preview.endswith("KLMN")
|
|
172
|
+
contains_ellipsis = ELLIPSIS in long_preview
|
|
173
|
+
|
|
174
|
+
assert starts_with_abcd and ends_with_klmn and contains_ellipsis
|
|
175
|
+
|
|
176
|
+
def test_safe_hints_suppress_detection():
|
|
177
|
+
safe_line = 'EXAMPLE_TOKEN = "sk_test_this_is_example_value_not_real_123456"\n'
|
|
178
|
+
out = list(scan_ctx(_ctx_from_source(safe_line)))
|
|
179
|
+
assert out == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|