112 """Return ``(is_string_like, kind)`` for use by :func:`check_no_hardcoded_secrets`.
114 ``kind`` is one of ``"literal"``, ``"f-string"``, or ``"concatenation"``
115 (OI-408). Extended detection catches f-strings and ``"a" + "b"`` patterns
116 in addition to plain string constants.
118 if isinstance(node, ast.Constant)
and isinstance(node.value, str):
119 return True,
"literal"
120 if isinstance(node, ast.JoinedStr):
121 return True,
"f-string"
122 if isinstance(node, ast.BinOp)
and isinstance(node.op, ast.Add):
125 if left_ok
or right_ok:
126 return True,
"concatenation"
154 """Return ``id()`` values of ast.Constant nodes that are docstrings."""
155 ids: set[int] = set()
156 for node
in ast.walk(tree):
157 if isinstance(node, (ast.Module, ast.ClassDef,
158 ast.FunctionDef, ast.AsyncFunctionDef)):
160 and isinstance(node.body[0], ast.Expr)
161 and isinstance(node.body[0].value, ast.Constant)
162 and isinstance(node.body[0].value.value, str)):
163 ids.add(id(node.body[0].value))
169 field_patterns: list[str],
171 """Return ``id()`` values of ast.Constant nodes that are keyword
172 arguments with names matching *field_patterns*.
174 This identifies strings like ``correct_pattern="..."`` in
175 ``RuleInfo(correct_pattern="...")`` calls — documentation/example
176 fields in namedtuple/dataclass constructors.
178 if not field_patterns:
180 ids: set[int] = set()
181 pattern_set = set(field_patterns)
182 for node
in ast.walk(tree):
183 if isinstance(node, ast.Call):
184 for kw
in node.keywords:
185 if (kw.arg
in pattern_set
186 and isinstance(kw.value, ast.Constant)
187 and isinstance(kw.value.value, str)):
188 ids.add(id(kw.value))
195 threshold: float = _ENTROPY_DEFAULT_THRESHOLD,
196 docstring_threshold: float = _DOCSTRING_ENTROPY_DEFAULT,
197 comment_threshold: float = _COMMENT_ENTROPY_DEFAULT,
198 min_length: int = _ENTROPY_MIN_LENGTH,
199 excluded_node_ids: frozenset[int] = frozenset(),
200) -> list[tuple[int, str, float]]:
201 """Walk AST for string constants exceeding the entropy threshold.
203 Returns a list of ``(lineno, value_preview, entropy_bits)`` tuples.
204 Only strings longer than *min_length* are checked. The preview is
205 truncated to 24 characters for readability.
207 Code strings use ``>=`` (catches at threshold). Docstrings use
208 strict ``>`` (must exceed threshold). ``comment_threshold`` is
209 accepted for forward compatibility but not yet consumed — comments
210 do not appear in the AST.
212 Nodes whose ``id()`` is in *excluded_node_ids* are skipped entirely
213 (field-name exclusions from octrc ``entropy_exclusions``).
216 violations: list[tuple[int, str, float]] = []
217 for node
in ast.walk(tree):
218 if isinstance(node, ast.Constant)
and isinstance(node.value, str):
219 if id(node)
in excluded_node_ids:
222 if len(value) < min_length:
224 is_docstring = id(node)
in docstring_ids
225 effective_threshold = (
226 docstring_threshold
if is_docstring
else threshold
232 flagged = entropy > effective_threshold
234 flagged = entropy >= effective_threshold
236 preview = value[:24] + (
"..." if len(value) > 24
else "")
237 violations.append((node.lineno, preview, entropy))
243 tree: ast.Module |
None =
None,
245 mode: str =
"substring",
246 code_entropy_threshold: float = _ENTROPY_DEFAULT_THRESHOLD,
247 docstring_entropy_threshold: float = _DOCSTRING_ENTROPY_DEFAULT,
248 comment_entropy_threshold: float = _COMMENT_ENTROPY_DEFAULT,
249 excluded_field_patterns: list[str] |
None =
None,
250) -> tuple[bool, str]:
251 """Detect string literals bound to secret-shaped names and high-entropy strings.
253 Inspects three AST shapes for name-based detection:
255 * Direct/annotated assignments — ``password = "literal"``.
256 * Dict literals — ``{"api_key": "literal"}``.
257 * Function default arguments — ``def f(token="literal"): ...``.
259 Safe patterns are ignored — calls, subscripts, env lookups.
261 FS-507: when *code_entropy_threshold* > 0, also flags any string literal
262 whose Shannon entropy exceeds the threshold (default 4.5 bits/char).
263 Code strings use ``>=``; docstrings use strict ``>``.
264 Set to 0 to disable entropy checking.
266 *docstring_entropy_threshold* (default 5.0) sets the bar for
267 docstring constants. *comment_entropy_threshold* (default 5.0) is
268 accepted for forward compatibility but not yet consumed.
270 OI-529: ``mode`` forwards to :func:`_name_matches_secret`. Default
271 is ``"substring"`` (legacy behaviour). Pass ``mode="word"`` for
272 segment-aware matching that avoids false positives like
273 ``compass``/``tokenizer``.
275 OI-540: *excluded_field_patterns* lists keyword-argument names
276 (e.g. ``["correct_pattern", "example"]``) whose string values are
277 exempt from entropy scanning — documentation fields in
278 namedtuple/dataclass constructors.
280 Returns ``(passed, detail_message)``; ``passed`` is False when at least
281 one violation is found.
284 tree, _ = safe_parse(text)
288 violations: list[str] = []
290 for node
in ast.walk(tree):
291 if isinstance(node, ast.Assign):
292 for target
in node.targets:
297 f
"L{node.lineno}: '{target.id}' assigned {kind}"
299 else f
"L{node.lineno}: '{target.id}' assigned string literal"
302 if isinstance(node, ast.AnnAssign)
and isinstance(node.target, ast.Name):
307 f
"L{node.lineno}: '{node.target.id}' assigned {kind}"
309 else f
"L{node.lineno}: '{node.target.id}' assigned string literal"
312 if isinstance(node, ast.Dict):
313 for key, value
in zip(node.keys, node.values):
320 f
"L{key.lineno}: dict key '{key.value}' mapped to {kind}"
322 else f
"L{key.lineno}: dict key '{key.value}' mapped to string literal"
325 if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
327 n_defaults = len(args.defaults)
329 defaulted_args = args.args[-n_defaults:]
330 for arg, default
in zip(defaulted_args, args.defaults):
335 f
"L{node.lineno}: parameter '{arg.arg}' has {kind} default"
337 else f
"L{node.lineno}: parameter '{arg.arg}' has string literal default"
339 for arg, default
in zip(args.kwonlyargs, args.kw_defaults):
344 f
"L{node.lineno}: parameter '{arg.arg}' has {kind} default"
346 else f
"L{node.lineno}: parameter '{arg.arg}' has string literal default"
350 if code_entropy_threshold > 0
and tree
is not None:
352 tree, excluded_field_patterns
or [],
356 threshold=code_entropy_threshold,
357 docstring_threshold=docstring_entropy_threshold,
358 comment_threshold=comment_entropy_threshold,
359 excluded_node_ids=frozenset(excluded_ids),
362 f
"L{lineno}: high-entropy string ({entropy:.2f} bits/char): \"{preview}\""
366 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)