sarj-python-lint 0.15.0__tar.gz → 0.15.1__tar.gz
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.
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/PKG-INFO +1 -1
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/pyproject.toml +1 -1
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/_secret_names.py +56 -5
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +11 -10
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/.gitignore +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/README.md +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/single_public_export.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/zero_assertion_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.1
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://github.com/sarj-ai/standards/tree/main/packages/python
|
|
6
6
|
Project-URL: Repository, https://github.com/sarj-ai/standards
|
|
@@ -11,13 +11,15 @@ misfired on a large false-positive class observed in a real audit:
|
|
|
11
11
|
`tokenize`, `tokenizer`, `token_budget`.
|
|
12
12
|
- Row-id / handle names: `api_key_id`, `*_key_id` — the id of a key row, not the
|
|
13
13
|
key material.
|
|
14
|
-
- Boolean feature / presence / state flags:
|
|
15
|
-
`token_present`, `password_set`, `password_configured`
|
|
16
|
-
|
|
17
|
-
is
|
|
14
|
+
- Boolean feature / presence / state flags, in both word orders: trailing
|
|
15
|
+
(`password_enabled`, `token_present`, `password_set`, `password_configured`)
|
|
16
|
+
and leading (`has_secret`, `hasSecret`, `is_token`, `isToken`) — a boolean
|
|
17
|
+
answering "is it there / was it set", not the credential itself. A `type`
|
|
18
|
+
discriminator is the same: `token_type` is `"Bearer"`, `credential_type` is a
|
|
19
|
+
class name.
|
|
18
20
|
- Innocent words embedding a secret word: `secretary` (embeds `secret`).
|
|
19
21
|
|
|
20
|
-
We fix this with
|
|
22
|
+
We fix this with three changes:
|
|
21
23
|
|
|
22
24
|
1. Match a secret word only as a WHOLE token (after snake_case / camelCase
|
|
23
25
|
splitting), never a substring. This alone clears `tokenize`, `tokenizer`,
|
|
@@ -28,6 +30,10 @@ We fix this with two changes:
|
|
|
28
30
|
also present — this clears `token_count`, `api_key_id`, `password_enabled`,
|
|
29
31
|
while still catching a credential that merely leads with such a word
|
|
30
32
|
(`valid_token`, `present_token` are secrets, not flags).
|
|
33
|
+
3. Disqualify an identifier whose LEADING WORD is a boolean predicate (`is`,
|
|
34
|
+
`has`, `was`, ...) — the mirror image of (2), clearing `has_secret` /
|
|
35
|
+
`hasSecret` / `is_token` / `isToken`, which name a boolean answering "does a
|
|
36
|
+
secret exist?" and are neither a leak nor a timing surface.
|
|
31
37
|
"""
|
|
32
38
|
|
|
33
39
|
from __future__ import annotations
|
|
@@ -89,6 +95,28 @@ _INNOCUOUS_WORDS = frozenset(
|
|
|
89
95
|
}
|
|
90
96
|
)
|
|
91
97
|
|
|
98
|
+
# A LEADING boolean-predicate word marks a flag, not the credential itself:
|
|
99
|
+
# `has_secret`, `hasSecret`, `is_token`, `isToken`, `should_rotate_token`.
|
|
100
|
+
#
|
|
101
|
+
# WHY THE SHARED PREDICATE (both SARJ011 and SARJ012), not just the SARJ011-only
|
|
102
|
+
# auth narrowing the TS port uses: this is the exact mirror of the TRAILING
|
|
103
|
+
# `_INNOCUOUS_WORDS` check above, which already exempts both rules. `has_token`
|
|
104
|
+
# and `token_present` are the same boolean; word order must not decide whether a
|
|
105
|
+
# name counts as a credential. And the SARJ012 case stands on its own — a boolean
|
|
106
|
+
# answering "does a secret exist?" leaks nothing when logged, so the rule was
|
|
107
|
+
# reporting a pure false positive. (As with the trailing form, the exemption keys
|
|
108
|
+
# on the NAME: someone who writes `has_secret=the_actual_secret` still leaks, but
|
|
109
|
+
# that hole predates this and is inherent to a name-only rule.)
|
|
110
|
+
#
|
|
111
|
+
# WHY IT IS SAFE IN THE PERMISSIVE DIRECTION: every member is a copula/auxiliary
|
|
112
|
+
# verb that is never the head noun of a credential. Real secret names are noun
|
|
113
|
+
# phrases — `auth_token`, `api_key`, `signing_secret`, `INTERNAL_ADMIN_TOKEN` —
|
|
114
|
+
# and none begins with `is`/`has`/`was`/`are`/`can`/`should`. Matching is on the
|
|
115
|
+
# whole leading WORD, never a prefix of one, so names that merely start with
|
|
116
|
+
# those letters keep firing: `hash_secret` (`hash` != `has`), `issuer_token`
|
|
117
|
+
# (`issuer` != `is`), `canary_token` (`canary` != `can`).
|
|
118
|
+
_FLAG_PREFIXES = frozenset({"is", "has", "was", "are", "can", "should"})
|
|
119
|
+
|
|
92
120
|
# camelCase / PascalCase / ALLCAPS / digit run splitter, applied to each
|
|
93
121
|
# snake/kebab segment. `APIKey` -> ["API", "Key"], `authToken` -> ["auth", "Token"].
|
|
94
122
|
_CAMEL_RE = re.compile(r"[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+")
|
|
@@ -115,6 +143,27 @@ def identifier_tokens(identifier: str) -> list[str]:
|
|
|
115
143
|
return tokens
|
|
116
144
|
|
|
117
145
|
|
|
146
|
+
def leading_word(identifier: str) -> str | None:
|
|
147
|
+
"""Return the first *word* of `identifier`, lowercased, or None if it has none.
|
|
148
|
+
|
|
149
|
+
`identifier_tokens` deliberately emits each whole snake/kebab segment before
|
|
150
|
+
its camel parts, so its first entry for the camelCase `hasSecret` is the
|
|
151
|
+
useless `hassecret` rather than `has`. Splitting the leading segment with the
|
|
152
|
+
same camel regex makes `has_secret` and `hasSecret` both yield `has`.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The lowercased leading word, or None when `identifier` has no word
|
|
156
|
+
characters at all.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
for segment in _SEGMENT_RE.split(identifier):
|
|
160
|
+
if not segment:
|
|
161
|
+
continue
|
|
162
|
+
parts = _CAMEL_RE.findall(segment)
|
|
163
|
+
return parts[0].lower() if parts else segment.lower()
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
118
167
|
def is_secret_name(identifier: str) -> bool:
|
|
119
168
|
"""Report whether `identifier` names raw secret material (a credential, not metadata).
|
|
120
169
|
|
|
@@ -125,6 +174,8 @@ def is_secret_name(identifier: str) -> bool:
|
|
|
125
174
|
tokens = identifier_tokens(identifier)
|
|
126
175
|
if tokens and tokens[-1] in _INNOCUOUS_WORDS:
|
|
127
176
|
return False
|
|
177
|
+
if leading_word(identifier) in _FLAG_PREFIXES:
|
|
178
|
+
return False
|
|
128
179
|
if any(tok in _SECRET_WORDS for tok in tokens):
|
|
129
180
|
return True
|
|
130
181
|
return _has_api_key(tokens)
|
|
@@ -49,10 +49,6 @@ _DESCRIPTOR_WORDS = frozenset({"type", "types", "name", "names", "id", "ids", "k
|
|
|
49
49
|
# credential: `TOKEN_TYPE_SYSTEM`, `credential_type`, `grant_kind`.
|
|
50
50
|
_CATEGORY_WORDS = frozenset({"type", "types", "kind", "kinds"})
|
|
51
51
|
|
|
52
|
-
# A leading boolean-predicate token marks a flag, not the credential itself:
|
|
53
|
-
# `is_token`, `has_secret`, `is_token_strategy`.
|
|
54
|
-
_FLAG_PREFIXES = frozenset({"is", "has", "was", "are", "can", "should"})
|
|
55
|
-
|
|
56
52
|
# Words that make an identifier a secret *only* via an integrity/content hash
|
|
57
53
|
# (`content_hash`, `metadata_hash`, `row_hash`) rather than an authenticator.
|
|
58
54
|
# A name that ALSO carries one of these keeps firing (`password_hash`,
|
|
@@ -182,10 +178,17 @@ def _is_auth_secret_name(identifier: str, *, crypto_module: bool) -> bool:
|
|
|
182
178
|
"""Report whether `identifier` names an authenticator (an access-gating secret).
|
|
183
179
|
|
|
184
180
|
Narrows the shared `is_secret_name` for SARJ011: strips category/handle
|
|
185
|
-
descriptors, `type`/`kind` discriminators,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
181
|
+
descriptors, `type`/`kind` discriminators, and integrity-only hashes, none
|
|
182
|
+
of which are a timing-attack surface. A name whose only auth token is the
|
|
183
|
+
polysemous `signature` needs the module to import crypto machinery —
|
|
184
|
+
otherwise it is a function signature, not a MAC.
|
|
185
|
+
|
|
186
|
+
Boolean flags (`is_token`, `hasSecret`) are NOT handled here: the shared
|
|
187
|
+
`is_secret_name` now rejects a leading flag word for both SARJ011 and
|
|
188
|
+
SARJ012, so the gate above has already returned. A duplicate local check
|
|
189
|
+
used to live here and was dead once that landed — worse, it read
|
|
190
|
+
`tokens[0]`, which is the whole snake segment (`"hassecret"`), so it never
|
|
191
|
+
matched a camelCase flag in the first place.
|
|
189
192
|
|
|
190
193
|
Returns:
|
|
191
194
|
True when comparing `identifier` in non-constant time leaks an auth secret.
|
|
@@ -194,8 +197,6 @@ def _is_auth_secret_name(identifier: str, *, crypto_module: bool) -> bool:
|
|
|
194
197
|
if not is_secret_name(identifier):
|
|
195
198
|
return False
|
|
196
199
|
tokens = identifier_tokens(identifier)
|
|
197
|
-
if tokens and tokens[0] in _FLAG_PREFIXES:
|
|
198
|
-
return False
|
|
199
200
|
if tokens and tokens[-1] in _DESCRIPTOR_WORDS:
|
|
200
201
|
return False
|
|
201
202
|
if any(tok in _CATEGORY_WORDS for tok in tokens):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_fat_try_blocks.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_fstring_in_log.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/no_select_star.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/prefer_class_row.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.15.0 → sarj_python_lint-0.15.1}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|