secretshield 0.1.0__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.
- secretshield-0.1.0/LICENSE +21 -0
- secretshield-0.1.0/PKG-INFO +297 -0
- secretshield-0.1.0/README.md +271 -0
- secretshield-0.1.0/pyproject.toml +46 -0
- secretshield-0.1.0/secretshield/__init__.py +44 -0
- secretshield-0.1.0/secretshield/cli.py +168 -0
- secretshield-0.1.0/secretshield/config.py +57 -0
- secretshield-0.1.0/secretshield/detector.py +125 -0
- secretshield-0.1.0/secretshield/guardian.py +222 -0
- secretshield-0.1.0/secretshield/notifications.py +73 -0
- secretshield-0.1.0/secretshield/patterns.py +102 -0
- secretshield-0.1.0/secretshield/redactor.py +49 -0
- secretshield-0.1.0/secretshield.egg-info/PKG-INFO +297 -0
- secretshield-0.1.0/secretshield.egg-info/SOURCES.txt +21 -0
- secretshield-0.1.0/secretshield.egg-info/dependency_links.txt +1 -0
- secretshield-0.1.0/secretshield.egg-info/entry_points.txt +2 -0
- secretshield-0.1.0/secretshield.egg-info/requires.txt +3 -0
- secretshield-0.1.0/secretshield.egg-info/top_level.txt +1 -0
- secretshield-0.1.0/setup.cfg +4 -0
- secretshield-0.1.0/tests/test_detector.py +147 -0
- secretshield-0.1.0/tests/test_logging.py +99 -0
- secretshield-0.1.0/tests/test_redactor.py +81 -0
- secretshield-0.1.0/tests/test_stdout.py +118 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 secretshield contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: secretshield
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Detect and redact likely secrets before they reach Python's terminal output or logging system.
|
|
5
|
+
Author: secretshield contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Sam3360/secretshield
|
|
8
|
+
Project-URL: Repository, https://github.com/Sam3360/secretshield
|
|
9
|
+
Project-URL: Issues, https://github.com/Sam3360/secretshield/issues
|
|
10
|
+
Keywords: security,secrets,redaction,logging,stdout,credentials
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# secretshield
|
|
28
|
+
|
|
29
|
+
`secretshield` is a local Python security utility that detects likely
|
|
30
|
+
secrets (API keys, tokens, passwords, private keys, and other
|
|
31
|
+
credential-shaped strings) and redacts them **before** they are printed
|
|
32
|
+
through Python's terminal output (`stdout`/`stderr`) or the standard
|
|
33
|
+
`logging` module.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import secretshield
|
|
37
|
+
|
|
38
|
+
api_key = "sk-example1234567890abcdefFAKEKEY"
|
|
39
|
+
print("API key:", api_key)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
API key: ********
|
|
44
|
+
⚠ secretshield: Potential secret detected and redacted.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The real secret value never appears in the redacted output, in
|
|
48
|
+
secretshield's own warning messages, or in any exception it raises.
|
|
49
|
+
|
|
50
|
+
## Why it exists
|
|
51
|
+
|
|
52
|
+
Secrets end up in terminal output and logs more often than anyone
|
|
53
|
+
intends: a debug `print()` left in accidentally, a stack trace that
|
|
54
|
+
includes a config dict, a `logger.info()` call that dumps request
|
|
55
|
+
headers. `secretshield` is a small, dependency-free safety net for
|
|
56
|
+
exactly that class of mistake during local development and debugging.
|
|
57
|
+
|
|
58
|
+
It is **not** a replacement for secret management, code review, or
|
|
59
|
+
static-analysis security tooling — see [Limitations](#limitations) below.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install secretshield
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For local development, from a cloned copy of this repository:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install -e ".[dev]"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Requires Python 3.10 or newer. No third-party runtime dependencies.
|
|
74
|
+
|
|
75
|
+
## Basic usage
|
|
76
|
+
|
|
77
|
+
Protection for `sys.stdout`, `sys.stderr`, and `logging` is enabled the
|
|
78
|
+
moment you import the package:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import secretshield
|
|
82
|
+
|
|
83
|
+
password = "hunter2-example-not-real"
|
|
84
|
+
print("Using password:", password)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
Using password: ********
|
|
89
|
+
⚠ secretshield: Potential secret detected and redacted.
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
You can also toggle protection manually:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import secretshield
|
|
96
|
+
|
|
97
|
+
secretshield.disable() # protection off
|
|
98
|
+
secretshield.enable() # protection back on (idempotent, safe to call repeatedly)
|
|
99
|
+
secretshield.is_enabled()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Detecting or redacting text directly
|
|
103
|
+
|
|
104
|
+
You don't need to route text through stdout/logging to use the
|
|
105
|
+
detection and redaction logic:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from secretshield import detect, redact
|
|
109
|
+
|
|
110
|
+
matches = detect("aws_key=AKIAABCDEFGHIJKLMNOP")
|
|
111
|
+
# [Match(start=8, end=28, value='AKIA...', kind='aws_access_key_id')]
|
|
112
|
+
|
|
113
|
+
safe_text, was_redacted = redact("aws_key=AKIAABCDEFGHIJKLMNOP")
|
|
114
|
+
# ("aws_key=********", True)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Examples
|
|
118
|
+
|
|
119
|
+
See the [`examples/`](examples/) directory:
|
|
120
|
+
|
|
121
|
+
* [`examples/basic.py`](examples/basic.py) — a fake secret printed to
|
|
122
|
+
the terminal.
|
|
123
|
+
* [`examples/logging_demo.py`](examples/logging_demo.py) — a fake secret
|
|
124
|
+
logged via both `%s`-style arguments and an f-string.
|
|
125
|
+
|
|
126
|
+
Run either with:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python examples/basic.py
|
|
130
|
+
python examples/logging_demo.py
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## CLI
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
secretshield --help
|
|
137
|
+
secretshield --version
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `run` — execute a script with runtime protection
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
secretshield run app.py [args...]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Runs `app.py` as `__main__` with `sys.stdout`, `sys.stderr`, and
|
|
147
|
+
`logging` protected for the duration of the script's execution. This is
|
|
148
|
+
useful for wrapping an existing script without editing its source.
|
|
149
|
+
|
|
150
|
+
### `scan` — static file/directory scanning
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
secretshield scan .
|
|
154
|
+
secretshield scan path/to/file.py
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Scans a file, or recursively scans a directory of text-like files
|
|
158
|
+
(`.py`, `.txt`, `.md`, `.env`, `.yml`, `.json`, `.ini`, `.toml`, `.sh`,
|
|
159
|
+
`.js`, `.ts`, etc.), reporting the *kind* and *location* of any likely
|
|
160
|
+
secrets found. `scan` does **not** execute any code and does **not**
|
|
161
|
+
print the secret values themselves — only where they were found. It
|
|
162
|
+
exits with status `1` if anything was found, `0` otherwise, so it can be
|
|
163
|
+
used as a pre-commit or CI check.
|
|
164
|
+
|
|
165
|
+
**`scan` is static analysis; `run` (and the automatic protection on
|
|
166
|
+
import) is runtime redaction.** They are separate features: `scan`
|
|
167
|
+
looks at file contents on disk, `run`/import-time protection looks at
|
|
168
|
+
what a running program actually writes out.
|
|
169
|
+
|
|
170
|
+
## Configuration
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import secretshield
|
|
174
|
+
|
|
175
|
+
secretshield.configure(
|
|
176
|
+
enabled=True, # master on/off switch
|
|
177
|
+
redact_with="********", # placeholder used in place of a secret
|
|
178
|
+
entropy_threshold=4.2, # bits/char threshold for generic detection
|
|
179
|
+
notify=True, # print the "potential secret" warning
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Sensible defaults mean most projects need zero configuration.
|
|
184
|
+
|
|
185
|
+
## Detection methods
|
|
186
|
+
|
|
187
|
+
`secretshield` combines two strategies:
|
|
188
|
+
|
|
189
|
+
1. **Known-format pattern matching** — regexes tuned to the shape of
|
|
190
|
+
common credential formats: AWS access keys, GitHub tokens, OpenAI-style
|
|
191
|
+
keys, Slack tokens, Stripe keys, Google API keys, JWTs, bearer tokens,
|
|
192
|
+
PEM-style private-key blocks, and generic `key = value` pairs whose
|
|
193
|
+
label looks like `api_key`, `secret`, `token`, `password`, etc.
|
|
194
|
+
2. **Generic high-entropy detection** — a Shannon-entropy check over
|
|
195
|
+
long, non-dictionary-like character runs, used to catch random-looking
|
|
196
|
+
secrets that don't match a known format. This is intentionally used
|
|
197
|
+
as a *supplement*, not the primary mechanism, because entropy alone
|
|
198
|
+
produces far too many false positives on things like hashes, UUIDs,
|
|
199
|
+
and encoded binary data that aren't secrets.
|
|
200
|
+
|
|
201
|
+
## Architecture
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
secretshield/
|
|
205
|
+
├── patterns.py # regexes for known secret formats
|
|
206
|
+
├── detector.py # detect(): pattern + entropy matching -> Match objects
|
|
207
|
+
├── redactor.py # redact(): turns Match spans into "********"
|
|
208
|
+
├── config.py # configure()/get_config(): runtime settings
|
|
209
|
+
├── notifications.py # safe, secret-free console/desktop warnings
|
|
210
|
+
├── guardian.py # stdout/stderr wrapping + logging record-factory hook
|
|
211
|
+
└── cli.py # `secretshield` command-line entry point
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Key design points:
|
|
215
|
+
|
|
216
|
+
* **Stream wrapping**, not monkey-patching `print`: `sys.stdout` and
|
|
217
|
+
`sys.stderr` are replaced with a thin wrapper object that redacts on
|
|
218
|
+
`write()` and delegates everything else (`flush`, `isatty`, attribute
|
|
219
|
+
access) to the original stream.
|
|
220
|
+
* **Logging protection** hooks `logging.setLogRecordFactory`, not a
|
|
221
|
+
`Filter` on the root logger. Filters attached to the root logger are
|
|
222
|
+
only consulted by the logger that originated a given call, so a
|
|
223
|
+
root-only filter would miss records from `logging.getLogger(__name__)`
|
|
224
|
+
child loggers. The record factory is invoked for every `LogRecord`
|
|
225
|
+
created anywhere in the process, so both `record.msg` (f-strings /
|
|
226
|
+
pre-formatted messages) and `record.args` (`%s`-style lazy arguments)
|
|
227
|
+
are reliably covered regardless of logger hierarchy.
|
|
228
|
+
* **Re-entrancy guards** prevent secretshield's own warning output from
|
|
229
|
+
being fed back into detection/logging and causing recursive loops.
|
|
230
|
+
* Detection and redaction failures are caught and swallowed — a bug in
|
|
231
|
+
secretshield should never crash or block the host application's
|
|
232
|
+
normal output.
|
|
233
|
+
|
|
234
|
+
## Testing
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
pip install -e ".[dev]"
|
|
238
|
+
pytest
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The test suite covers known-token detection, entropy detection, false
|
|
242
|
+
positives, single/multiple/repeated secrets, multiline text, stdout,
|
|
243
|
+
stderr, logging (`%s` args and f-strings), enable/disable idempotency,
|
|
244
|
+
and stream restoration. All secrets used in tests and examples are fake.
|
|
245
|
+
|
|
246
|
+
## Limitations
|
|
247
|
+
|
|
248
|
+
`secretshield` protects **Python's own `stdout`, `stderr`, and `logging`
|
|
249
|
+
output within the current process.** It is a helpful safety net, not a
|
|
250
|
+
comprehensive security boundary. Specifically, it does **not**:
|
|
251
|
+
|
|
252
|
+
* Prevent secrets from appearing in **screenshots** or screen recordings.
|
|
253
|
+
* Prevent **clipboard** leaks.
|
|
254
|
+
* Prevent secrets written via **arbitrary file writes** (e.g. `open(...).write(...)`,
|
|
255
|
+
`json.dump`, writing to a database).
|
|
256
|
+
* Protect **other applications** or processes outside this Python
|
|
257
|
+
interpreter.
|
|
258
|
+
* Redact output from **arbitrary subprocesses** — only output written
|
|
259
|
+
through this process's own `sys.stdout`/`sys.stderr`/`logging` is
|
|
260
|
+
covered, not everything a spawned subprocess itself prints to its own
|
|
261
|
+
inherited file descriptors before Python sees it.
|
|
262
|
+
* Prevent **network leaks** (secrets sent over HTTP, sockets, etc.).
|
|
263
|
+
* Catch **every possible way** a secret can leave a computer. Detection
|
|
264
|
+
is pattern- and entropy-based and can miss unusual or obfuscated
|
|
265
|
+
formats, and can occasionally over- or under-match.
|
|
266
|
+
|
|
267
|
+
Treat `secretshield` as a defense-in-depth safety net for accidental
|
|
268
|
+
local exposure during development and debugging — not as a substitute
|
|
269
|
+
for proper secret management (vaults, environment isolation, `.gitignore`
|
|
270
|
+
discipline, secret scanning in CI, least-privilege credentials, etc.).
|
|
271
|
+
|
|
272
|
+
## Security considerations
|
|
273
|
+
|
|
274
|
+
* secretshield performs **no network calls** and collects **no
|
|
275
|
+
telemetry**. All detection and redaction happens locally, in-process.
|
|
276
|
+
* Desktop notifications (if you wire up your own backend beyond the
|
|
277
|
+
built-in best-effort `notify-send`/`osascript` calls) are optional and
|
|
278
|
+
fail silently if unavailable — they never crash the host application.
|
|
279
|
+
* Because detection is heuristic, it can produce false negatives (a real
|
|
280
|
+
secret slips through) or false positives (harmless text gets redacted).
|
|
281
|
+
Tune `entropy_threshold` and, where needed, extend `patterns.py` for
|
|
282
|
+
your own credential formats.
|
|
283
|
+
|
|
284
|
+
## Contributing
|
|
285
|
+
|
|
286
|
+
Issues and pull requests are welcome. Please:
|
|
287
|
+
|
|
288
|
+
1. Add tests for any new detection pattern or behavior change.
|
|
289
|
+
2. Use only fake/example credentials in tests, examples, and docs —
|
|
290
|
+
never real secrets.
|
|
291
|
+
3. Keep the standard-library-only dependency policy unless there's a
|
|
292
|
+
strong reason to add a dependency, and discuss it in an issue first.
|
|
293
|
+
4. Run `pytest` before opening a PR.
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# secretshield
|
|
2
|
+
|
|
3
|
+
`secretshield` is a local Python security utility that detects likely
|
|
4
|
+
secrets (API keys, tokens, passwords, private keys, and other
|
|
5
|
+
credential-shaped strings) and redacts them **before** they are printed
|
|
6
|
+
through Python's terminal output (`stdout`/`stderr`) or the standard
|
|
7
|
+
`logging` module.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import secretshield
|
|
11
|
+
|
|
12
|
+
api_key = "sk-example1234567890abcdefFAKEKEY"
|
|
13
|
+
print("API key:", api_key)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
API key: ********
|
|
18
|
+
⚠ secretshield: Potential secret detected and redacted.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The real secret value never appears in the redacted output, in
|
|
22
|
+
secretshield's own warning messages, or in any exception it raises.
|
|
23
|
+
|
|
24
|
+
## Why it exists
|
|
25
|
+
|
|
26
|
+
Secrets end up in terminal output and logs more often than anyone
|
|
27
|
+
intends: a debug `print()` left in accidentally, a stack trace that
|
|
28
|
+
includes a config dict, a `logger.info()` call that dumps request
|
|
29
|
+
headers. `secretshield` is a small, dependency-free safety net for
|
|
30
|
+
exactly that class of mistake during local development and debugging.
|
|
31
|
+
|
|
32
|
+
It is **not** a replacement for secret management, code review, or
|
|
33
|
+
static-analysis security tooling — see [Limitations](#limitations) below.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install secretshield
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For local development, from a cloned copy of this repository:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install -e ".[dev]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Requires Python 3.10 or newer. No third-party runtime dependencies.
|
|
48
|
+
|
|
49
|
+
## Basic usage
|
|
50
|
+
|
|
51
|
+
Protection for `sys.stdout`, `sys.stderr`, and `logging` is enabled the
|
|
52
|
+
moment you import the package:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import secretshield
|
|
56
|
+
|
|
57
|
+
password = "hunter2-example-not-real"
|
|
58
|
+
print("Using password:", password)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
Using password: ********
|
|
63
|
+
⚠ secretshield: Potential secret detected and redacted.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
You can also toggle protection manually:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import secretshield
|
|
70
|
+
|
|
71
|
+
secretshield.disable() # protection off
|
|
72
|
+
secretshield.enable() # protection back on (idempotent, safe to call repeatedly)
|
|
73
|
+
secretshield.is_enabled()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Detecting or redacting text directly
|
|
77
|
+
|
|
78
|
+
You don't need to route text through stdout/logging to use the
|
|
79
|
+
detection and redaction logic:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from secretshield import detect, redact
|
|
83
|
+
|
|
84
|
+
matches = detect("aws_key=AKIAABCDEFGHIJKLMNOP")
|
|
85
|
+
# [Match(start=8, end=28, value='AKIA...', kind='aws_access_key_id')]
|
|
86
|
+
|
|
87
|
+
safe_text, was_redacted = redact("aws_key=AKIAABCDEFGHIJKLMNOP")
|
|
88
|
+
# ("aws_key=********", True)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Examples
|
|
92
|
+
|
|
93
|
+
See the [`examples/`](examples/) directory:
|
|
94
|
+
|
|
95
|
+
* [`examples/basic.py`](examples/basic.py) — a fake secret printed to
|
|
96
|
+
the terminal.
|
|
97
|
+
* [`examples/logging_demo.py`](examples/logging_demo.py) — a fake secret
|
|
98
|
+
logged via both `%s`-style arguments and an f-string.
|
|
99
|
+
|
|
100
|
+
Run either with:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
python examples/basic.py
|
|
104
|
+
python examples/logging_demo.py
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## CLI
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
secretshield --help
|
|
111
|
+
secretshield --version
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `run` — execute a script with runtime protection
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
secretshield run app.py [args...]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Runs `app.py` as `__main__` with `sys.stdout`, `sys.stderr`, and
|
|
121
|
+
`logging` protected for the duration of the script's execution. This is
|
|
122
|
+
useful for wrapping an existing script without editing its source.
|
|
123
|
+
|
|
124
|
+
### `scan` — static file/directory scanning
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
secretshield scan .
|
|
128
|
+
secretshield scan path/to/file.py
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Scans a file, or recursively scans a directory of text-like files
|
|
132
|
+
(`.py`, `.txt`, `.md`, `.env`, `.yml`, `.json`, `.ini`, `.toml`, `.sh`,
|
|
133
|
+
`.js`, `.ts`, etc.), reporting the *kind* and *location* of any likely
|
|
134
|
+
secrets found. `scan` does **not** execute any code and does **not**
|
|
135
|
+
print the secret values themselves — only where they were found. It
|
|
136
|
+
exits with status `1` if anything was found, `0` otherwise, so it can be
|
|
137
|
+
used as a pre-commit or CI check.
|
|
138
|
+
|
|
139
|
+
**`scan` is static analysis; `run` (and the automatic protection on
|
|
140
|
+
import) is runtime redaction.** They are separate features: `scan`
|
|
141
|
+
looks at file contents on disk, `run`/import-time protection looks at
|
|
142
|
+
what a running program actually writes out.
|
|
143
|
+
|
|
144
|
+
## Configuration
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
import secretshield
|
|
148
|
+
|
|
149
|
+
secretshield.configure(
|
|
150
|
+
enabled=True, # master on/off switch
|
|
151
|
+
redact_with="********", # placeholder used in place of a secret
|
|
152
|
+
entropy_threshold=4.2, # bits/char threshold for generic detection
|
|
153
|
+
notify=True, # print the "potential secret" warning
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Sensible defaults mean most projects need zero configuration.
|
|
158
|
+
|
|
159
|
+
## Detection methods
|
|
160
|
+
|
|
161
|
+
`secretshield` combines two strategies:
|
|
162
|
+
|
|
163
|
+
1. **Known-format pattern matching** — regexes tuned to the shape of
|
|
164
|
+
common credential formats: AWS access keys, GitHub tokens, OpenAI-style
|
|
165
|
+
keys, Slack tokens, Stripe keys, Google API keys, JWTs, bearer tokens,
|
|
166
|
+
PEM-style private-key blocks, and generic `key = value` pairs whose
|
|
167
|
+
label looks like `api_key`, `secret`, `token`, `password`, etc.
|
|
168
|
+
2. **Generic high-entropy detection** — a Shannon-entropy check over
|
|
169
|
+
long, non-dictionary-like character runs, used to catch random-looking
|
|
170
|
+
secrets that don't match a known format. This is intentionally used
|
|
171
|
+
as a *supplement*, not the primary mechanism, because entropy alone
|
|
172
|
+
produces far too many false positives on things like hashes, UUIDs,
|
|
173
|
+
and encoded binary data that aren't secrets.
|
|
174
|
+
|
|
175
|
+
## Architecture
|
|
176
|
+
|
|
177
|
+
```text
|
|
178
|
+
secretshield/
|
|
179
|
+
├── patterns.py # regexes for known secret formats
|
|
180
|
+
├── detector.py # detect(): pattern + entropy matching -> Match objects
|
|
181
|
+
├── redactor.py # redact(): turns Match spans into "********"
|
|
182
|
+
├── config.py # configure()/get_config(): runtime settings
|
|
183
|
+
├── notifications.py # safe, secret-free console/desktop warnings
|
|
184
|
+
├── guardian.py # stdout/stderr wrapping + logging record-factory hook
|
|
185
|
+
└── cli.py # `secretshield` command-line entry point
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Key design points:
|
|
189
|
+
|
|
190
|
+
* **Stream wrapping**, not monkey-patching `print`: `sys.stdout` and
|
|
191
|
+
`sys.stderr` are replaced with a thin wrapper object that redacts on
|
|
192
|
+
`write()` and delegates everything else (`flush`, `isatty`, attribute
|
|
193
|
+
access) to the original stream.
|
|
194
|
+
* **Logging protection** hooks `logging.setLogRecordFactory`, not a
|
|
195
|
+
`Filter` on the root logger. Filters attached to the root logger are
|
|
196
|
+
only consulted by the logger that originated a given call, so a
|
|
197
|
+
root-only filter would miss records from `logging.getLogger(__name__)`
|
|
198
|
+
child loggers. The record factory is invoked for every `LogRecord`
|
|
199
|
+
created anywhere in the process, so both `record.msg` (f-strings /
|
|
200
|
+
pre-formatted messages) and `record.args` (`%s`-style lazy arguments)
|
|
201
|
+
are reliably covered regardless of logger hierarchy.
|
|
202
|
+
* **Re-entrancy guards** prevent secretshield's own warning output from
|
|
203
|
+
being fed back into detection/logging and causing recursive loops.
|
|
204
|
+
* Detection and redaction failures are caught and swallowed — a bug in
|
|
205
|
+
secretshield should never crash or block the host application's
|
|
206
|
+
normal output.
|
|
207
|
+
|
|
208
|
+
## Testing
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pip install -e ".[dev]"
|
|
212
|
+
pytest
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The test suite covers known-token detection, entropy detection, false
|
|
216
|
+
positives, single/multiple/repeated secrets, multiline text, stdout,
|
|
217
|
+
stderr, logging (`%s` args and f-strings), enable/disable idempotency,
|
|
218
|
+
and stream restoration. All secrets used in tests and examples are fake.
|
|
219
|
+
|
|
220
|
+
## Limitations
|
|
221
|
+
|
|
222
|
+
`secretshield` protects **Python's own `stdout`, `stderr`, and `logging`
|
|
223
|
+
output within the current process.** It is a helpful safety net, not a
|
|
224
|
+
comprehensive security boundary. Specifically, it does **not**:
|
|
225
|
+
|
|
226
|
+
* Prevent secrets from appearing in **screenshots** or screen recordings.
|
|
227
|
+
* Prevent **clipboard** leaks.
|
|
228
|
+
* Prevent secrets written via **arbitrary file writes** (e.g. `open(...).write(...)`,
|
|
229
|
+
`json.dump`, writing to a database).
|
|
230
|
+
* Protect **other applications** or processes outside this Python
|
|
231
|
+
interpreter.
|
|
232
|
+
* Redact output from **arbitrary subprocesses** — only output written
|
|
233
|
+
through this process's own `sys.stdout`/`sys.stderr`/`logging` is
|
|
234
|
+
covered, not everything a spawned subprocess itself prints to its own
|
|
235
|
+
inherited file descriptors before Python sees it.
|
|
236
|
+
* Prevent **network leaks** (secrets sent over HTTP, sockets, etc.).
|
|
237
|
+
* Catch **every possible way** a secret can leave a computer. Detection
|
|
238
|
+
is pattern- and entropy-based and can miss unusual or obfuscated
|
|
239
|
+
formats, and can occasionally over- or under-match.
|
|
240
|
+
|
|
241
|
+
Treat `secretshield` as a defense-in-depth safety net for accidental
|
|
242
|
+
local exposure during development and debugging — not as a substitute
|
|
243
|
+
for proper secret management (vaults, environment isolation, `.gitignore`
|
|
244
|
+
discipline, secret scanning in CI, least-privilege credentials, etc.).
|
|
245
|
+
|
|
246
|
+
## Security considerations
|
|
247
|
+
|
|
248
|
+
* secretshield performs **no network calls** and collects **no
|
|
249
|
+
telemetry**. All detection and redaction happens locally, in-process.
|
|
250
|
+
* Desktop notifications (if you wire up your own backend beyond the
|
|
251
|
+
built-in best-effort `notify-send`/`osascript` calls) are optional and
|
|
252
|
+
fail silently if unavailable — they never crash the host application.
|
|
253
|
+
* Because detection is heuristic, it can produce false negatives (a real
|
|
254
|
+
secret slips through) or false positives (harmless text gets redacted).
|
|
255
|
+
Tune `entropy_threshold` and, where needed, extend `patterns.py` for
|
|
256
|
+
your own credential formats.
|
|
257
|
+
|
|
258
|
+
## Contributing
|
|
259
|
+
|
|
260
|
+
Issues and pull requests are welcome. Please:
|
|
261
|
+
|
|
262
|
+
1. Add tests for any new detection pattern or behavior change.
|
|
263
|
+
2. Use only fake/example credentials in tests, examples, and docs —
|
|
264
|
+
never real secrets.
|
|
265
|
+
3. Keep the standard-library-only dependency policy unless there's a
|
|
266
|
+
strong reason to add a dependency, and discuss it in an issue first.
|
|
267
|
+
4. Run `pytest` before opening a PR.
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "secretshield"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Detect and redact likely secrets before they reach Python's terminal output or logging system."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "secretshield contributors" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["security", "secrets", "redaction", "logging", "stdout", "credentials"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Security",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=7.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/Sam3360/secretshield"
|
|
36
|
+
Repository = "https://github.com/Sam3360/secretshield"
|
|
37
|
+
Issues = "https://github.com/Sam3360/secretshield/issues"
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
secretshield = "secretshield.cli:main"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
include = ["secretshield*"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
secretshield: detect and redact likely secrets before they reach
|
|
3
|
+
Python's terminal output or logging system.
|
|
4
|
+
|
|
5
|
+
Importing this package automatically enables protection for
|
|
6
|
+
``sys.stdout``, ``sys.stderr``, and the standard ``logging`` module::
|
|
7
|
+
|
|
8
|
+
import secretshield
|
|
9
|
+
|
|
10
|
+
api_key = "example-secret-value"
|
|
11
|
+
print("API key:", api_key)
|
|
12
|
+
# API key: ********
|
|
13
|
+
# \u26a0 secretshield: Potential secret detected and redacted.
|
|
14
|
+
|
|
15
|
+
Protection can be toggled manually with :func:`enable` / :func:`disable`,
|
|
16
|
+
and behavior can be tuned with :func:`configure`.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .config import Config, configure, get_config, reset_config
|
|
22
|
+
from .detector import Match, detect
|
|
23
|
+
from .guardian import disable, enable, is_enabled
|
|
24
|
+
from .redactor import redact
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"__version__",
|
|
30
|
+
"enable",
|
|
31
|
+
"disable",
|
|
32
|
+
"is_enabled",
|
|
33
|
+
"configure",
|
|
34
|
+
"get_config",
|
|
35
|
+
"reset_config",
|
|
36
|
+
"Config",
|
|
37
|
+
"detect",
|
|
38
|
+
"redact",
|
|
39
|
+
"Match",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Automatically protect stdout/stderr/logging as soon as secretshield is
|
|
43
|
+
# imported, per the tool's core promise: "import it and you're protected."
|
|
44
|
+
enable()
|