skylos 2.2.3__py3-none-any.whl → 2.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of skylos might be problematic. Click here for more details.
- skylos/__init__.py +1 -1
- skylos/analyzer.py +28 -7
- skylos/cli.py +16 -2
- skylos/rules/dangerous.py +135 -0
- skylos/rules/secrets.py +34 -5
- {skylos-2.2.3.dist-info → skylos-2.2.4.dist-info}/METADATA +1 -1
- {skylos-2.2.3.dist-info → skylos-2.2.4.dist-info}/RECORD +12 -10
- test/test_dangerous.py +70 -0
- test/test_secrets.py +24 -10
- {skylos-2.2.3.dist-info → skylos-2.2.4.dist-info}/WHEEL +0 -0
- {skylos-2.2.3.dist-info → skylos-2.2.4.dist-info}/entry_points.txt +0 -0
- {skylos-2.2.3.dist-info → skylos-2.2.4.dist-info}/top_level.txt +0 -0
skylos/__init__.py
CHANGED
skylos/analyzer.py
CHANGED
|
@@ -9,6 +9,7 @@ from skylos.visitor import Visitor
|
|
|
9
9
|
from skylos.constants import ( PENALTIES, AUTO_CALLED )
|
|
10
10
|
from skylos.visitors.test_aware import TestAwareVisitor
|
|
11
11
|
from skylos.rules.secrets import scan_ctx as _secrets_scan_ctx
|
|
12
|
+
from skylos.rules.dangerous import scan_ctx as scan_dangerous
|
|
12
13
|
import os
|
|
13
14
|
import traceback
|
|
14
15
|
from skylos.visitors.framework_aware import FrameworkAwareVisitor, detect_framework_usage
|
|
@@ -238,7 +239,7 @@ class Skylos:
|
|
|
238
239
|
if method.simple_name == "format" and cls.endswith("Formatter"):
|
|
239
240
|
method.references += 1
|
|
240
241
|
|
|
241
|
-
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_dangerous = False):
|
|
242
243
|
files, root = self._get_python_files(path, exclude_folders)
|
|
243
244
|
|
|
244
245
|
if not files:
|
|
@@ -262,6 +263,7 @@ class Skylos:
|
|
|
262
263
|
modmap[f] = self._module(root, f)
|
|
263
264
|
|
|
264
265
|
all_secrets = []
|
|
266
|
+
all_dangers = []
|
|
265
267
|
for file in files:
|
|
266
268
|
mod = modmap[file]
|
|
267
269
|
defs, refs, dyn, exports, test_flags, framework_flags = proc_file(file, mod)
|
|
@@ -276,13 +278,23 @@ class Skylos:
|
|
|
276
278
|
|
|
277
279
|
if enable_secrets and _secrets_scan_ctx is not None:
|
|
278
280
|
try:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
+
src = Path(file).read_text(encoding="utf-8", errors="ignore")
|
|
282
|
+
src_lines = src.splitlines(True)
|
|
283
|
+
rel = str(Path(file).relative_to(root))
|
|
284
|
+
ctx = {"relpath": rel, "lines": src_lines, "tree": None}
|
|
281
285
|
findings = list(_secrets_scan_ctx(ctx))
|
|
282
286
|
if findings:
|
|
283
287
|
all_secrets.extend(findings)
|
|
284
288
|
except Exception:
|
|
285
289
|
pass
|
|
290
|
+
|
|
291
|
+
if enable_dangerous and scan_dangerous is not None:
|
|
292
|
+
try:
|
|
293
|
+
findings = scan_dangerous(root, [file])
|
|
294
|
+
if findings:
|
|
295
|
+
all_dangers.extend(findings)
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
286
298
|
|
|
287
299
|
self._mark_refs()
|
|
288
300
|
self._apply_heuristics()
|
|
@@ -296,7 +308,6 @@ class Skylos:
|
|
|
296
308
|
for d in sorted(self.defs.values(), key=def_sort_key):
|
|
297
309
|
if shown >= 50:
|
|
298
310
|
break
|
|
299
|
-
print(f" type={d.type} refs={d.references} conf={d.confidence} exported={d.is_exported} line={d.line} name={d.name}")
|
|
300
311
|
shown += 1
|
|
301
312
|
|
|
302
313
|
unused = []
|
|
@@ -318,7 +329,12 @@ class Skylos:
|
|
|
318
329
|
|
|
319
330
|
if enable_secrets and all_secrets:
|
|
320
331
|
result["secrets"] = all_secrets
|
|
332
|
+
result["analysis_summary"]["secrets_count"] = len(all_secrets)
|
|
321
333
|
|
|
334
|
+
if enable_dangerous and all_dangers:
|
|
335
|
+
result["dangerous"] = all_dangers
|
|
336
|
+
result["analysis_summary"]["dangerous_count"] = len(all_dangers)
|
|
337
|
+
|
|
322
338
|
for u in unused:
|
|
323
339
|
if u["type"] in ("function", "method"):
|
|
324
340
|
result["unused_functions"].append(u)
|
|
@@ -370,13 +386,18 @@ def proc_file(file_or_args, mod=None):
|
|
|
370
386
|
|
|
371
387
|
return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
|
|
372
388
|
|
|
373
|
-
def analyze(path, conf=60, exclude_folders=None, enable_secrets=False):
|
|
374
|
-
return Skylos().analyze(path,conf, exclude_folders, enable_secrets)
|
|
389
|
+
def analyze(path, conf=60, exclude_folders=None, enable_secrets=False, enable_dangerous=False):
|
|
390
|
+
return Skylos().analyze(path,conf, exclude_folders, enable_secrets, enable_dangerous)
|
|
375
391
|
|
|
376
392
|
if __name__ == "__main__":
|
|
377
393
|
if len(sys.argv)>1:
|
|
378
394
|
p = sys.argv[1]
|
|
379
|
-
|
|
395
|
+
|
|
396
|
+
if len(sys.argv) > 2:
|
|
397
|
+
confidence = int(sys.argv[2])
|
|
398
|
+
else:
|
|
399
|
+
confidence = 60
|
|
400
|
+
|
|
380
401
|
result = analyze(p,confidence)
|
|
381
402
|
|
|
382
403
|
data = json.loads(result)
|
skylos/cli.py
CHANGED
|
@@ -43,7 +43,7 @@ def setup_logger(output_file=None):
|
|
|
43
43
|
|
|
44
44
|
formatter = CleanFormatter()
|
|
45
45
|
|
|
46
|
-
console_handler = logging.StreamHandler(sys.
|
|
46
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
47
47
|
console_handler.setFormatter(formatter)
|
|
48
48
|
logger.addHandler(console_handler)
|
|
49
49
|
|
|
@@ -270,6 +270,9 @@ def main():
|
|
|
270
270
|
parser.add_argument("--secrets", action="store_true",
|
|
271
271
|
help="Scan for API keys. Off by default.")
|
|
272
272
|
|
|
273
|
+
parser.add_argument("--danger", action="store_true",
|
|
274
|
+
help="Scan for security issues. Off by default.")
|
|
275
|
+
|
|
273
276
|
args = parser.parse_args()
|
|
274
277
|
|
|
275
278
|
if args.list_default_excludes:
|
|
@@ -305,6 +308,11 @@ def main():
|
|
|
305
308
|
|
|
306
309
|
try:
|
|
307
310
|
result_json = run_analyze(args.path, conf=args.confidence, enable_secrets=bool(args.secrets), exclude_folders=list(final_exclude_folders))
|
|
311
|
+
|
|
312
|
+
if args.json:
|
|
313
|
+
print(result_json)
|
|
314
|
+
return
|
|
315
|
+
|
|
308
316
|
result = json.loads(result_json)
|
|
309
317
|
|
|
310
318
|
except Exception as e:
|
|
@@ -312,8 +320,14 @@ def main():
|
|
|
312
320
|
sys.exit(1)
|
|
313
321
|
|
|
314
322
|
if args.json:
|
|
315
|
-
|
|
323
|
+
lg = logging.getLogger('skylos')
|
|
324
|
+
for h in list(lg.handlers):
|
|
325
|
+
if isinstance(h, logging.StreamHandler):
|
|
326
|
+
lg.removeHandler(h)
|
|
327
|
+
print(result_json)
|
|
316
328
|
return
|
|
329
|
+
|
|
330
|
+
result = json.loads(result_json)
|
|
317
331
|
|
|
318
332
|
unused_functions = result.get("unused_functions", [])
|
|
319
333
|
unused_imports = result.get("unused_imports", [])
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
ALLOWED_SUFFIXES = (".py", ".pyi", ".pyw")
|
|
6
|
+
|
|
7
|
+
## will expand this list later with more rules
|
|
8
|
+
DANGEROUS_CALLS = {
|
|
9
|
+
"eval": ("SKY-D201", "HIGH", "Use of eval()"),
|
|
10
|
+
"exec": ("SKY-D202", "HIGH", "Use of exec()"),
|
|
11
|
+
"os.system": ("SKY-D203", "MEDIUM", "Use of os.system"),
|
|
12
|
+
"pickle.load": ("SKY-D204", "CRITICAL", "Untrusted deserialization via pickle.load"),
|
|
13
|
+
"pickle.loads": ("SKY-D205", "CRITICAL", "Untrusted deserialization via pickle.loads"),
|
|
14
|
+
"yaml.load": ("SKY-D206", "HIGH", "yaml.load without SafeLoader"),
|
|
15
|
+
"hashlib.md5": ("SKY-D207", "MEDIUM", "Weak hash (MD5)"),
|
|
16
|
+
"hashlib.sha1": ("SKY-D208", "MEDIUM", "Weak hash (SHA1)"),
|
|
17
|
+
## this is for arguments like process
|
|
18
|
+
"subprocess.*": ("SKY-D209", "HIGH", "subprocess.* with shell=True",
|
|
19
|
+
{"kw_equals": {"shell": True}}),
|
|
20
|
+
|
|
21
|
+
"requests.*": ("SKY-D210", "HIGH", "requests call with verify=False",
|
|
22
|
+
{"kw_equals": {"verify": False}}),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def _matches_rule(name, rule_key):
|
|
26
|
+
if not name:
|
|
27
|
+
return False
|
|
28
|
+
if rule_key.endswith(".*"):
|
|
29
|
+
return name.startswith(rule_key[:-2] + ".")
|
|
30
|
+
return name == rule_key
|
|
31
|
+
|
|
32
|
+
def _kw_equals(node: ast.Call, requirements):
|
|
33
|
+
if not requirements:
|
|
34
|
+
return True
|
|
35
|
+
kw_map = {}
|
|
36
|
+
keywords = node.keywords or []
|
|
37
|
+
for kw in keywords:
|
|
38
|
+
if kw.arg:
|
|
39
|
+
kw_map[kw.arg] = kw.value
|
|
40
|
+
|
|
41
|
+
for key, expected in requirements.items():
|
|
42
|
+
val = kw_map.get(key)
|
|
43
|
+
if not isinstance(val, ast.Constant):
|
|
44
|
+
return False
|
|
45
|
+
if val.value is not expected:
|
|
46
|
+
return False
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
def qualified_name_from_call(node: ast.Call):
|
|
50
|
+
f = node.func
|
|
51
|
+
parts = []
|
|
52
|
+
while isinstance(f, ast.Attribute):
|
|
53
|
+
parts.append(f.attr)
|
|
54
|
+
f = f.value
|
|
55
|
+
if isinstance(f, ast.Name):
|
|
56
|
+
parts.append(f.id)
|
|
57
|
+
parts.reverse()
|
|
58
|
+
return ".".join(parts)
|
|
59
|
+
if isinstance(f, ast.Name):
|
|
60
|
+
return f.id
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def _yaml_load_without_safeloader(node: ast.Call):
|
|
64
|
+
name = qualified_name_from_call(node)
|
|
65
|
+
if name != "yaml.load":
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
for kw in node.keywords or []:
|
|
69
|
+
if kw.arg == "Loader":
|
|
70
|
+
try:
|
|
71
|
+
text = ast.unparse(kw.value)
|
|
72
|
+
return "SafeLoader" not in text
|
|
73
|
+
except Exception:
|
|
74
|
+
return True
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
def _add_finding(findings,
|
|
78
|
+
file_path: Path,
|
|
79
|
+
node: ast.AST,
|
|
80
|
+
rule_id,
|
|
81
|
+
severity,
|
|
82
|
+
message):
|
|
83
|
+
findings.append({
|
|
84
|
+
"rule_id": rule_id,
|
|
85
|
+
"severity": severity,
|
|
86
|
+
"message": message,
|
|
87
|
+
"file": str(file_path),
|
|
88
|
+
"line": getattr(node, "lineno", 1),
|
|
89
|
+
"col": getattr(node, "col_offset", 0),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
def scan_ctx(root, files):
|
|
93
|
+
findings = []
|
|
94
|
+
|
|
95
|
+
for file_path in files:
|
|
96
|
+
if file_path.suffix.lower() not in ALLOWED_SUFFIXES:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
src = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
101
|
+
tree = ast.parse(src)
|
|
102
|
+
except Exception:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
for node in ast.walk(tree):
|
|
106
|
+
if not isinstance(node, ast.Call):
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
name = qualified_name_from_call(node)
|
|
110
|
+
if not name:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
for rule_key, tup in DANGEROUS_CALLS.items():
|
|
114
|
+
rule_id, severity, message, *rest = tup
|
|
115
|
+
|
|
116
|
+
if rest:
|
|
117
|
+
opts = rest[0]
|
|
118
|
+
else:
|
|
119
|
+
opts = None
|
|
120
|
+
|
|
121
|
+
if not _matches_rule(name, rule_key):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
if rule_key == "yaml.load":
|
|
125
|
+
if not _yaml_load_without_safeloader(node):
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
if opts and "kw_equals" in opts:
|
|
129
|
+
if not _kw_equals(node, opts["kw_equals"]):
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
_add_finding(findings, file_path, node, rule_id, severity, message)
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
return findings
|
skylos/rules/secrets.py
CHANGED
|
@@ -22,7 +22,15 @@ GENERIC_VALUE = re.compile(r"""(?ix)
|
|
|
22
22
|
(?:
|
|
23
23
|
(token|api[_-]?key|secret|password|passwd|pwd|bearer|auth[_-]?token|access[_-]?token)
|
|
24
24
|
\s*[:=]\s*(?P<q>['"])(?P<val>[^'"]{16,})(?P=q)
|
|
25
|
-
)
|
|
25
|
+
)
|
|
26
|
+
|
|
|
27
|
+
(?P<bare>
|
|
28
|
+
(?=[A-Za-z0-9_-]{32,}\b)
|
|
29
|
+
(?=.*[A-Z])
|
|
30
|
+
(?=.*[a-z])
|
|
31
|
+
(?=.*\d)
|
|
32
|
+
[A-Za-z0-9_-]+
|
|
33
|
+
)
|
|
26
34
|
""")
|
|
27
35
|
|
|
28
36
|
SAFE_TEST_HINTS = {
|
|
@@ -30,8 +38,12 @@ SAFE_TEST_HINTS = {
|
|
|
30
38
|
"changeme", "password", "secret", "not_a_real", "do_not_use",
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
42
|
+
|
|
33
43
|
IGNORE_DIRECTIVE = "skylos: ignore[SKY-S101]"
|
|
34
|
-
DEFAULT_MIN_ENTROPY = 3.
|
|
44
|
+
DEFAULT_MIN_ENTROPY = 3.9
|
|
45
|
+
|
|
46
|
+
IS_TEST_PATH = re.compile(r"(^|/)(tests?(/|$)|test_[^/]+\.py$)")
|
|
35
47
|
|
|
36
48
|
def _entropy(s):
|
|
37
49
|
if len(s) == 0:
|
|
@@ -64,6 +76,9 @@ def _mask(tok):
|
|
|
64
76
|
last_part = tok[-4:]
|
|
65
77
|
return first_part + "…" + last_part
|
|
66
78
|
|
|
79
|
+
def _looks_like_identifier(s):
|
|
80
|
+
return bool(_IDENTIFIER.fullmatch(s))
|
|
81
|
+
|
|
67
82
|
def _docstring_lines(tree):
|
|
68
83
|
if tree is None:
|
|
69
84
|
return set()
|
|
@@ -107,12 +122,15 @@ def _docstring_lines(tree):
|
|
|
107
122
|
return docstring_line_numbers
|
|
108
123
|
|
|
109
124
|
def scan_ctx(ctx, *, min_entropy= DEFAULT_MIN_ENTROPY, scan_comments= True,
|
|
110
|
-
scan_docstrings= True, allowlist_patterns= None, ignore_path_substrings= None):
|
|
125
|
+
scan_docstrings= True, allowlist_patterns= None, ignore_path_substrings= None, ignore_tests=True):
|
|
111
126
|
|
|
112
127
|
rel_path = ctx.get("relpath", "")
|
|
113
128
|
if not rel_path.endswith(ALLOWED_FILE_SUFFIXES):
|
|
114
129
|
return []
|
|
115
130
|
|
|
131
|
+
if ignore_tests and IS_TEST_PATH.search(rel_path.replace("\\", "/")):
|
|
132
|
+
return []
|
|
133
|
+
|
|
116
134
|
if ignore_path_substrings:
|
|
117
135
|
for substring in ignore_path_substrings:
|
|
118
136
|
if substring and substring in rel_path:
|
|
@@ -220,22 +238,33 @@ def scan_ctx(ctx, *, min_entropy= DEFAULT_MIN_ENTROPY, scan_comments= True,
|
|
|
220
238
|
"entropy": round(tok_entropy, 2),
|
|
221
239
|
}
|
|
222
240
|
findings.append(aws_finding)
|
|
223
|
-
|
|
224
|
-
|
|
241
|
+
|
|
242
|
+
in_tests = bool(IS_TEST_PATH.search(rel_path.replace("\\", "/")))
|
|
243
|
+
|
|
244
|
+
if in_tests:
|
|
245
|
+
generic_match = None
|
|
246
|
+
else:
|
|
247
|
+
generic_match= GENERIC_VALUE.search(line_content)
|
|
248
|
+
|
|
225
249
|
if generic_match:
|
|
226
250
|
val_group = generic_match.group("val")
|
|
227
251
|
bare_group = generic_match.group("bare")
|
|
228
252
|
|
|
253
|
+
is_bare = False
|
|
229
254
|
if val_group:
|
|
230
255
|
extracted_token = val_group
|
|
231
256
|
elif bare_group:
|
|
232
257
|
extracted_token = bare_group
|
|
258
|
+
is_bare = True
|
|
233
259
|
else:
|
|
234
260
|
extracted_token = ""
|
|
235
261
|
|
|
236
262
|
clean_token = extracted_token.strip()
|
|
237
263
|
|
|
238
264
|
if clean_token:
|
|
265
|
+
if is_bare and _looks_like_identifier(clean_token):
|
|
266
|
+
continue
|
|
267
|
+
|
|
239
268
|
token_lowercase = clean_token.lower()
|
|
240
269
|
has_safe_hint = False
|
|
241
270
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
skylos/__init__.py,sha256=
|
|
2
|
-
skylos/analyzer.py,sha256=
|
|
3
|
-
skylos/cli.py,sha256=
|
|
1
|
+
skylos/__init__.py,sha256=YwPfQwovq4tgmZty1bPKmaNXjqgsmv6wbi94D2oEo6Y,229
|
|
2
|
+
skylos/analyzer.py,sha256=iiymCNg3LphPSHhR3TYLmObR2vlGC99wKligDtHHzDE,17449
|
|
3
|
+
skylos/cli.py,sha256=oEZHS8ogV8pZloj_EIk4qzPVbJS7QenHKJDIp7q2OGg,19882
|
|
4
4
|
skylos/codemods.py,sha256=-1ehjFLc1MmtK3inoSZF_RyBNWF7XZSOq2KH5_MDzuA,9519
|
|
5
5
|
skylos/constants.py,sha256=kU-2FKQAV-ju4rYw62Tw25uRvqauzjZFUqtvGWaI6Es,1571
|
|
6
6
|
skylos/server.py,sha256=oHuevjdDFvJVbvpTtCDjSLOJ6Zy1jL4BYLYV4VFNMXs,15903
|
|
7
7
|
skylos/visitor.py,sha256=o_2JxjXKXAcWLQr8nmoatGAz2EhBk225qE_piHf3hDg,20458
|
|
8
8
|
skylos/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
skylos/rules/
|
|
9
|
+
skylos/rules/dangerous.py,sha256=a1P2wpjxxAHOG_pa83xSKDOnb1HFGPlHzATtkgQtd8Y,4111
|
|
10
|
+
skylos/rules/secrets.py,sha256=xZOKJwfeyx2el-HlGjTflc50XtjUL7R-8mLOJUkwf30,10092
|
|
10
11
|
skylos/visitors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
12
|
skylos/visitors/framework_aware.py,sha256=eX4oU0jQwDWejkkW4kjRNctre27sVLHK1CTDDiqPqRw,13054
|
|
12
13
|
skylos/visitors/test_aware.py,sha256=kxYoMG2m02kbMlxtFOM-MWJO8qqxHviP9HgAMbKRfvU,2304
|
|
@@ -19,10 +20,11 @@ test/test_changes_analyzer.py,sha256=l1hspCFz-sF8gqOilJvntUDuGckwhYsVtzvSRB13ZWw
|
|
|
19
20
|
test/test_cli.py,sha256=rtdKzanDRJT_F92jKkCQFdhvlfwVJxfXKO8Hrbn-mIg,13180
|
|
20
21
|
test/test_codemods.py,sha256=Tbp9jE95HDl77EenTmyTtB1Sc3L8fwY9xiMNVDN5lxo,4522
|
|
21
22
|
test/test_constants.py,sha256=pMuDy0UpC81zENMDCeK6Bqmm3BR_HHZQSlMG-9TgOm0,12602
|
|
23
|
+
test/test_dangerous.py,sha256=zFc2Fi43T824yWY-nKziP1YH_Z4on5rXwxsvgYQ6EN4,2523
|
|
22
24
|
test/test_framework_aware.py,sha256=G9va1dEQ31wsvd4X7ROf_1YhhAQG5CogB7v0hYCojQ8,8802
|
|
23
25
|
test/test_integration.py,sha256=bNKGUe-w0xEZEdnoQNHbssvKMGs9u9fmFQTOz1lX9_k,12398
|
|
24
26
|
test/test_new_behaviours.py,sha256=vAON8rR09RXawZT1knYtZHseyRcECKz5bdOspJoMjUA,1472
|
|
25
|
-
test/test_secrets.py,sha256=
|
|
27
|
+
test/test_secrets.py,sha256=cxbe8zH6lv5aKgazCW01q-mX5F2dRsiH6N2lDXb7ueY,6010
|
|
26
28
|
test/test_skylos.py,sha256=kz77STrS4k3Eez5RDYwGxOg2WH3e7zNZPUYEaTLbGTs,15608
|
|
27
29
|
test/test_test_aware.py,sha256=VmbR_MQY0m941CAxxux8OxJHIr7l8crfWRouSeBMhIo,9390
|
|
28
30
|
test/test_visitor.py,sha256=xAbGv-XaozKm_0WJJhr0hMb6mLaJcbPz57G9-SWkxFU,22764
|
|
@@ -33,8 +35,8 @@ test/sample_repo/sample_repo/commands.py,sha256=b6gQ9YDabt2yyfqGbOpLo0osF7wya8O4
|
|
|
33
35
|
test/sample_repo/sample_repo/models.py,sha256=xXIg3pToEZwKuUCmKX2vTlCF_VeFA0yZlvlBVPIy5Qw,3320
|
|
34
36
|
test/sample_repo/sample_repo/routes.py,sha256=8yITrt55BwS01G7nWdESdx8LuxmReqop1zrGUKPeLi8,2475
|
|
35
37
|
test/sample_repo/sample_repo/utils.py,sha256=S56hEYh8wkzwsD260MvQcmUFOkw2EjFU27nMLFE6G2k,1103
|
|
36
|
-
skylos-2.2.
|
|
37
|
-
skylos-2.2.
|
|
38
|
-
skylos-2.2.
|
|
39
|
-
skylos-2.2.
|
|
40
|
-
skylos-2.2.
|
|
38
|
+
skylos-2.2.4.dist-info/METADATA,sha256=S5mGNVK7yVnSeqxS_5pr1PEXqaUrCcdu3NDZEqDHdYo,314
|
|
39
|
+
skylos-2.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
40
|
+
skylos-2.2.4.dist-info/entry_points.txt,sha256=zzRpN2ByznlQoLeuLolS_TFNYSQxUGBL1EXQsAd6bIA,43
|
|
41
|
+
skylos-2.2.4.dist-info/top_level.txt,sha256=f8GA_7KwfaEopPMP8-EXDQXaqd4IbsOQPakZy01LkdQ,12
|
|
42
|
+
skylos-2.2.4.dist-info/RECORD,,
|
test/test_dangerous.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from skylos.rules.dangerous import scan_ctx
|
|
3
|
+
|
|
4
|
+
def _write(tmp_path: Path, name, code):
|
|
5
|
+
p = tmp_path / name
|
|
6
|
+
p.write_text(code, encoding="utf-8")
|
|
7
|
+
return p
|
|
8
|
+
|
|
9
|
+
def _rule_ids(findings):
|
|
10
|
+
rule_ids = set()
|
|
11
|
+
for f in findings:
|
|
12
|
+
rule_ids.add(f["rule_id"])
|
|
13
|
+
return rule_ids
|
|
14
|
+
|
|
15
|
+
def _scan_one(tmp_path: Path, name, code):
|
|
16
|
+
file_path = _write(tmp_path, name, code)
|
|
17
|
+
return scan_ctx(tmp_path, [file_path])
|
|
18
|
+
|
|
19
|
+
def test_eval(tmp_path):
|
|
20
|
+
out = _scan_one(tmp_path, "a_eval.py", 'eval("1+1")\n')
|
|
21
|
+
assert "SKY-D201" in _rule_ids(out)
|
|
22
|
+
|
|
23
|
+
def test_exec(tmp_path):
|
|
24
|
+
out = _scan_one(tmp_path, "a_exec.py", 'exec("print(1)")\n')
|
|
25
|
+
assert "SKY-D202" in _rule_ids(out)
|
|
26
|
+
|
|
27
|
+
def test_os_system(tmp_path):
|
|
28
|
+
out = _scan_one(tmp_path, "a_os.py", "import os\nos.system('echo hi')\n")
|
|
29
|
+
assert "SKY-D203" in _rule_ids(out)
|
|
30
|
+
|
|
31
|
+
def test_pickle_loads(tmp_path):
|
|
32
|
+
out = _scan_one(tmp_path, "a_pickle.py", "import pickle\npickle.loads(b'\\x80\\x04K\\x01.')\n")
|
|
33
|
+
assert "SKY-D205" in _rule_ids(out)
|
|
34
|
+
|
|
35
|
+
def test_yaml_load_without_safeloader(tmp_path):
|
|
36
|
+
out = _scan_one(tmp_path, "a_yaml.py", "import yaml\nyaml.load('a: 1')\n")
|
|
37
|
+
assert "SKY-D206" in _rule_ids(out)
|
|
38
|
+
|
|
39
|
+
def test_md5_sha1(tmp_path):
|
|
40
|
+
out = _scan_one(tmp_path, "a_hashes.py", "import hashlib\nhashlib.md5(b'd')\nhashlib.sha1(b'd')\n")
|
|
41
|
+
ids = _rule_ids(out)
|
|
42
|
+
assert "SKY-D207" in ids
|
|
43
|
+
assert "SKY-D208" in ids
|
|
44
|
+
|
|
45
|
+
def test_subprocess_shell_true(tmp_path):
|
|
46
|
+
out = _scan_one(tmp_path, "a_subproc.py", "import subprocess\nsubprocess.run('echo hi', shell=True)\n")
|
|
47
|
+
assert "SKY-D209" in _rule_ids(out)
|
|
48
|
+
|
|
49
|
+
def test_requests_verify_false(tmp_path):
|
|
50
|
+
out = _scan_one(tmp_path, "a_requests.py", "import requests\nrequests.get('https://x', verify=False)\n")
|
|
51
|
+
assert "SKY-D210" in _rule_ids(out)
|
|
52
|
+
|
|
53
|
+
def test_yaml_safe_loader_does_not_trigger(tmp_path):
|
|
54
|
+
code = (
|
|
55
|
+
"import yaml\n"
|
|
56
|
+
"from yaml import SafeLoader\n"
|
|
57
|
+
"yaml.load('a: 1', Loader=SafeLoader)\n"
|
|
58
|
+
)
|
|
59
|
+
out = _scan_one(tmp_path, "b_yaml_safe.py", code)
|
|
60
|
+
assert "SKY-D206" not in _rule_ids(out)
|
|
61
|
+
|
|
62
|
+
def test_subprocess_without_shell_true_is_ok(tmp_path):
|
|
63
|
+
code = "import subprocess\nsubprocess.run(['echo','hi'])\n"
|
|
64
|
+
out = _scan_one(tmp_path, "b_subproc_ok.py", code)
|
|
65
|
+
assert "SKY-D209" not in _rule_ids(out)
|
|
66
|
+
|
|
67
|
+
def test_requests_default_verify_true_is_ok(tmp_path):
|
|
68
|
+
code = "import requests\nrequests.get('https://example.com')\n"
|
|
69
|
+
out = _scan_one(tmp_path, "b_requests_ok.py", code)
|
|
70
|
+
assert "SKY-D210" not in _rule_ids(out)
|
test/test_secrets.py
CHANGED
|
@@ -77,16 +77,6 @@ def test_aws_secret_access_key_special_case():
|
|
|
77
77
|
assert "entropy" in hit and isinstance(hit["entropy"], float)
|
|
78
78
|
assert ELLIPSIS in hit["preview"]
|
|
79
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
80
|
def test_ignore_directive_suppresses_matches():
|
|
91
81
|
src = 'GITHUB_TOKEN = "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # skylos: ignore[SKY-S101]\n'
|
|
92
82
|
findings = list(scan_ctx(_ctx_from_source(src)))
|
|
@@ -177,3 +167,27 @@ def test_safe_hints_suppress_detection():
|
|
|
177
167
|
safe_line = 'EXAMPLE_TOKEN = "sk_test_this_is_example_value_not_real_123456"\n'
|
|
178
168
|
out = list(scan_ctx(_ctx_from_source(safe_line)))
|
|
179
169
|
assert out == []
|
|
170
|
+
|
|
171
|
+
def test_generic_is_suppressed_in_test_paths():
|
|
172
|
+
src = 'X = "o2uV7Ew1kZ9Q3nR8sT5yU6pX4cJ2mL7a"\n'
|
|
173
|
+
findings = list(scan_ctx(_ctx_from_source(src, rel="tests/unit/test_secrets.py")))
|
|
174
|
+
|
|
175
|
+
generic_findings = []
|
|
176
|
+
for f in findings:
|
|
177
|
+
if f["provider"] == "generic":
|
|
178
|
+
generic_findings.append(f)
|
|
179
|
+
|
|
180
|
+
assert len(generic_findings) == 0
|
|
181
|
+
|
|
182
|
+
def test_normal_strings_ignored():
|
|
183
|
+
src = 'X = "config_path"\n'
|
|
184
|
+
ctx = _ctx_from_source(src)
|
|
185
|
+
findings = list(scan_ctx(ctx))
|
|
186
|
+
|
|
187
|
+
generic_findings = []
|
|
188
|
+
for f in findings:
|
|
189
|
+
if f["provider"] == "generic":
|
|
190
|
+
generic_findings.append(f)
|
|
191
|
+
|
|
192
|
+
assert len(generic_findings) == 0
|
|
193
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|