119 """Return ``(is_string_like, kind)`` for use by :func:`check_no_hardcoded_secrets`.
121 ``kind`` is one of ``"literal"``, ``"f-string"``, or ``"concatenation"``
122 (OI-408). Extended detection catches f-strings and ``"a" + "b"`` patterns
123 in addition to plain string constants.
125 if isinstance(node, ast.Constant)
and isinstance(node.value, str):
126 return True,
"literal"
127 if isinstance(node, ast.JoinedStr):
128 return True,
"f-string"
129 if isinstance(node, ast.BinOp)
and isinstance(node.op, ast.Add):
132 if left_ok
or right_ok:
133 return True,
"concatenation"
161 """Return ``id()`` values of ast.Constant nodes that are docstrings."""
162 ids: set[int] = set()
163 for node
in ast.walk(tree):
164 if isinstance(node, (ast.Module, ast.ClassDef,
165 ast.FunctionDef, ast.AsyncFunctionDef)):
167 and isinstance(node.body[0], ast.Expr)
168 and isinstance(node.body[0].value, ast.Constant)
169 and isinstance(node.body[0].value.value, str)):
170 ids.add(id(node.body[0].value))
176 field_patterns: list[str],
178 """Return ``id()`` values of ast.Constant nodes that are keyword
179 arguments with names matching *field_patterns*.
181 This identifies strings like ``correct_pattern="..."`` in
182 ``RuleInfo(correct_pattern="...")`` calls - documentation/example
183 fields in namedtuple/dataclass constructors.
185 if not field_patterns:
187 ids: set[int] = set()
188 pattern_set = set(field_patterns)
189 for node
in ast.walk(tree):
190 if isinstance(node, ast.Call):
191 for kw
in node.keywords:
192 if (kw.arg
in pattern_set
193 and isinstance(kw.value, ast.Constant)
194 and isinstance(kw.value.value, str)):
195 ids.add(id(kw.value))
202 threshold: float = _ENTROPY_DEFAULT_THRESHOLD,
203 docstring_threshold: float = _DOCSTRING_ENTROPY_DEFAULT,
204 comment_threshold: float = _COMMENT_ENTROPY_DEFAULT,
205 min_length: int = _ENTROPY_MIN_LENGTH,
206 excluded_node_ids: frozenset[int] = frozenset(),
207) -> list[tuple[int, str, float]]:
208 """Walk AST for string constants exceeding the entropy threshold.
210 Returns a list of ``(lineno, value_preview, entropy_bits)`` tuples.
211 Only strings longer than *min_length* are checked. The preview is
212 truncated to 24 characters for readability.
214 Code strings use ``>=`` (catches at threshold). Docstrings use
215 strict ``>`` (must exceed threshold). ``comment_threshold`` is
216 accepted for forward compatibility but not yet consumed - comments
217 do not appear in the AST.
219 Nodes whose ``id()`` is in *excluded_node_ids* are skipped entirely
220 (field-name exclusions from octrc ``entropy_exclusions``).
223 violations: list[tuple[int, str, float]] = []
224 for node
in ast.walk(tree):
225 if isinstance(node, ast.Constant)
and isinstance(node.value, str):
226 if id(node)
in excluded_node_ids:
229 if len(value) < min_length:
231 is_docstring = id(node)
in docstring_ids
232 effective_threshold = (
233 docstring_threshold
if is_docstring
else threshold
239 flagged = entropy > effective_threshold
241 flagged = entropy >= effective_threshold
243 preview = value[:24] + (
"..." if len(value) > 24
else "")
244 violations.append((node.lineno, preview, entropy))
250 tree: ast.Module |
None =
None,
252 mode: str =
"substring",
253 code_entropy_threshold: float = _ENTROPY_DEFAULT_THRESHOLD,
254 docstring_entropy_threshold: float = _DOCSTRING_ENTROPY_DEFAULT,
255 comment_entropy_threshold: float = _COMMENT_ENTROPY_DEFAULT,
256 excluded_field_patterns: list[str] |
None =
None,
257) -> tuple[bool, str]:
258 """Detect string literals bound to secret-shaped names and high-entropy strings.
260 Inspects three AST shapes for name-based detection:
262 * Direct/annotated assignments - ``password = "literal"``.
263 * Dict literals - ``{"api_key": "literal"}``.
264 * Function default arguments - ``def f(token="literal"): ...``.
266 Safe patterns are ignored - calls, subscripts, env lookups.
268 FS-507: when *code_entropy_threshold* > 0, also flags any string literal
269 whose Shannon entropy exceeds the threshold (default 4.5 bits/char).
270 Code strings use ``>=``; docstrings use strict ``>``.
271 Set to 0 to disable entropy checking.
273 *docstring_entropy_threshold* (default 5.0) sets the bar for
274 docstring constants. *comment_entropy_threshold* (default 5.0) is
275 accepted for forward compatibility but not yet consumed.
277 OI-529: ``mode`` forwards to :func:`_name_matches_secret`. Default
278 is ``"substring"`` (legacy behaviour). Pass ``mode="word"`` for
279 segment-aware matching that avoids false positives like
280 ``compass``/``tokenizer``.
282 OI-540: *excluded_field_patterns* lists keyword-argument names
283 (e.g. ``["correct_pattern", "example"]``) whose string values are
284 exempt from entropy scanning - documentation fields in
285 namedtuple/dataclass constructors.
287 Returns ``(passed, detail_message)``; ``passed`` is False when at least
288 one violation is found.
291 tree, _ = safe_parse(text)
300 return False,
"file is unparseable; secret scan could not run (cannot certify secret-free)"
302 violations: list[str] = []
304 for node
in ast.walk(tree):
305 if isinstance(node, ast.Assign):
306 for target
in node.targets:
311 f
"L{node.lineno}: '{target.id}' assigned {kind}"
313 else f
"L{node.lineno}: '{target.id}' assigned string literal"
316 if isinstance(node, ast.AnnAssign)
and isinstance(node.target, ast.Name):
321 f
"L{node.lineno}: '{node.target.id}' assigned {kind}"
323 else f
"L{node.lineno}: '{node.target.id}' assigned string literal"
326 if isinstance(node, ast.Dict):
327 for key, value
in zip(node.keys, node.values):
334 f
"L{key.lineno}: dict key '{key.value}' mapped to {kind}"
336 else f
"L{key.lineno}: dict key '{key.value}' mapped to string literal"
339 if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
341 n_defaults = len(args.defaults)
343 defaulted_args = args.args[-n_defaults:]
344 for arg, default
in zip(defaulted_args, args.defaults):
349 f
"L{node.lineno}: parameter '{arg.arg}' has {kind} default"
351 else f
"L{node.lineno}: parameter '{arg.arg}' has string literal default"
353 for arg, default
in zip(args.kwonlyargs, args.kw_defaults):
358 f
"L{node.lineno}: parameter '{arg.arg}' has {kind} default"
360 else f
"L{node.lineno}: parameter '{arg.arg}' has string literal default"
364 if code_entropy_threshold > 0
and tree
is not None:
366 tree, excluded_field_patterns
or [],
370 threshold=code_entropy_threshold,
371 docstring_threshold=docstring_entropy_threshold,
372 comment_threshold=comment_entropy_threshold,
373 excluded_node_ids=frozenset(excluded_ids),
376 f
"L{lineno}: high-entropy string ({entropy:.2f} bits/char): \"{preview}\""
380 return False,
"; ".join(violations)
list[tuple[int, str, float]] _check_entropy_violations(ast.Module tree, *, float threshold=_ENTROPY_DEFAULT_THRESHOLD, float docstring_threshold=_DOCSTRING_ENTROPY_DEFAULT, float comment_threshold=_COMMENT_ENTROPY_DEFAULT, int min_length=_ENTROPY_MIN_LENGTH, frozenset[int] excluded_node_ids=frozenset())
tuple[bool, str] check_no_hardcoded_secrets(str text, ast.Module|None tree=None, *, str mode="substring", float code_entropy_threshold=_ENTROPY_DEFAULT_THRESHOLD, float docstring_entropy_threshold=_DOCSTRING_ENTROPY_DEFAULT, float comment_entropy_threshold=_COMMENT_ENTROPY_DEFAULT, list[str]|None excluded_field_patterns=None)