8FS-507 regression coverage for Shannon entropy-based secret detection.
12- Validate ``_shannon_entropy()`` for known inputs.
13- Validate ``_check_entropy_violations()`` for AST-level detection.
14- Validate that ``check_no_hardcoded_secrets()`` integrates entropy checking.
15- Validate threshold configurability and the ``--no-entropy`` opt-out.
19Domain: SECRET-SCAN-TESTS
22 L3 — assertion details
27- Tests operate on in-memory strings and AST snippets; no files are
31from __future__
import annotations
40from oct.tools.secret_scanner
import (
42 _check_entropy_violations,
43 _collect_docstring_node_ids,
44 _collect_keyword_excluded_node_ids,
45 _ENTROPY_DEFAULT_THRESHOLD,
46 _DOCSTRING_ENTROPY_DEFAULT,
47 _COMMENT_ENTROPY_DEFAULT,
48 check_no_hardcoded_secrets,
59 assert _shannon_entropy(
"") == 0.0
62 assert _shannon_entropy(
"aaaa") == 0.0
63 assert _shannon_entropy(
"bbbbbbbbb") == 0.0
66 result = _shannon_entropy(
"ab")
67 assert abs(result - 1.0) < 1e-9
70 result = _shannon_entropy(
"abcd")
71 assert abs(result - 2.0) < 1e-9
74 entropy = _shannon_entropy(_B64_TEST_STR)
79 prose =
"the quick brown fox jumps"
80 entropy = _shannon_entropy(prose)
84 small = _shannon_entropy(
"aabbccdd")
85 large = _shannon_entropy(
"abcdefgh")
95 def _parse(self, code: str) -> ast.Module:
96 return ast.parse(textwrap.dedent(code))
99 code = f
'x = "{_B64_TEST_STR}"\n'
100 tree = ast.parse(code)
101 violations = _check_entropy_violations(tree, threshold=4.0)
102 assert len(violations) == 1
103 lineno, preview, entropy = violations[0]
104 assert entropy == 5.0
111 violations = _check_entropy_violations(tree, threshold=1.0, min_length=16)
112 assert len(violations) == 0
116 msg = "This is a normal message for the user interface"
119 violations = _check_entropy_violations(tree, threshold=4.5)
120 assert len(violations) == 0
124 x = "abcdefghijklmnopqrstuvwxyz"
127 low_threshold = _check_entropy_violations(tree, threshold=3.0)
128 high_threshold = _check_entropy_violations(tree, threshold=7.0)
129 assert len(low_threshold) >= len(high_threshold)
132 long_str =
"a" * 10 +
"B" * 10 +
"c" * 10 +
"D" * 10
133 code = f
'x = "{long_str}"'
134 tree = ast.parse(code)
135 violations = _check_entropy_violations(tree, threshold=1.0, min_length=16)
137 _, preview, _ = violations[0]
138 assert len(preview) <= 27
148 code = f
'x = "{_B64_TEST_STR}"\n'
149 passed, msg = check_no_hardcoded_secrets(code)
151 assert "high-entropy" in msg
154 code = f
'x = "{_B64_TEST_STR}"\n'
155 passed, msg = check_no_hardcoded_secrets(code, code_entropy_threshold=0)
159 code = f
'api_key = "sk-{_B64_TEST_STR[:20]}"\n'
160 passed, msg = check_no_hardcoded_secrets(code, code_entropy_threshold=4.0)
162 assert "api_key" in msg
163 assert "high-entropy" in msg
166 src = tmp_path /
"test_project"
168 test_file = src /
"example.py"
169 test_file.write_text(textwrap.dedent(f
'''\
170 #!/usr/bin/env python3
171 # -*- coding: utf-8 -*-
174 x = "{_B64_TEST_STR}"
175 '''), encoding=
"utf-8")
178 run_linter(src, [
"--json", str(test_file)])
179 captured = capsys.readouterr()
180 data = json.loads(captured.out)
181 file_result = data[
"files"][0]
182 secrets_check = file_result[
"checks"][
"no_hardcoded_secrets"]
183 assert not secrets_check[
"pass"]
184 assert "high-entropy" in secrets_check[
"message"]
187 assert _ENTROPY_DEFAULT_THRESHOLD == 4.5
190 code = textwrap.dedent(
'''\
191 x = "abcdefghijklmnop"
193 entropy_val = _shannon_entropy(
"abcdefghijklmnop")
194 strict = check_no_hardcoded_secrets(code, code_entropy_threshold=entropy_val - 0.1)
195 lenient = check_no_hardcoded_secrets(code, code_entropy_threshold=entropy_val + 0.5)
205_B64_TEST_STR =
"aB3dE7fG9hJ2kL5mN8pQ1rS4tU6vW0xY"
207_MID_ENTROPY_STR =
"abcdefghijklmnopqrstuvwxyz01"
209_HIGH_ENTROPY_STR =
"abcdefghijklmnopqrstuvwxyz0123456789AB"
213 """Context-aware entropy scanning: docstrings use strict >, code uses >=."""
215 def _parse(self, code: str) -> ast.Module:
216 return ast.parse(textwrap.dedent(code))
219 """Module docstring at ~4.8 bits/char passes default 5.0 threshold."""
220 code = f
'"""{_MID_ENTROPY_STR}"""\nx = 1\n'
221 tree = ast.parse(code)
222 violations = _check_entropy_violations(tree, threshold=4.5, docstring_threshold=5.0)
223 assert len(violations) == 0
226 """Module docstring at ~5.2 bits/char is caught (strict >)."""
227 code = f
'"""{_HIGH_ENTROPY_STR}"""\nx = 1\n'
228 tree = ast.parse(code)
229 violations = _check_entropy_violations(tree, threshold=4.5, docstring_threshold=5.0)
230 assert len(violations) == 1
233 """Function docstring at ~4.8 bits/char passes default 5.0 threshold."""
234 code = textwrap.dedent(f
'''\
236 """{_MID_ENTROPY_STR}"""
239 tree = ast.parse(code)
240 violations = _check_entropy_violations(tree, threshold=4.5, docstring_threshold=5.0)
241 assert len(violations) == 0
244 """Class docstring at ~4.8 bits/char passes default 5.0 threshold."""
245 code = textwrap.dedent(f
'''\
247 """{_MID_ENTROPY_STR}"""
250 tree = ast.parse(code)
251 violations = _check_entropy_violations(tree, threshold=4.5, docstring_threshold=5.0)
252 assert len(violations) == 0
255 """Regular string at ~4.8 bits/char IS caught at code threshold 4.5."""
256 code = f
'x = "{_MID_ENTROPY_STR}"\n'
257 tree = ast.parse(code)
258 violations = _check_entropy_violations(tree, threshold=4.5, docstring_threshold=5.0)
259 assert len(violations) == 1
262 """Explicit docstring_entropy_threshold=4.0 catches docstrings above 4.0."""
263 code = f
'"""{_MID_ENTROPY_STR}"""\nx = 1\n'
264 tree = ast.parse(code)
266 violations = _check_entropy_violations(tree, threshold=4.5, docstring_threshold=4.0)
267 assert len(violations) == 1
270 """Code string at exactly its threshold IS caught (>= semantics)."""
271 test_str = _MID_ENTROPY_STR
272 exact_entropy = _shannon_entropy(test_str)
273 code = f
'x = "{test_str}"\n'
274 tree = ast.parse(code)
276 violations = _check_entropy_violations(tree, threshold=exact_entropy, docstring_threshold=9.0)
277 assert len(violations) == 1
280 """Docstring at exactly its threshold is NOT caught (strict > semantics)."""
281 test_str = _MID_ENTROPY_STR
282 exact_entropy = _shannon_entropy(test_str)
283 code = f
'"""{test_str}"""\nx = 1\n'
284 tree = ast.parse(code)
286 violations = _check_entropy_violations(tree, threshold=4.5, docstring_threshold=exact_entropy)
287 assert len(violations) == 0
290 """comment_threshold parameter is accepted without error."""
291 code = f
'x = "{_MID_ENTROPY_STR}"\n'
292 tree = ast.parse(code)
294 violations = _check_entropy_violations(
295 tree, threshold=4.5, docstring_threshold=5.0, comment_threshold=3.0,
297 assert len(violations) == 1
307 """Keyword arg matching a field pattern is excluded from entropy."""
308 code = f
'RuleInfo(correct_pattern="{_B64_TEST_STR}")\n'
309 tree = ast.parse(code)
310 excluded = _collect_keyword_excluded_node_ids(tree, [
"correct_pattern"])
311 violations = _check_entropy_violations(
312 tree, threshold=4.0, excluded_node_ids=frozenset(excluded),
314 assert len(violations) == 0
317 """Keyword arg NOT in patterns is still flagged."""
318 code = f
'RuleInfo(name="{_B64_TEST_STR}")\n'
319 tree = ast.parse(code)
320 excluded = _collect_keyword_excluded_node_ids(tree, [
"correct_pattern"])
321 violations = _check_entropy_violations(
322 tree, threshold=4.0, excluded_node_ids=frozenset(excluded),
324 assert len(violations) == 1
327 """Regular assignment to excluded name IS still flagged."""
328 code = f
'correct_pattern = "{_B64_TEST_STR}"\n'
329 tree = ast.parse(code)
330 excluded = _collect_keyword_excluded_node_ids(tree, [
"correct_pattern"])
331 violations = _check_entropy_violations(
332 tree, threshold=4.0, excluded_node_ids=frozenset(excluded),
334 assert len(violations) == 1
337 """Empty pattern list produces empty excluded set."""
338 code = f
'RuleInfo(correct_pattern="{_B64_TEST_STR}")\n'
339 tree = ast.parse(code)
340 excluded = _collect_keyword_excluded_node_ids(tree, [])
341 assert len(excluded) == 0
344 """check_no_hardcoded_secrets respects excluded_field_patterns."""
345 code = f
'RuleInfo(correct_pattern="{_B64_TEST_STR}")\n'
346 passed, msg = check_no_hardcoded_secrets(
347 code, excluded_field_patterns=[
"correct_pattern"],
352 """Without excluded_field_patterns the same code is flagged."""
353 code = f
'RuleInfo(correct_pattern="{_B64_TEST_STR}")\n'
354 passed, msg = check_no_hardcoded_secrets(code)
356 assert "high-entropy" in msg
366 """Well-formed entropy_exclusions pass validation."""
370 "entropy_exclusions": [
372 "field_name_pattern":
"correct_pattern",
373 "context":
"namedtuple_field",
376 "path_pattern":
"tests/",
377 "context":
"test_directory",
378 "restrictions": {
"exempt_entropy_only":
True},
383 errors = _validate_octrc_schema(cfg)
384 assert len(errors) == 0
387 """Invalid context value is caught."""
391 "entropy_exclusions": [
392 {
"context":
"unknown_type",
"field_name_pattern":
"x"},
396 errors = _validate_octrc_schema(cfg)
397 assert any(
"context" in e
for e
in errors)
400 """namedtuple_field without field_name_pattern is caught."""
404 "entropy_exclusions": [
405 {
"context":
"namedtuple_field"},
409 errors = _validate_octrc_schema(cfg)
410 assert any(
"field_name_pattern" in e
for e
in errors)
413 """test_directory without path_pattern is caught."""
417 "entropy_exclusions": [
418 {
"context":
"test_directory"},
422 errors = _validate_octrc_schema(cfg)
423 assert any(
"path_pattern" in e
for e
in errors)
426 """entropy_exclusions must be an array."""
430 "entropy_exclusions":
"not_a_list",
433 errors = _validate_octrc_schema(cfg)
434 assert any(
"must be an array" in e
for e
in errors)
444 """parse_inline_waivers is importable from oct.core.waivers."""
445 from oct.core.waivers
import parse_inline_waivers
as fn
449 """Shared parse_inline_waivers returns correct structure."""
450 from oct.core.waivers
import parse_inline_waivers
451 text =
'x = 1 # OCT-LINT: disable=no_hardcoded_secrets reason="test fixture"'
452 waivers = parse_inline_waivers(text)
454 assert waivers[1][
"rule"] ==
"no_hardcoded_secrets"
455 assert waivers[1][
"reason"]
is not None