leftblock 0.4.0__tar.gz → 0.4.3__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.
- {leftblock-0.4.0 → leftblock-0.4.3}/PKG-INFO +13 -18
- {leftblock-0.4.0 → leftblock-0.4.3}/README.md +12 -17
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock/cli.py +14 -6
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock/data/gitleaks.toml +30 -67
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock.egg-info/PKG-INFO +13 -18
- {leftblock-0.4.0 → leftblock-0.4.3}/pyproject.toml +5 -5
- {leftblock-0.4.0 → leftblock-0.4.3}/tests/test_bypasses.py +1 -1
- {leftblock-0.4.0 → leftblock-0.4.3}/tests/test_false_positives.py +9 -6
- {leftblock-0.4.0 → leftblock-0.4.3}/tests/test_kanbus_advice.py +11 -3
- {leftblock-0.4.0 → leftblock-0.4.3}/tests/test_recorded_overrides.py +3 -3
- {leftblock-0.4.0 → leftblock-0.4.3}/tests/test_sensitive_rules.py +167 -163
- {leftblock-0.4.0 → leftblock-0.4.3}/LICENSE +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock/__init__.py +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock/allow.py +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock/bypass.py +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock/core.py +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock/hooks.py +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock/merge.py +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock.egg-info/SOURCES.txt +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock.egg-info/dependency_links.txt +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock.egg-info/entry_points.txt +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock.egg-info/requires.txt +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/leftblock.egg-info/top_level.txt +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/setup.cfg +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/tests/test_branding.py +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/tests/test_commit_gate.py +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/tests/test_default_config.py +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/tests/test_install.py +0 -0
- {leftblock-0.4.0 → leftblock-0.4.3}/tests/test_local_install.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: leftblock
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: LeftBlock: a pluggable inspection gate for Git commits
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -124,7 +124,7 @@ configures the integration; it does not install scanner binaries.
|
|
|
124
124
|
In the intended Python environment:
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
python -m pip install leftblock==0.4.
|
|
127
|
+
python -m pip install leftblock==0.4.3
|
|
128
128
|
python -m leftblock.cli --help
|
|
129
129
|
```
|
|
130
130
|
|
|
@@ -397,11 +397,11 @@ PEM private keys, and other standard secret formats.
|
|
|
397
397
|
|
|
398
398
|
| Rule | What it catches |
|
|
399
399
|
|---|---|
|
|
400
|
-
| `leftblock-personal-email` | Personal email addresses (infrastructure identities like `git@github.com`, `@users.noreply.github.com
|
|
400
|
+
| `leftblock-personal-email` | Personal email addresses (infrastructure identities like `git@github.com`, `@users.noreply.github.com`, no-reply addresses, `@example.*` placeholders, `@example.edu` and any address on a `.test`, `.example`, `.invalid` or `.localhost` domain are allowlisted) |
|
|
401
401
|
| `leftblock-home-path-posix` | Home-folder paths `/Users/<name>`, `/home/<name>` |
|
|
402
402
|
| `leftblock-home-path-windows` | `C:\Users\<name>` and `\Users\<name>` |
|
|
403
|
-
| `leftblock-session-uuid` | Potential session UUIDs
|
|
404
|
-
| `leftblock-actor-id` | `actor_id` / `user_id` / `author_id` JSON values |
|
|
403
|
+
| `leftblock-session-uuid` | Potential session UUIDs (not flagged in Kanbus board data; example values and the `.kanbus.yml` `project_id` line are also allowed) |
|
|
404
|
+
| `leftblock-actor-id` | `actor_id` / `user_id` / `author_id` JSON values (not flagged in Kanbus board data) |
|
|
405
405
|
| `leftblock-openai-key` | `sk-`, `sk-proj-`, `sk-svcacct-` OpenAI keys |
|
|
406
406
|
| `leftblock-anthropic-key` | `sk-ant-` Anthropic keys |
|
|
407
407
|
| `leftblock-eth-private-key` | Ethereum-style private keys (`0x` + 64 hex) |
|
|
@@ -409,7 +409,7 @@ PEM private keys, and other standard secret formats.
|
|
|
409
409
|
| `leftblock-bip39-mnemonic` | BIP-39 seed phrases (12–24 words) in seed/mnemonic/recovery context |
|
|
410
410
|
|
|
411
411
|
A blocked check prints a one-line hint after each rule's findings (once per
|
|
412
|
-
rule): use a role or placeholder
|
|
412
|
+
rule): use a role address or a reserved placeholder for personal emails; a relative path,
|
|
413
413
|
`$HOME` or `~` for home paths; drop or describe session and actor identifiers;
|
|
414
414
|
and for API, private and wallet keys, remove the value **and rotate the key**,
|
|
415
415
|
since it may already be exposed. Findings in Kanbus board data
|
|
@@ -417,18 +417,13 @@ since it may already be exposed. Findings in Kanbus board data
|
|
|
417
417
|
those files and that the content should be fixed through `kbs` (for example
|
|
418
418
|
`kbs update` or `kbs comment`) rather than by hand.
|
|
419
419
|
|
|
420
|
-
Kanbus
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
Provider session fields, nested actor values, prose and arbitrary arrays
|
|
428
|
-
remain scanned. A project prefix alone grants no exception, and a second
|
|
429
|
-
value on the same line cannot inherit one. These checks recognize syntax;
|
|
430
|
-
they do not validate a full JSON schema or prove an identifier is public.
|
|
431
|
-
Other secret rules still apply to values in the accepted fields.
|
|
420
|
+
Kanbus is the mandated task tracker, so identifiers in its board data (actor
|
|
421
|
+
ids, session UUIDs, and UUIDs in any field or prose) are not sensitive and never
|
|
422
|
+
block a `kbs` write. Board data means `project/issues/*.json`,
|
|
423
|
+
`project/events/*.json` and `project/.cache/index.json`; the exception is
|
|
424
|
+
limited to the session-UUID and actor-id rules, so personal emails, home paths
|
|
425
|
+
and API or wallet keys in those files are still detected, and the same content
|
|
426
|
+
in any other path is still flagged.
|
|
432
427
|
|
|
433
428
|
### Crypto wallet material: what is and is not flagged
|
|
434
429
|
|
|
@@ -91,7 +91,7 @@ configures the integration; it does not install scanner binaries.
|
|
|
91
91
|
In the intended Python environment:
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
|
-
python -m pip install leftblock==0.4.
|
|
94
|
+
python -m pip install leftblock==0.4.3
|
|
95
95
|
python -m leftblock.cli --help
|
|
96
96
|
```
|
|
97
97
|
|
|
@@ -364,11 +364,11 @@ PEM private keys, and other standard secret formats.
|
|
|
364
364
|
|
|
365
365
|
| Rule | What it catches |
|
|
366
366
|
|---|---|
|
|
367
|
-
| `leftblock-personal-email` | Personal email addresses (infrastructure identities like `git@github.com`, `@users.noreply.github.com
|
|
367
|
+
| `leftblock-personal-email` | Personal email addresses (infrastructure identities like `git@github.com`, `@users.noreply.github.com`, no-reply addresses, `@example.*` placeholders, `@example.edu` and any address on a `.test`, `.example`, `.invalid` or `.localhost` domain are allowlisted) |
|
|
368
368
|
| `leftblock-home-path-posix` | Home-folder paths `/Users/<name>`, `/home/<name>` |
|
|
369
369
|
| `leftblock-home-path-windows` | `C:\Users\<name>` and `\Users\<name>` |
|
|
370
|
-
| `leftblock-session-uuid` | Potential session UUIDs
|
|
371
|
-
| `leftblock-actor-id` | `actor_id` / `user_id` / `author_id` JSON values |
|
|
370
|
+
| `leftblock-session-uuid` | Potential session UUIDs (not flagged in Kanbus board data; example values and the `.kanbus.yml` `project_id` line are also allowed) |
|
|
371
|
+
| `leftblock-actor-id` | `actor_id` / `user_id` / `author_id` JSON values (not flagged in Kanbus board data) |
|
|
372
372
|
| `leftblock-openai-key` | `sk-`, `sk-proj-`, `sk-svcacct-` OpenAI keys |
|
|
373
373
|
| `leftblock-anthropic-key` | `sk-ant-` Anthropic keys |
|
|
374
374
|
| `leftblock-eth-private-key` | Ethereum-style private keys (`0x` + 64 hex) |
|
|
@@ -376,7 +376,7 @@ PEM private keys, and other standard secret formats.
|
|
|
376
376
|
| `leftblock-bip39-mnemonic` | BIP-39 seed phrases (12–24 words) in seed/mnemonic/recovery context |
|
|
377
377
|
|
|
378
378
|
A blocked check prints a one-line hint after each rule's findings (once per
|
|
379
|
-
rule): use a role or placeholder
|
|
379
|
+
rule): use a role address or a reserved placeholder for personal emails; a relative path,
|
|
380
380
|
`$HOME` or `~` for home paths; drop or describe session and actor identifiers;
|
|
381
381
|
and for API, private and wallet keys, remove the value **and rotate the key**,
|
|
382
382
|
since it may already be exposed. Findings in Kanbus board data
|
|
@@ -384,18 +384,13 @@ since it may already be exposed. Findings in Kanbus board data
|
|
|
384
384
|
those files and that the content should be fixed through `kbs` (for example
|
|
385
385
|
`kbs update` or `kbs comment`) rather than by hand.
|
|
386
386
|
|
|
387
|
-
Kanbus
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
Provider session fields, nested actor values, prose and arbitrary arrays
|
|
395
|
-
remain scanned. A project prefix alone grants no exception, and a second
|
|
396
|
-
value on the same line cannot inherit one. These checks recognize syntax;
|
|
397
|
-
they do not validate a full JSON schema or prove an identifier is public.
|
|
398
|
-
Other secret rules still apply to values in the accepted fields.
|
|
387
|
+
Kanbus is the mandated task tracker, so identifiers in its board data (actor
|
|
388
|
+
ids, session UUIDs, and UUIDs in any field or prose) are not sensitive and never
|
|
389
|
+
block a `kbs` write. Board data means `project/issues/*.json`,
|
|
390
|
+
`project/events/*.json` and `project/.cache/index.json`; the exception is
|
|
391
|
+
limited to the session-UUID and actor-id rules, so personal emails, home paths
|
|
392
|
+
and API or wallet keys in those files are still detected, and the same content
|
|
393
|
+
in any other path is still flagged.
|
|
399
394
|
|
|
400
395
|
### Crypto wallet material: what is and is not flagged
|
|
401
396
|
|
|
@@ -335,7 +335,7 @@ _REMOVE_AND_ROTATE = "Remove it AND rotate the key: it may already be exposed."
|
|
|
335
335
|
_HOME_PATH = "Use a relative path, $HOME or ~ instead."
|
|
336
336
|
_DESCRIBE_ID = "Drop the identifier or describe it instead."
|
|
337
337
|
RULE_HINTS = {
|
|
338
|
-
"leftblock-personal-email": "Use a role or placeholder
|
|
338
|
+
"leftblock-personal-email": "Use a role address or a reserved placeholder (example.com/.org/.net, or any .test, .example or .invalid domain), or describe it instead.",
|
|
339
339
|
"leftblock-home-path-posix": _HOME_PATH,
|
|
340
340
|
"leftblock-home-path-windows": _HOME_PATH,
|
|
341
341
|
"leftblock-session-uuid": _DESCRIBE_ID,
|
|
@@ -348,11 +348,18 @@ RULE_HINTS = {
|
|
|
348
348
|
bypass.MARKER_RULE: "Remove the marker and fix the content, or record a reasoned override.",
|
|
349
349
|
bypass.IGNORE_RULE: "Don't suppress findings silently; record a reasoned override per finding.",
|
|
350
350
|
}
|
|
351
|
-
|
|
352
|
-
"This file is written by Kanbus; fix the content
|
|
353
|
-
"(
|
|
351
|
+
KANBUS_ISSUE_HINT = (
|
|
352
|
+
"This issue file is written by Kanbus; fix the content with kbs update "
|
|
353
|
+
"(or kbs comment), don't hand-edit it."
|
|
354
354
|
)
|
|
355
|
-
|
|
355
|
+
# Events are append-only history: kbs update/comment add a new event and leave
|
|
356
|
+
# the flagged text in place.
|
|
357
|
+
KANBUS_EVENT_HINT = (
|
|
358
|
+
"This Kanbus event is append-only history and can't be edited. If the issue "
|
|
359
|
+
"isn't committed yet, run kbs delete <issue id> and recreate it without the "
|
|
360
|
+
"content; otherwise ask the board owner."
|
|
361
|
+
)
|
|
362
|
+
KANBUS_DATA = re.compile(r"(^|/)project/(issues|events)/[^/]+\.jsonl?$")
|
|
356
363
|
|
|
357
364
|
|
|
358
365
|
def print_failures(failures):
|
|
@@ -377,7 +384,8 @@ def print_failures(failures):
|
|
|
377
384
|
if rule in RULE_HINTS:
|
|
378
385
|
print(" Hint: " + RULE_HINTS[rule], file=sys.stderr)
|
|
379
386
|
for name in kanbus_files:
|
|
380
|
-
|
|
387
|
+
hint = KANBUS_EVENT_HINT if KANBUS_DATA.search(name).group(2) == "events" else KANBUS_ISSUE_HINT
|
|
388
|
+
print(f" {_plain(name)}: {hint}", file=sys.stderr)
|
|
381
389
|
else:
|
|
382
390
|
print_warn(str(result.findings))
|
|
383
391
|
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
# indistinguishable from SHA-256 and is intentionally left to the
|
|
22
22
|
# generic-api-key keyword context (see docs).
|
|
23
23
|
#
|
|
24
|
-
# Allowlists are scoped to exact
|
|
25
|
-
#
|
|
26
|
-
#
|
|
24
|
+
# Allowlists are scoped to exact values (placeholder usernames/actor ids,
|
|
25
|
+
# the 00000000-0000-4000-8000-... example UUID shape, the .kanbus.yml
|
|
26
|
+
# project_id line). The one path-wide exception is Kanbus board data, whose
|
|
27
|
+
# identifiers are not sensitive for the session-uuid and actor-id rules only.
|
|
27
28
|
|
|
28
29
|
title = "LeftBlock sensitive-data rules"
|
|
29
30
|
|
|
@@ -62,8 +63,16 @@ keywords = ["@"]
|
|
|
62
63
|
regexes = [
|
|
63
64
|
# Infrastructure and placeholder identities, not personal ones.
|
|
64
65
|
'''@users\.noreply\.github\.com''',
|
|
66
|
+
# No-reply addresses (e.g. commit Co-Authored-By trailers) are not personal.
|
|
67
|
+
'''(^|[^a-zA-Z0-9._%+\-'])no-?reply@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}''',
|
|
65
68
|
'''git@github\.com''',
|
|
66
69
|
'''@(example|github)\.(com|org|net|io|ai)''',
|
|
70
|
+
# Domains reserved for documentation and testing: the TLDs .test, .example,
|
|
71
|
+
# .invalid and .localhost (RFC 2606 section 2, RFC 6761 section 6) at any
|
|
72
|
+
# subdomain depth, and example.edu (RFC 2606 section 3). Anchored on the end
|
|
73
|
+
# of the address so look-alikes such as test.com or example.test.co still flag.
|
|
74
|
+
'''@(?:[a-zA-Z0-9\-]+\.)*(?:test|example|invalid|localhost)$''',
|
|
75
|
+
'''@example\.edu$''',
|
|
67
76
|
]
|
|
68
77
|
|
|
69
78
|
[[rules]]
|
|
@@ -106,64 +115,19 @@ regexes = [
|
|
|
106
115
|
'''^11111111-1111-4111-8111-1111111111[0-9a-f]{2}$''',
|
|
107
116
|
]
|
|
108
117
|
|
|
109
|
-
# Kanbus
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
# single boundary byte, plus CRLF endings, without spanning properties.
|
|
118
|
+
# Kanbus board data (issues, events and the derived cache index) is written by
|
|
119
|
+
# Kanbus and is the mandated tracker in these repositories. Its identifiers,
|
|
120
|
+
# including actor ids and session UUIDs, are not treated as sensitive
|
|
121
|
+
# (decision 2026-10-20), so LeftBlock never blocks a kbs write on them. Emails,
|
|
122
|
+
# home paths and keys in board files are still detected by their own rules.
|
|
115
123
|
[[rules.allowlists]]
|
|
116
|
-
description = "Kanbus board
|
|
117
|
-
condition = "AND"
|
|
124
|
+
description = "Kanbus board data identifiers"
|
|
118
125
|
paths = [
|
|
119
126
|
'''(^|/)project/(issues|events)/[^/]+\.json$''',
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
regexes = [
|
|
123
|
-
'''^\n?[ \t]*"(id|comment_id|event_id|issue_id|parent_id|parent|target|target_id|from|to)"[ \t]*:[ \t]*"(?:[A-Za-z][A-Za-z0-9_]*-)*[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",?[ \t]*\r?$''',
|
|
124
|
-
]
|
|
125
|
-
|
|
126
|
-
# Event actor attribution is structural too. Keep this separate from
|
|
127
|
-
# session_id/provider_session_id/resumed_session, which remain detected.
|
|
128
|
-
[[rules.allowlists]]
|
|
129
|
-
description = "Kanbus event UUID actor attribution"
|
|
130
|
-
condition = "AND"
|
|
131
|
-
paths = [
|
|
132
|
-
'''(^|/)project/events/[^/]+\.json$''',
|
|
133
|
-
]
|
|
134
|
-
regexTarget = "line"
|
|
135
|
-
regexes = [
|
|
136
|
-
'''^\n? "actor_id"[ \t]*:[ \t]*"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",?[ \t]*\r?$''',
|
|
137
|
-
]
|
|
138
|
-
|
|
139
|
-
# Coordination events refer to a generated claim by its prefixed key.
|
|
140
|
-
# Require the canonical payload-property depth; bare session values,
|
|
141
|
-
# root/deeper properties, prose and neighboring values are not exempt.
|
|
142
|
-
[[rules.allowlists]]
|
|
143
|
-
description = "Kanbus coordination payload claim reference"
|
|
144
|
-
condition = "AND"
|
|
145
|
-
paths = [
|
|
146
|
-
'''(^|/)project/events/[^/]+\.json$''',
|
|
147
|
-
]
|
|
148
|
-
regexTarget = "line"
|
|
149
|
-
regexes = [
|
|
150
|
-
'''^\n? "claim_id"[ \t]*:[ \t]*"(?:[A-Za-z][A-Za-z0-9_]*-)+[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",?[ \t]*\r?$''',
|
|
151
|
-
]
|
|
152
|
-
|
|
153
|
-
# The legacy Kanbus cache duplicates issue identifiers and stores numeric
|
|
154
|
-
# mtimes keyed by issue filename. Do not exempt the cache file: it also
|
|
155
|
-
# contains full descriptions and comments that still require scanning.
|
|
156
|
-
[[rules.allowlists]]
|
|
157
|
-
description = "Kanbus cache issue identifiers and numeric filename mtimes"
|
|
158
|
-
condition = "AND"
|
|
159
|
-
paths = [
|
|
127
|
+
# Kanbus's LLM usage log (one JSON record per line).
|
|
128
|
+
'''(^|/)project/events/[^/]+\.jsonl$''',
|
|
160
129
|
'''(^|/)project/\.cache/index\.json$''',
|
|
161
130
|
]
|
|
162
|
-
regexTarget = "line"
|
|
163
|
-
regexes = [
|
|
164
|
-
'''^\n? "(id|parent)"[ \t]*:[ \t]*"(?:[A-Za-z][A-Za-z0-9_]*-)+[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",?[ \t]*\r?$''',
|
|
165
|
-
'''^\n? "(?:[A-Za-z][A-Za-z0-9_]*-)+[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.json"[ \t]*:[ \t]*-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?,?[ \t]*\r?$''',
|
|
166
|
-
]
|
|
167
131
|
|
|
168
132
|
# Kanbus 0.40+ records a public per-project UUID v4 in .kanbus.yml so every
|
|
169
133
|
# clone shares one realtime topic. Accept only the top-level YAML key with a
|
|
@@ -191,19 +155,18 @@ regexes = [
|
|
|
191
155
|
'''^example[-_a-z0-9]*$''',
|
|
192
156
|
]
|
|
193
157
|
|
|
194
|
-
# Kanbus
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
#
|
|
158
|
+
# Kanbus board data (issues, events and the derived cache index) is written by
|
|
159
|
+
# Kanbus and is the mandated tracker in these repositories. Its identifiers,
|
|
160
|
+
# including actor ids and session UUIDs, are not treated as sensitive
|
|
161
|
+
# (decision 2026-10-20), so LeftBlock never blocks a kbs write on them. Emails,
|
|
162
|
+
# home paths and keys in board files are still detected by their own rules.
|
|
198
163
|
[[rules.allowlists]]
|
|
199
|
-
description = "Kanbus board
|
|
200
|
-
condition = "AND"
|
|
164
|
+
description = "Kanbus board data identifiers"
|
|
201
165
|
paths = [
|
|
202
|
-
'''(^|/)project/events/[^/]+\.json$''',
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
'''^\n? "actor_id"[ \t]*:[ \t]*"[^"]+",?[ \t]*\r?$''',
|
|
166
|
+
'''(^|/)project/(issues|events)/[^/]+\.json$''',
|
|
167
|
+
# Kanbus's LLM usage log (one JSON record per line).
|
|
168
|
+
'''(^|/)project/events/[^/]+\.jsonl$''',
|
|
169
|
+
'''(^|/)project/\.cache/index\.json$''',
|
|
207
170
|
]
|
|
208
171
|
|
|
209
172
|
# --------------------------------------------------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: leftblock
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: LeftBlock: a pluggable inspection gate for Git commits
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -124,7 +124,7 @@ configures the integration; it does not install scanner binaries.
|
|
|
124
124
|
In the intended Python environment:
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
python -m pip install leftblock==0.4.
|
|
127
|
+
python -m pip install leftblock==0.4.3
|
|
128
128
|
python -m leftblock.cli --help
|
|
129
129
|
```
|
|
130
130
|
|
|
@@ -397,11 +397,11 @@ PEM private keys, and other standard secret formats.
|
|
|
397
397
|
|
|
398
398
|
| Rule | What it catches |
|
|
399
399
|
|---|---|
|
|
400
|
-
| `leftblock-personal-email` | Personal email addresses (infrastructure identities like `git@github.com`, `@users.noreply.github.com
|
|
400
|
+
| `leftblock-personal-email` | Personal email addresses (infrastructure identities like `git@github.com`, `@users.noreply.github.com`, no-reply addresses, `@example.*` placeholders, `@example.edu` and any address on a `.test`, `.example`, `.invalid` or `.localhost` domain are allowlisted) |
|
|
401
401
|
| `leftblock-home-path-posix` | Home-folder paths `/Users/<name>`, `/home/<name>` |
|
|
402
402
|
| `leftblock-home-path-windows` | `C:\Users\<name>` and `\Users\<name>` |
|
|
403
|
-
| `leftblock-session-uuid` | Potential session UUIDs
|
|
404
|
-
| `leftblock-actor-id` | `actor_id` / `user_id` / `author_id` JSON values |
|
|
403
|
+
| `leftblock-session-uuid` | Potential session UUIDs (not flagged in Kanbus board data; example values and the `.kanbus.yml` `project_id` line are also allowed) |
|
|
404
|
+
| `leftblock-actor-id` | `actor_id` / `user_id` / `author_id` JSON values (not flagged in Kanbus board data) |
|
|
405
405
|
| `leftblock-openai-key` | `sk-`, `sk-proj-`, `sk-svcacct-` OpenAI keys |
|
|
406
406
|
| `leftblock-anthropic-key` | `sk-ant-` Anthropic keys |
|
|
407
407
|
| `leftblock-eth-private-key` | Ethereum-style private keys (`0x` + 64 hex) |
|
|
@@ -409,7 +409,7 @@ PEM private keys, and other standard secret formats.
|
|
|
409
409
|
| `leftblock-bip39-mnemonic` | BIP-39 seed phrases (12–24 words) in seed/mnemonic/recovery context |
|
|
410
410
|
|
|
411
411
|
A blocked check prints a one-line hint after each rule's findings (once per
|
|
412
|
-
rule): use a role or placeholder
|
|
412
|
+
rule): use a role address or a reserved placeholder for personal emails; a relative path,
|
|
413
413
|
`$HOME` or `~` for home paths; drop or describe session and actor identifiers;
|
|
414
414
|
and for API, private and wallet keys, remove the value **and rotate the key**,
|
|
415
415
|
since it may already be exposed. Findings in Kanbus board data
|
|
@@ -417,18 +417,13 @@ since it may already be exposed. Findings in Kanbus board data
|
|
|
417
417
|
those files and that the content should be fixed through `kbs` (for example
|
|
418
418
|
`kbs update` or `kbs comment`) rather than by hand.
|
|
419
419
|
|
|
420
|
-
Kanbus
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
Provider session fields, nested actor values, prose and arbitrary arrays
|
|
428
|
-
remain scanned. A project prefix alone grants no exception, and a second
|
|
429
|
-
value on the same line cannot inherit one. These checks recognize syntax;
|
|
430
|
-
they do not validate a full JSON schema or prove an identifier is public.
|
|
431
|
-
Other secret rules still apply to values in the accepted fields.
|
|
420
|
+
Kanbus is the mandated task tracker, so identifiers in its board data (actor
|
|
421
|
+
ids, session UUIDs, and UUIDs in any field or prose) are not sensitive and never
|
|
422
|
+
block a `kbs` write. Board data means `project/issues/*.json`,
|
|
423
|
+
`project/events/*.json` and `project/.cache/index.json`; the exception is
|
|
424
|
+
limited to the session-UUID and actor-id rules, so personal emails, home paths
|
|
425
|
+
and API or wallet keys in those files are still detected, and the same content
|
|
426
|
+
in any other path is still flagged.
|
|
432
427
|
|
|
433
428
|
### Crypto wallet material: what is and is not flagged
|
|
434
429
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "leftblock"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.3"
|
|
8
8
|
description = "LeftBlock: a pluggable inspection gate for Git commits"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {file = "LICENSE"}
|
|
@@ -27,14 +27,14 @@ leftblock = "leftblock.cli:main"
|
|
|
27
27
|
version_toml = ["pyproject.toml:project.version"]
|
|
28
28
|
commit_message = "chore(release): {version}"
|
|
29
29
|
tag_format = "v{version}"
|
|
30
|
-
#
|
|
31
|
-
#
|
|
30
|
+
# scripts/release_prepare.py keeps the README pin on NEW_VERSION and drops
|
|
31
|
+
# commit trailers from the generated changelog; both files are committed.
|
|
32
32
|
build_command = """
|
|
33
|
-
|
|
33
|
+
python scripts/release_prepare.py
|
|
34
34
|
python -m pip install build
|
|
35
35
|
python -m build
|
|
36
36
|
"""
|
|
37
|
-
assets = ["README.md"]
|
|
37
|
+
assets = ["README.md", "CHANGELOG.md"]
|
|
38
38
|
|
|
39
39
|
[tool.semantic_release.branches.main]
|
|
40
40
|
match = "main"
|
|
@@ -10,7 +10,7 @@ from leftblock import bypass
|
|
|
10
10
|
from test_commit_gate import GITLEAKS, RepositoryFixture
|
|
11
11
|
|
|
12
12
|
MARKER = "gitleaks" + ":allow"
|
|
13
|
-
EMAIL = "info" + "@" + "public-role.
|
|
13
|
+
EMAIL = "info" + "@" + "public-role" + ".co"
|
|
14
14
|
REASON = "Reviewed: documentation example, not a real finding"
|
|
15
15
|
|
|
16
16
|
|
|
@@ -73,12 +73,15 @@ class KanbusIdTests(unittest.TestCase):
|
|
|
73
73
|
"notes.md": "see PPY-%s; session %s\n" % (_uuid(rng), _uuid(rng)),
|
|
74
74
|
}))
|
|
75
75
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
def test_session_value_with_prefix_passes_in_board_files_only(self):
|
|
77
|
+
doc = json.dumps({"session_id": "PPY-" + _uuid(_rng())}, indent=2)
|
|
78
|
+
self.assertEqual(
|
|
79
|
+
scan_and_rule_ids({"project/events/synthetic.json": doc}), [])
|
|
80
|
+
for path in ("src/synthetic.json", "project/other.json",
|
|
81
|
+
"project/events/sub/synthetic.json"):
|
|
82
|
+
with self.subTest(path=path):
|
|
83
|
+
self.assertIn("leftblock-session-uuid",
|
|
84
|
+
scan_and_rule_ids({path: doc}))
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
def _uuid4(rng):
|
|
@@ -24,7 +24,7 @@ class RuleHintTests(unittest.TestCase):
|
|
|
24
24
|
for rule in ("openai-key", "anthropic-key", "eth-private-key", "wif-key", "bip39-mnemonic"):
|
|
25
25
|
self.assertIn("rotate", cli.RULE_HINTS["leftblock-" + rule])
|
|
26
26
|
self.assertIn("relative path", cli.RULE_HINTS["leftblock-home-path-posix"])
|
|
27
|
-
self.assertIn("
|
|
27
|
+
self.assertIn("reserved placeholder", cli.RULE_HINTS["leftblock-personal-email"])
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
@unittest.skipUnless(GITLEAKS, "Gitleaks must be installed for actual scanner tests")
|
|
@@ -79,11 +79,19 @@ class KanbusBoardTests(RepositoryFixture):
|
|
|
79
79
|
self.assertIn("leftblock-home-path-posix", proc.stderr)
|
|
80
80
|
self.assertIn("Hint: Use a relative path, $HOME or ~ instead.", proc.stderr)
|
|
81
81
|
self.assertEqual(
|
|
82
|
-
proc.stderr.count("This file is written by Kanbus; fix the content
|
|
83
|
-
"(e.g. kbs update or comment), don't hand-edit it."), 1,
|
|
82
|
+
proc.stderr.count("This issue file is written by Kanbus; fix the content with kbs update"), 1,
|
|
84
83
|
)
|
|
85
84
|
self.assertNotIn(home_path, proc.stdout + proc.stderr)
|
|
86
85
|
|
|
86
|
+
def test_event_file_hint_says_delete_and_recreate_not_update(self):
|
|
87
|
+
self.write("project/events/2026-10-20T10-00-00.000Z__x.json",
|
|
88
|
+
'{\n "note": "see /' + 'Users/someone/notes"\n}\n')
|
|
89
|
+
proc = self.leftblock("check")
|
|
90
|
+
self.assertEqual(proc.returncode, 1)
|
|
91
|
+
self.assertIn("append-only history", proc.stderr)
|
|
92
|
+
self.assertIn("kbs delete <issue id>", proc.stderr)
|
|
93
|
+
self.assertNotIn("kbs update", proc.stderr)
|
|
94
|
+
|
|
87
95
|
def test_hint_is_printed_once_per_rule(self):
|
|
88
96
|
self.write("notes.md", "a /" + "Users/one/x\nb /" + "Users/two/y\n")
|
|
89
97
|
proc = self.leftblock("check")
|
|
@@ -12,8 +12,8 @@ from test_commit_gate import GITLEAKS, RepositoryFixture
|
|
|
12
12
|
|
|
13
13
|
# Flagged by the shipped personal-email rule; the rule allowlists only
|
|
14
14
|
# example.* and GitHub identities.
|
|
15
|
-
EMAIL = "info" + "@" + "public-role.
|
|
16
|
-
OTHER_EMAIL = "help" + "@" + "public-role.
|
|
15
|
+
EMAIL = "info" + "@" + "public-role" + ".co"
|
|
16
|
+
OTHER_EMAIL = "help" + "@" + "public-role" + ".co"
|
|
17
17
|
PAGE = "page.tsx"
|
|
18
18
|
REASON = "Public role address for the site"
|
|
19
19
|
|
|
@@ -100,7 +100,7 @@ class RecordedOverrideTests(RepositoryFixture):
|
|
|
100
100
|
detail = [l for l in proc.stderr.splitlines() if l.startswith(" ")]
|
|
101
101
|
self.assertEqual(detail, [
|
|
102
102
|
f" {PAGE}:2: leftblock-personal-email [id {ident}] (Personal email address)",
|
|
103
|
-
" Hint: Use a role or placeholder
|
|
103
|
+
" Hint: Use a role address or a reserved placeholder (example.com/.org/.net, or any .test, .example or .invalid domain), or describe it instead.",
|
|
104
104
|
])
|
|
105
105
|
self.assertIn(f"leftblock-personal-email in {PAGE}:2 [id {ident}]", proc.stderr.splitlines()[-1])
|
|
106
106
|
self.assertNotIn(EMAIL, proc.stdout + proc.stderr)
|
|
@@ -109,7 +109,7 @@ class SensitiveFixtureTests(unittest.TestCase):
|
|
|
109
109
|
# Synthetic personal values are assembled at test time so this
|
|
110
110
|
# test source is itself clean under the shipped ruleset.
|
|
111
111
|
user = "jd" + "oe"
|
|
112
|
-
email = "jane." + "doe@" + "example-corp
|
|
112
|
+
email = "jane." + "doe@" + "example-corp" + ".co"
|
|
113
113
|
findings = scan_and_rule_ids({
|
|
114
114
|
"issues/one.json": (
|
|
115
115
|
'{"assignee_email": "%s", "session_id": "%s",'
|
|
@@ -222,6 +222,34 @@ class SensitiveFixtureTests(unittest.TestCase):
|
|
|
222
222
|
class NegativeTests(unittest.TestCase):
|
|
223
223
|
"""Public wallet addresses and hash-like hex must NOT be flagged."""
|
|
224
224
|
|
|
225
|
+
def test_no_reply_addresses_pass_but_lookalikes_still_flag(self):
|
|
226
|
+
at = "@"
|
|
227
|
+
self.assertEqual(scan_and_rule_ids({"CHANGELOG.md": "\n".join([
|
|
228
|
+
"Co-Authored-By: Bot <noreply" + at + "vendor" + ".co" + ">",
|
|
229
|
+
"From: no-reply" + at + "vendor" + ".co",
|
|
230
|
+
]) + "\n"}), [])
|
|
231
|
+
self.assertEqual(scan_and_rule_ids({"notes.md": "mail jo" + at + "vendor" + ".co\n"}),
|
|
232
|
+
["leftblock-personal-email"])
|
|
233
|
+
self.assertEqual(scan_and_rule_ids({"notes.md": "mail notnoreply" + at + "vendor" + ".co\n"}),
|
|
234
|
+
["leftblock-personal-email"])
|
|
235
|
+
|
|
236
|
+
def test_reserved_placeholder_domains_pass_but_lookalikes_still_flag(self):
|
|
237
|
+
at = "@"
|
|
238
|
+
passing = [
|
|
239
|
+
"person" + at + "example.invalid",
|
|
240
|
+
"a" + at + "mail.acme.test",
|
|
241
|
+
"b" + at + "deep.sub.host.example",
|
|
242
|
+
"c" + at + "app.localhost",
|
|
243
|
+
"d" + at + "example.edu",
|
|
244
|
+
"e" + at + "x.test",
|
|
245
|
+
]
|
|
246
|
+
self.assertEqual(scan_and_rule_ids({"notes.md": "\n".join("mail " + a for a in passing) + "\n"}), [])
|
|
247
|
+
for domain in ["test" + ".com", "invalid" + ".org", "example.test" + ".co", "mytest" + ".dev",
|
|
248
|
+
"example" + ".edu.co", "notexample" + ".edu"]:
|
|
249
|
+
with self.subTest(domain=domain):
|
|
250
|
+
self.assertEqual(scan_and_rule_ids({"notes.md": "mail jo" + at + domain + "\n"}),
|
|
251
|
+
["leftblock-personal-email"])
|
|
252
|
+
|
|
225
253
|
def test_public_wallet_addresses_and_hashes_not_flagged(self):
|
|
226
254
|
rng = _rng()
|
|
227
255
|
findings = scan_and_rule_ids({
|
|
@@ -332,172 +360,148 @@ def _uuid(rng):
|
|
|
332
360
|
|
|
333
361
|
@unittest.skipUnless(GITLEAKS, "gitleaks not installed")
|
|
334
362
|
class KanbusBoardAllowlistTests(unittest.TestCase):
|
|
335
|
-
"""Kanbus board
|
|
363
|
+
"""Kanbus board data identifiers are not sensitive; nothing else changes.
|
|
364
|
+
|
|
365
|
+
Policy (2026-10-20): in board data, actor ids, session UUIDs and UUIDs in
|
|
366
|
+
any field or prose pass. Emails, home paths and keys are still detected
|
|
367
|
+
there, and the same content outside board data is still flagged.
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
BOARD_PATHS = (
|
|
371
|
+
"project/issues/PPY-x.json",
|
|
372
|
+
"project/events/2026-10-20T00-00-00.000Z__x.json",
|
|
373
|
+
"project/events/llm_usage.jsonl",
|
|
374
|
+
"project/.cache/index.json",
|
|
375
|
+
"sub/dir/project/issues/PPY-x.json",
|
|
376
|
+
"sub/dir/project/events/e.json",
|
|
377
|
+
"sub/dir/project/.cache/index.json",
|
|
378
|
+
)
|
|
379
|
+
NON_BOARD_PATHS = (
|
|
380
|
+
"notes.md",
|
|
381
|
+
"project/other.json",
|
|
382
|
+
"project/issues/sub/x.json",
|
|
383
|
+
"project/events/sub/x.json",
|
|
384
|
+
"project/events.json",
|
|
385
|
+
"project/issues/x.json.bak",
|
|
386
|
+
"project/issues/x.jsonl",
|
|
387
|
+
"src/project/issues-like/x.json",
|
|
388
|
+
"myproject/issues/x.json",
|
|
389
|
+
"project/.cache/other.json",
|
|
390
|
+
"project/.cache/sub/index.json",
|
|
391
|
+
)
|
|
336
392
|
|
|
337
393
|
def setUp(self):
|
|
338
394
|
self.rng = _rng()
|
|
339
395
|
|
|
340
|
-
def
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
"
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
json.dumps({"id": "PPY-" + value + " extra"}, indent=2),
|
|
402
|
-
json.dumps({"description": ['PPY-' + value]}, indent=2),
|
|
403
|
-
json.dumps({"actor_id": value, "session_id": _uuid(self.rng)}),
|
|
404
|
-
):
|
|
405
|
-
with self.subTest(doc_shape=len(doc)):
|
|
406
|
-
self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
|
|
407
|
-
"project/events/synthetic.json": doc,
|
|
408
|
-
}))
|
|
409
|
-
|
|
410
|
-
def test_all_structural_prefixes_still_flagged_outside_board_files(self):
|
|
411
|
-
for prefix in ("", "PPY-", "BOTSPY-", "kanbus-"):
|
|
412
|
-
with self.subTest(prefix=prefix):
|
|
413
|
-
doc = json.dumps({"id": prefix + _uuid(self.rng)}, indent=2)
|
|
414
|
-
self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
|
|
415
|
-
"src/state.json": doc,
|
|
416
|
-
}))
|
|
417
|
-
|
|
418
|
-
def test_actor_attribution_does_not_allow_other_sensitive_rules(self):
|
|
419
|
-
actor = "sk-proj-" + rand_openai_like(self.rng, 56)
|
|
420
|
-
doc = json.dumps({"actor_id": actor}, indent=2)
|
|
421
|
-
self.assertIn("leftblock-openai-key", scan_and_rule_ids({
|
|
422
|
-
"project/events/synthetic.json": doc,
|
|
423
|
-
}))
|
|
424
|
-
|
|
425
|
-
def test_nested_actor_and_split_line_actor_remain_detected(self):
|
|
426
|
-
nested = json.dumps({"payload": {"actor_id": _uuid(self.rng)}}, indent=2)
|
|
427
|
-
findings = scan_and_rule_ids({"project/events/synthetic.json": nested})
|
|
428
|
-
self.assertIn("leftblock-actor-id", findings)
|
|
429
|
-
self.assertIn("leftblock-session-uuid", findings)
|
|
430
|
-
split = '"actor_id"\n:\n"local' + 'user"'
|
|
431
|
-
self.assertIn("leftblock-actor-id", scan_and_rule_ids({"src/state.json": split}))
|
|
432
|
-
|
|
433
|
-
def test_example_or_cache_key_does_not_hide_another_uuid(self):
|
|
434
|
-
example = "-".join(("00000000", "0000", "4000", "8000", "0123456789ab"))
|
|
435
|
-
for first in (example, f"PPY-{_uuid(self.rng)}.json"):
|
|
436
|
-
line = first + " session=" + _uuid(self.rng)
|
|
437
|
-
self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
|
|
438
|
-
"project/events/synthetic.json": line,
|
|
439
|
-
}))
|
|
440
|
-
|
|
441
|
-
def test_canonical_crlf_event_passes(self):
|
|
442
|
-
event = json.dumps({
|
|
443
|
-
"event_id": _uuid(self.rng), "actor_id": _uuid(self.rng),
|
|
444
|
-
"issue_id": f"PPY-{_uuid(self.rng)}",
|
|
445
|
-
}, indent=2).replace("\n", "\r\n")
|
|
446
|
-
self.assertEqual(scan_and_rule_ids({"project/events/synthetic.json": event}), [])
|
|
447
|
-
|
|
448
|
-
def test_coordination_claim_reference_passes_only_in_event_payload(self):
|
|
449
|
-
claim = "claim-" + _uuid(self.rng)
|
|
450
|
-
event = json.dumps({"event_type": "coordination.claim",
|
|
451
|
-
"payload": {"claim_id": claim}}, indent=2)
|
|
452
|
-
self.assertEqual(scan_and_rule_ids({"project/events/synthetic.json": event}), [])
|
|
453
|
-
for path in ("project/issues/synthetic.json", "src/state.json"):
|
|
454
|
-
with self.subTest(path=path):
|
|
455
|
-
self.assertIn("leftblock-session-uuid", scan_and_rule_ids({path: event}))
|
|
456
|
-
for doc in (
|
|
457
|
-
json.dumps({"claim_id": claim}, indent=2),
|
|
458
|
-
json.dumps({"payload": {"nested": {"claim_id": claim}}}, indent=2),
|
|
459
|
-
json.dumps({"payload": {"claim_id": claim + " copied"}}, indent=2),
|
|
460
|
-
json.dumps({"payload": {"session_id": claim}}, indent=2),
|
|
461
|
-
json.dumps({"payload": {"claim_id": claim, "session_id": _uuid(self.rng)}}),
|
|
462
|
-
json.dumps({"payload": {"claim_id": _uuid(self.rng)}}, indent=2),
|
|
463
|
-
):
|
|
464
|
-
self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
|
|
465
|
-
"project/events/synthetic.json": doc,
|
|
466
|
-
}))
|
|
467
|
-
|
|
468
|
-
def test_cache_bookkeeping_passes_without_exempting_cached_content(self):
|
|
396
|
+
def _content(self):
|
|
397
|
+
"""Identifier-bearing content in many shapes (indented and minified)."""
|
|
398
|
+
u = lambda: _uuid(self.rng) # noqa: E731
|
|
399
|
+
actor_key = "actor" + "_id"
|
|
400
|
+
return {
|
|
401
|
+
"session_fields": json.dumps({
|
|
402
|
+
"session_id": u(), "provider_session_id": u(),
|
|
403
|
+
"resumed_session": u()}, indent=2),
|
|
404
|
+
"prose": json.dumps({
|
|
405
|
+
"description": "session " + u() + " reproduced it",
|
|
406
|
+
"text": "see PPY-" + u() + " and BOTSPY-" + u()}, indent=2),
|
|
407
|
+
"nested_actor": json.dumps(
|
|
408
|
+
{"payload": {actor_key: u()}}, indent=2),
|
|
409
|
+
"split_actor": '"%s"\n:\n"%s"' % (actor_key, u()),
|
|
410
|
+
"minified_event": json.dumps({
|
|
411
|
+
"event_id": u(), actor_key: u(), "session_id": u(),
|
|
412
|
+
"payload": {"comment_id": u(), "text": "x " + u()}}),
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
def test_identifiers_pass_in_every_board_path(self):
|
|
416
|
+
for path in self.BOARD_PATHS:
|
|
417
|
+
for shape, doc in self._content().items():
|
|
418
|
+
with self.subTest(path=path, shape=shape):
|
|
419
|
+
self.assertEqual(scan_and_rule_ids({path: doc}), [])
|
|
420
|
+
|
|
421
|
+
def test_session_style_fields_pass_with_any_prefix_in_board_files(self):
|
|
422
|
+
for path in self.BOARD_PATHS:
|
|
423
|
+
for field in ("session_id", "provider_session_id", "resumed_session"):
|
|
424
|
+
for prefix in ("", "PPY-", "BOTSPY-", "kanbus-"):
|
|
425
|
+
with self.subTest(path=path, field=field, prefix=prefix):
|
|
426
|
+
doc = json.dumps({field: prefix + _uuid(self.rng)}, indent=2)
|
|
427
|
+
self.assertEqual(scan_and_rule_ids({path: doc}), [])
|
|
428
|
+
|
|
429
|
+
def test_bare_and_prefixed_uuids_in_prose_pass_in_board_files(self):
|
|
430
|
+
for path in self.BOARD_PATHS:
|
|
431
|
+
for field in ("description", "text"):
|
|
432
|
+
for prefix in ("", "PPY-", "kanbus-"):
|
|
433
|
+
with self.subTest(path=path, field=field, prefix=prefix):
|
|
434
|
+
doc = json.dumps({
|
|
435
|
+
"id": "PPY-" + _uuid(self.rng),
|
|
436
|
+
field: "seen in %s%s today" % (prefix, _uuid(self.rng)),
|
|
437
|
+
}, indent=2)
|
|
438
|
+
self.assertEqual(scan_and_rule_ids({path: doc}), [])
|
|
439
|
+
|
|
440
|
+
def test_actor_ids_pass_nested_split_and_minified_in_board_files(self):
|
|
441
|
+
key = "actor" + "_id"
|
|
442
|
+
for value in (_uuid(self.rng), "local" + "user"):
|
|
443
|
+
shapes = {
|
|
444
|
+
"root": json.dumps({key: value}, indent=2),
|
|
445
|
+
"nested": json.dumps({"payload": {key: value}}, indent=2),
|
|
446
|
+
"split": '"%s"\n:\n"%s"' % (key, value),
|
|
447
|
+
"minified": json.dumps({"event_type": "x", key: value}),
|
|
448
|
+
"same_line_session": json.dumps(
|
|
449
|
+
{key: value, "session_id": _uuid(self.rng)}),
|
|
450
|
+
}
|
|
451
|
+
for path in self.BOARD_PATHS:
|
|
452
|
+
for shape, doc in shapes.items():
|
|
453
|
+
with self.subTest(path=path, shape=shape):
|
|
454
|
+
self.assertEqual(scan_and_rule_ids({path: doc}), [])
|
|
455
|
+
|
|
456
|
+
def test_canonical_crlf_event_and_cache_pass(self):
|
|
469
457
|
iid = "PPY-" + _uuid(self.rng)
|
|
470
|
-
|
|
458
|
+
event = json.dumps({
|
|
459
|
+
"event_id": _uuid(self.rng), "actor" + "_id": _uuid(self.rng),
|
|
460
|
+
"issue_id": iid}, indent=2).replace("\n", "\r\n")
|
|
461
|
+
self.assertEqual(scan_and_rule_ids({"project/events/e.json": event}), [])
|
|
462
|
+
cache = json.dumps({
|
|
471
463
|
"file_mtimes": {iid + ".json": 1792500000.25},
|
|
472
|
-
"issues": [{"id": iid, "
|
|
464
|
+
"issues": [{"id": iid, "description": "s " + _uuid(self.rng),
|
|
465
|
+
"session_id": _uuid(self.rng)}]}, indent=2)
|
|
466
|
+
self.assertEqual(scan_and_rule_ids({"project/.cache/index.json": cache}), [])
|
|
467
|
+
|
|
468
|
+
def test_same_identifiers_still_flagged_outside_board_paths(self):
|
|
469
|
+
for path in self.NON_BOARD_PATHS:
|
|
470
|
+
for shape, doc in self._content().items():
|
|
471
|
+
with self.subTest(path=path, shape=shape):
|
|
472
|
+
findings = scan_and_rule_ids({path: doc})
|
|
473
|
+
self.assertIn("leftblock-session-uuid", findings)
|
|
474
|
+
|
|
475
|
+
def test_actor_ids_still_flagged_outside_board_paths(self):
|
|
476
|
+
key = "actor" + "_id"
|
|
477
|
+
doc = json.dumps({key: "local" + "user"}, indent=2)
|
|
478
|
+
split = '"%s"\n:\n"%s"' % (key, "local" + "user")
|
|
479
|
+
for path in self.NON_BOARD_PATHS:
|
|
480
|
+
for shape, content in (("root", doc), ("split", split)):
|
|
481
|
+
with self.subTest(path=path, shape=shape):
|
|
482
|
+
self.assertIn("leftblock-actor-id",
|
|
483
|
+
scan_and_rule_ids({path: content}))
|
|
484
|
+
|
|
485
|
+
def test_personal_email_home_path_and_key_still_flagged_in_board_files(self):
|
|
486
|
+
email = "jane." + "doe@" + "example-corp" + ".co"
|
|
487
|
+
user = "jd" + "oe"
|
|
488
|
+
cases = {
|
|
489
|
+
"leftblock-personal-email": json.dumps(
|
|
490
|
+
{"assignee": email}, indent=2),
|
|
491
|
+
"leftblock-home-path-posix": json.dumps(
|
|
492
|
+
{"description": "see /Users/%s/work" % user}, indent=2),
|
|
493
|
+
"leftblock-openai-key": json.dumps(
|
|
494
|
+
{"text": "key sk-proj-" + rand_openai_like(self.rng, 56)},
|
|
495
|
+
indent=2),
|
|
473
496
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
for
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
path: json.dumps(cache, indent=2),
|
|
486
|
-
}))
|
|
487
|
-
del cache["issues"][0][field]
|
|
488
|
-
cache["issues"][0]["actor_id"] = "sk-proj-" + rand_openai_like(self.rng, 56)
|
|
489
|
-
self.assertIn("leftblock-openai-key", scan_and_rule_ids({
|
|
490
|
-
path: json.dumps(cache, indent=2),
|
|
491
|
-
}))
|
|
492
|
-
|
|
493
|
-
def test_cache_key_allowance_cannot_hide_another_uuid(self):
|
|
494
|
-
key = "PPY-" + _uuid(self.rng) + ".json"
|
|
495
|
-
value = _uuid(self.rng)
|
|
496
|
-
for line in (
|
|
497
|
-
f' "{key}": 123.5, "session_id": "{value}"',
|
|
498
|
-
f' "{key}": "{value}"',
|
|
499
|
-
f' "{key}": {{"session_id": "{value}"}}',
|
|
500
|
-
):
|
|
501
|
-
self.assertIn("leftblock-session-uuid", scan_and_rule_ids({
|
|
502
|
-
"project/.cache/index.json": line,
|
|
503
|
-
}))
|
|
497
|
+
for path in self.BOARD_PATHS:
|
|
498
|
+
for rule, doc in cases.items():
|
|
499
|
+
with self.subTest(path=path, rule=rule):
|
|
500
|
+
self.assertIn(rule, scan_and_rule_ids({path: doc}))
|
|
501
|
+
|
|
502
|
+
def test_key_in_actor_field_still_flagged_in_board_files(self):
|
|
503
|
+
doc = json.dumps({"actor" + "_id": "sk-proj-" + rand_openai_like(self.rng, 56)},
|
|
504
|
+
indent=2)
|
|
505
|
+
for path in self.BOARD_PATHS:
|
|
506
|
+
with self.subTest(path=path):
|
|
507
|
+
self.assertIn("leftblock-openai-key", scan_and_rule_ids({path: doc}))
|
|
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
|
|
File without changes
|
|
File without changes
|