secretshield 0.4.2__tar.gz → 0.5.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.4.2/secretshield.egg-info → secretshield-0.5.0}/PKG-INFO +18 -3
- {secretshield-0.4.2 → secretshield-0.5.0}/README.md +17 -2
- {secretshield-0.4.2 → secretshield-0.5.0}/pyproject.toml +1 -1
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/__init__.py +1 -1
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/cli.py +3 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/guardian.py +24 -25
- secretshield-0.5.0/secretshield/ignorefile.py +42 -0
- {secretshield-0.4.2 → secretshield-0.5.0/secretshield.egg-info}/PKG-INFO +18 -3
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield.egg-info/SOURCES.txt +3 -0
- secretshield-0.5.0/tests/test_ignorefile.py +44 -0
- secretshield-0.5.0/tests/test_ignorefile_cli_integration.py +62 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_stdout.py +56 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/LICENSE +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/autofix/__init__.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/autofix/env.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/autofix/fixer.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/autofix/gitignore.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/autofix/python.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/baseline.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/config.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/detector.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/git/__init__.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/git/hooks.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/github/__init__.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/github/actions.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/notifications.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/patterns.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/project_config.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield/redactor.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield.egg-info/dependency_links.txt +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield.egg-info/entry_points.txt +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield.egg-info/requires.txt +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/secretshield.egg-info/top_level.txt +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/setup.cfg +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_autofix.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_baseline.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_detector.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_git_hooks.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_github_action.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_logging.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_project_config.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_redactor.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_scan.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_ss_alias_and_creator.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_v030_cli_integration.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.0}/tests/test_v040_cli_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: secretshield
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Stop secrets before they leak -- runtime redaction, multi-language scanning, auto-fix, Git hooks, and CI, all in one CLI.
|
|
5
5
|
Author: Samarth Chugh (Sam3360)
|
|
6
6
|
License-Expression: MIT
|
|
@@ -33,7 +33,7 @@ Dynamic: license-file
|
|
|
33
33
|
|
|
34
34
|
[](https://pypi.org/project/secretshield/)
|
|
35
35
|
[](https://pypi.org/project/secretshield/)
|
|
36
|
-
[](https://pypi.org/project/secretshield/)
|
|
37
37
|
[](LICENSE)
|
|
38
38
|
|
|
39
39
|
[Install](#installation) · [Demo](#demo) · [Quick start](#quick-start) · [Features](#what-secretshield-does) · [Docs below](#cli-reference)
|
|
@@ -272,6 +272,20 @@ format = "text"
|
|
|
272
272
|
|
|
273
273
|
CLI flags always override the file.
|
|
274
274
|
|
|
275
|
+
Prefer a plain `.gitignore`-style file instead of editing TOML? Drop a
|
|
276
|
+
`.secretshieldignore` in the project root — one glob pattern per line,
|
|
277
|
+
`#` comments and blank lines ignored, same idea as `.gitignore`:
|
|
278
|
+
|
|
279
|
+
```text
|
|
280
|
+
# .secretshieldignore
|
|
281
|
+
vendor/
|
|
282
|
+
*.min.js
|
|
283
|
+
tests/fixtures/
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Both `secretshield.toml`'s `[scan.ignore]` and `.secretshieldignore`
|
|
287
|
+
can be used together — their patterns combine.
|
|
288
|
+
|
|
275
289
|
## Runtime configuration
|
|
276
290
|
|
|
277
291
|
```python
|
|
@@ -304,7 +318,7 @@ secretshield/
|
|
|
304
318
|
├── patterns.py, detector.py, redactor.py # detection & redaction engine
|
|
305
319
|
├── guardian.py # stdout/stderr + logging protection
|
|
306
320
|
├── config.py, notifications.py # runtime settings & safe warnings
|
|
307
|
-
├── project_config.py, baseline.py
|
|
321
|
+
├── project_config.py, baseline.py, ignorefile.py # secretshield.toml, --baseline, .secretshieldignore
|
|
308
322
|
├── cli.py # command-line interface
|
|
309
323
|
├── autofix/ # interactive scan --fix
|
|
310
324
|
├── git/ # pre-commit hook
|
|
@@ -357,3 +371,4 @@ If you find this project useful, consider [supporting its development through Gi
|
|
|
357
371
|
## License
|
|
358
372
|
|
|
359
373
|
MIT — see [LICENSE](LICENSE).
|
|
374
|
+
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
[](https://pypi.org/project/secretshield/)
|
|
9
9
|
[](https://pypi.org/project/secretshield/)
|
|
10
|
-
[](https://pypi.org/project/secretshield/)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
|
|
13
13
|
[Install](#installation) · [Demo](#demo) · [Quick start](#quick-start) · [Features](#what-secretshield-does) · [Docs below](#cli-reference)
|
|
@@ -246,6 +246,20 @@ format = "text"
|
|
|
246
246
|
|
|
247
247
|
CLI flags always override the file.
|
|
248
248
|
|
|
249
|
+
Prefer a plain `.gitignore`-style file instead of editing TOML? Drop a
|
|
250
|
+
`.secretshieldignore` in the project root — one glob pattern per line,
|
|
251
|
+
`#` comments and blank lines ignored, same idea as `.gitignore`:
|
|
252
|
+
|
|
253
|
+
```text
|
|
254
|
+
# .secretshieldignore
|
|
255
|
+
vendor/
|
|
256
|
+
*.min.js
|
|
257
|
+
tests/fixtures/
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Both `secretshield.toml`'s `[scan.ignore]` and `.secretshieldignore`
|
|
261
|
+
can be used together — their patterns combine.
|
|
262
|
+
|
|
249
263
|
## Runtime configuration
|
|
250
264
|
|
|
251
265
|
```python
|
|
@@ -278,7 +292,7 @@ secretshield/
|
|
|
278
292
|
├── patterns.py, detector.py, redactor.py # detection & redaction engine
|
|
279
293
|
├── guardian.py # stdout/stderr + logging protection
|
|
280
294
|
├── config.py, notifications.py # runtime settings & safe warnings
|
|
281
|
-
├── project_config.py, baseline.py
|
|
295
|
+
├── project_config.py, baseline.py, ignorefile.py # secretshield.toml, --baseline, .secretshieldignore
|
|
282
296
|
├── cli.py # command-line interface
|
|
283
297
|
├── autofix/ # interactive scan --fix
|
|
284
298
|
├── git/ # pre-commit hook
|
|
@@ -331,3 +345,4 @@ If you find this project useful, consider [supporting its development through Gi
|
|
|
331
345
|
## License
|
|
332
346
|
|
|
333
347
|
MIT — see [LICENSE](LICENSE).
|
|
348
|
+
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "secretshield"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Stop secrets before they leak -- runtime redaction, multi-language scanning, auto-fix, Git hooks, and CI, all in one CLI."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -423,6 +423,9 @@ def _run_scan(args: argparse.Namespace) -> int:
|
|
|
423
423
|
# A trailing "/" means "this directory and everything in it" --
|
|
424
424
|
# translate that into a glob so it matches nested files too.
|
|
425
425
|
exclude_patterns.append(raw if not raw.endswith("/") else raw + "*")
|
|
426
|
+
from .ignorefile import load_ignore_patterns
|
|
427
|
+
|
|
428
|
+
exclude_patterns.extend(load_ignore_patterns(Path.cwd()))
|
|
426
429
|
|
|
427
430
|
json_output = bool(args.json) or (project_cfg.output_format == "json")
|
|
428
431
|
|
|
@@ -17,13 +17,12 @@ from .redactor import redact
|
|
|
17
17
|
# enable() calls don't stack wrappers on top of each other.
|
|
18
18
|
_WRAPPED_MARKER = "_secretshield_wrapped"
|
|
19
19
|
|
|
20
|
-
# Module-level state tracking whether protection
|
|
21
|
-
#
|
|
22
|
-
# disable()
|
|
20
|
+
# Module-level state tracking whether protection has been explicitly
|
|
21
|
+
# enabled (used by is_enabled()). The actual wrap/unwrap decision on
|
|
22
|
+
# each enable()/disable() call is made by inspecting sys.stdout/
|
|
23
|
+
# sys.stderr directly (see below) rather than trusting this flag alone.
|
|
23
24
|
_state: dict[str, Any] = {
|
|
24
25
|
"active": False,
|
|
25
|
-
"orig_stdout": None,
|
|
26
|
-
"orig_stderr": None,
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
# Guard against re-entrant redaction triggered by secretshield's own
|
|
@@ -235,13 +234,16 @@ def enable() -> None:
|
|
|
235
234
|
Enable secretshield protection for ``sys.stdout``, ``sys.stderr``, and
|
|
236
235
|
the standard ``logging`` module. Safe to call multiple times; repeated
|
|
237
236
|
calls do not create duplicate wrappers.
|
|
238
|
-
"""
|
|
239
|
-
if _state["active"]:
|
|
240
|
-
return
|
|
241
|
-
|
|
242
|
-
_state["orig_stdout"] = sys.stdout
|
|
243
|
-
_state["orig_stderr"] = sys.stderr
|
|
244
237
|
|
|
238
|
+
Always (re)wraps whatever ``sys.stdout``/``sys.stderr`` currently
|
|
239
|
+
are, rather than relying solely on a single global "already active"
|
|
240
|
+
flag to skip the work. That matters if something else reassigns
|
|
241
|
+
those streams between calls -- a test framework's output capture,
|
|
242
|
+
a user's own redirection, etc. -- so a later ``enable()`` correctly
|
|
243
|
+
protects the stream actually in use *now*, instead of silently
|
|
244
|
+
doing nothing because protection was already on for some earlier,
|
|
245
|
+
now-replaced stream.
|
|
246
|
+
"""
|
|
245
247
|
sys.stdout = _wrap_stream(sys.stdout)
|
|
246
248
|
sys.stderr = _wrap_stream(sys.stderr)
|
|
247
249
|
|
|
@@ -254,29 +256,26 @@ def disable() -> None:
|
|
|
254
256
|
"""
|
|
255
257
|
Disable secretshield protection, restoring the original ``sys.stdout``
|
|
256
258
|
and ``sys.stderr`` streams and removing the logging filter.
|
|
257
|
-
"""
|
|
258
|
-
if not _state["active"]:
|
|
259
|
-
return
|
|
260
259
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
260
|
+
Unwraps whatever ``sys.stdout``/``sys.stderr`` currently are (only
|
|
261
|
+
if they're currently a guarded stream), rather than restoring a
|
|
262
|
+
reference captured back whenever ``enable()`` first happened to run
|
|
263
|
+
-- which could be stale if the streams were swapped out since then.
|
|
264
|
+
"""
|
|
265
|
+
for name in ("stdout", "stderr"):
|
|
266
|
+
stream = getattr(sys, name)
|
|
265
267
|
if isinstance(stream, _GuardedStream):
|
|
268
|
+
# Flush any buffered partial-line content (e.g. output with
|
|
269
|
+
# no trailing newline yet) before unwrapping, so it isn't
|
|
270
|
+
# silently lost.
|
|
266
271
|
try:
|
|
267
272
|
stream.flush()
|
|
268
273
|
except Exception:
|
|
269
274
|
pass
|
|
270
|
-
|
|
271
|
-
if _state["orig_stdout"] is not None:
|
|
272
|
-
sys.stdout = _state["orig_stdout"]
|
|
273
|
-
if _state["orig_stderr"] is not None:
|
|
274
|
-
sys.stderr = _state["orig_stderr"]
|
|
275
|
+
setattr(sys, name, stream._wrapped)
|
|
275
276
|
|
|
276
277
|
_remove_logging_protection()
|
|
277
278
|
|
|
278
|
-
_state["orig_stdout"] = None
|
|
279
|
-
_state["orig_stderr"] = None
|
|
280
279
|
_state["active"] = False
|
|
281
280
|
|
|
282
281
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`.secretshieldignore` -- a familiar, `.gitignore`-style companion to
|
|
3
|
+
`secretshield.toml`'s `[scan.ignore]` section, for anyone who'd rather
|
|
4
|
+
drop in a plain ignore file than edit TOML.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
IGNORE_FILENAME = ".secretshieldignore"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def ignore_file_path(root: Path) -> Path:
|
|
15
|
+
return root / IGNORE_FILENAME
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_ignore_patterns(root: Path | None = None) -> list[str]:
|
|
19
|
+
"""
|
|
20
|
+
Read `.secretshieldignore` from `root` (default: current directory)
|
|
21
|
+
if present. One glob pattern per line; blank lines and lines
|
|
22
|
+
starting with `#` are skipped, same as `.gitignore`. A trailing
|
|
23
|
+
`/` means "this directory and everything under it," matching
|
|
24
|
+
`secretshield.toml`'s `[scan.ignore].paths` convention -- both
|
|
25
|
+
ultimately feed the same exclude-pattern matching used by
|
|
26
|
+
`--exclude`.
|
|
27
|
+
"""
|
|
28
|
+
path = ignore_file_path(root or Path.cwd())
|
|
29
|
+
if not path.exists():
|
|
30
|
+
return []
|
|
31
|
+
try:
|
|
32
|
+
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
33
|
+
except Exception:
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
patterns: list[str] = []
|
|
37
|
+
for raw_line in lines:
|
|
38
|
+
line = raw_line.strip()
|
|
39
|
+
if not line or line.startswith("#"):
|
|
40
|
+
continue
|
|
41
|
+
patterns.append(line if not line.endswith("/") else line + "*")
|
|
42
|
+
return patterns
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: secretshield
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Stop secrets before they leak -- runtime redaction, multi-language scanning, auto-fix, Git hooks, and CI, all in one CLI.
|
|
5
5
|
Author: Samarth Chugh (Sam3360)
|
|
6
6
|
License-Expression: MIT
|
|
@@ -33,7 +33,7 @@ Dynamic: license-file
|
|
|
33
33
|
|
|
34
34
|
[](https://pypi.org/project/secretshield/)
|
|
35
35
|
[](https://pypi.org/project/secretshield/)
|
|
36
|
-
[](https://pypi.org/project/secretshield/)
|
|
37
37
|
[](LICENSE)
|
|
38
38
|
|
|
39
39
|
[Install](#installation) · [Demo](#demo) · [Quick start](#quick-start) · [Features](#what-secretshield-does) · [Docs below](#cli-reference)
|
|
@@ -272,6 +272,20 @@ format = "text"
|
|
|
272
272
|
|
|
273
273
|
CLI flags always override the file.
|
|
274
274
|
|
|
275
|
+
Prefer a plain `.gitignore`-style file instead of editing TOML? Drop a
|
|
276
|
+
`.secretshieldignore` in the project root — one glob pattern per line,
|
|
277
|
+
`#` comments and blank lines ignored, same idea as `.gitignore`:
|
|
278
|
+
|
|
279
|
+
```text
|
|
280
|
+
# .secretshieldignore
|
|
281
|
+
vendor/
|
|
282
|
+
*.min.js
|
|
283
|
+
tests/fixtures/
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Both `secretshield.toml`'s `[scan.ignore]` and `.secretshieldignore`
|
|
287
|
+
can be used together — their patterns combine.
|
|
288
|
+
|
|
275
289
|
## Runtime configuration
|
|
276
290
|
|
|
277
291
|
```python
|
|
@@ -304,7 +318,7 @@ secretshield/
|
|
|
304
318
|
├── patterns.py, detector.py, redactor.py # detection & redaction engine
|
|
305
319
|
├── guardian.py # stdout/stderr + logging protection
|
|
306
320
|
├── config.py, notifications.py # runtime settings & safe warnings
|
|
307
|
-
├── project_config.py, baseline.py
|
|
321
|
+
├── project_config.py, baseline.py, ignorefile.py # secretshield.toml, --baseline, .secretshieldignore
|
|
308
322
|
├── cli.py # command-line interface
|
|
309
323
|
├── autofix/ # interactive scan --fix
|
|
310
324
|
├── git/ # pre-commit hook
|
|
@@ -357,3 +371,4 @@ If you find this project useful, consider [supporting its development through Gi
|
|
|
357
371
|
## License
|
|
358
372
|
|
|
359
373
|
MIT — see [LICENSE](LICENSE).
|
|
374
|
+
|
|
@@ -7,6 +7,7 @@ secretshield/cli.py
|
|
|
7
7
|
secretshield/config.py
|
|
8
8
|
secretshield/detector.py
|
|
9
9
|
secretshield/guardian.py
|
|
10
|
+
secretshield/ignorefile.py
|
|
10
11
|
secretshield/notifications.py
|
|
11
12
|
secretshield/patterns.py
|
|
12
13
|
secretshield/project_config.py
|
|
@@ -31,6 +32,8 @@ tests/test_baseline.py
|
|
|
31
32
|
tests/test_detector.py
|
|
32
33
|
tests/test_git_hooks.py
|
|
33
34
|
tests/test_github_action.py
|
|
35
|
+
tests/test_ignorefile.py
|
|
36
|
+
tests/test_ignorefile_cli_integration.py
|
|
34
37
|
tests/test_logging.py
|
|
35
38
|
tests/test_project_config.py
|
|
36
39
|
tests/test_redactor.py
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Tests for secretshield.ignorefile -- .secretshieldignore support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from secretshield.ignorefile import ignore_file_path, load_ignore_patterns
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_no_ignore_file_returns_empty_list(tmp_path):
|
|
9
|
+
assert load_ignore_patterns(tmp_path) == []
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_loads_simple_patterns(tmp_path):
|
|
13
|
+
(tmp_path / ".secretshieldignore").write_text("*.min.js\nvendor/\n")
|
|
14
|
+
patterns = load_ignore_patterns(tmp_path)
|
|
15
|
+
assert "*.min.js" in patterns
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_trailing_slash_becomes_glob(tmp_path):
|
|
19
|
+
(tmp_path / ".secretshieldignore").write_text("vendor/\n")
|
|
20
|
+
patterns = load_ignore_patterns(tmp_path)
|
|
21
|
+
assert "vendor/*" in patterns
|
|
22
|
+
assert "vendor/" not in patterns
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_comments_and_blank_lines_skipped(tmp_path):
|
|
26
|
+
(tmp_path / ".secretshieldignore").write_text(
|
|
27
|
+
"# this is a comment\n\nvendor/\n\n# another comment\n*.log\n"
|
|
28
|
+
)
|
|
29
|
+
patterns = load_ignore_patterns(tmp_path)
|
|
30
|
+
assert patterns == ["vendor/*", "*.log"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_ignore_file_path_helper(tmp_path):
|
|
34
|
+
path = ignore_file_path(tmp_path)
|
|
35
|
+
assert path.name == ".secretshieldignore"
|
|
36
|
+
assert path.parent == tmp_path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_malformed_file_does_not_crash(tmp_path):
|
|
40
|
+
# Write invalid UTF-8 bytes directly.
|
|
41
|
+
(tmp_path / ".secretshieldignore").write_bytes(b"\xff\xfe\x00garbage")
|
|
42
|
+
# Should not raise -- errors="ignore" handles invalid bytes.
|
|
43
|
+
patterns = load_ignore_patterns(tmp_path)
|
|
44
|
+
assert isinstance(patterns, list)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""CLI-level integration tests for .secretshieldignore."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from secretshield.cli import main
|
|
6
|
+
|
|
7
|
+
FAKE_SECRET = "ignoreFileIntegrationFakeSecret123456"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_scan_respects_secretshieldignore(tmp_path, monkeypatch):
|
|
11
|
+
monkeypatch.chdir(tmp_path)
|
|
12
|
+
(tmp_path / ".secretshieldignore").write_text("vendor/\n")
|
|
13
|
+
(tmp_path / "vendor").mkdir()
|
|
14
|
+
(tmp_path / "vendor" / "lib.py").write_text(f'API_KEY = "{FAKE_SECRET}"\n')
|
|
15
|
+
(tmp_path / "app.py").write_text("print('clean')\n")
|
|
16
|
+
|
|
17
|
+
exit_code = main(["scan", "."])
|
|
18
|
+
assert exit_code == 0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_scan_still_catches_non_ignored_secret(tmp_path, monkeypatch):
|
|
22
|
+
monkeypatch.chdir(tmp_path)
|
|
23
|
+
(tmp_path / ".secretshieldignore").write_text("vendor/\n")
|
|
24
|
+
(tmp_path / "vendor").mkdir()
|
|
25
|
+
(tmp_path / "vendor" / "lib.py").write_text('API_KEY = "not-caught-anyway"\n')
|
|
26
|
+
(tmp_path / "app.py").write_text(f'API_KEY = "{FAKE_SECRET}"\n')
|
|
27
|
+
|
|
28
|
+
exit_code = main(["scan", "."])
|
|
29
|
+
assert exit_code == 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_no_ignore_file_behaves_as_before(tmp_path, monkeypatch):
|
|
33
|
+
monkeypatch.chdir(tmp_path)
|
|
34
|
+
(tmp_path / "app.py").write_text(f'API_KEY = "{FAKE_SECRET}"\n')
|
|
35
|
+
|
|
36
|
+
exit_code = main(["scan", "."])
|
|
37
|
+
assert exit_code == 1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_secretshieldignore_and_toml_ignore_paths_combine(tmp_path, monkeypatch):
|
|
41
|
+
monkeypatch.chdir(tmp_path)
|
|
42
|
+
(tmp_path / ".secretshieldignore").write_text("vendor/\n")
|
|
43
|
+
(tmp_path / "secretshield.toml").write_text('[scan.ignore]\npaths = ["docs/"]\n')
|
|
44
|
+
(tmp_path / "vendor").mkdir()
|
|
45
|
+
(tmp_path / "docs").mkdir()
|
|
46
|
+
(tmp_path / "vendor" / "a.py").write_text(f'A = "{FAKE_SECRET}"\n')
|
|
47
|
+
(tmp_path / "docs" / "b.py").write_text(f'B = "{FAKE_SECRET}"\n')
|
|
48
|
+
(tmp_path / "app.py").write_text("print('clean')\n")
|
|
49
|
+
|
|
50
|
+
exit_code = main(["scan", "."])
|
|
51
|
+
assert exit_code == 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_explicit_exclude_flag_still_works_alongside_ignorefile(tmp_path, monkeypatch):
|
|
55
|
+
monkeypatch.chdir(tmp_path)
|
|
56
|
+
(tmp_path / ".secretshieldignore").write_text("vendor/\n")
|
|
57
|
+
(tmp_path / "vendor").mkdir()
|
|
58
|
+
(tmp_path / "vendor" / "a.py").write_text(f'A = "{FAKE_SECRET}"\n')
|
|
59
|
+
(tmp_path / "other.py").write_text(f'B = "{FAKE_SECRET}"\n')
|
|
60
|
+
|
|
61
|
+
exit_code = main(["scan", ".", "--exclude", "other.py"])
|
|
62
|
+
assert exit_code == 0
|
|
@@ -154,3 +154,59 @@ def test_partial_line_without_newline_flushed_on_flush_call():
|
|
|
154
154
|
output = buffer.getvalue()
|
|
155
155
|
assert "MyDogFluffy99" not in output
|
|
156
156
|
assert "********" in output
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# --- Regression test for a real bug: enable() was a no-op once already
|
|
160
|
+
# "active", even if sys.stdout/sys.stderr had been swapped out from
|
|
161
|
+
# under it (e.g. by a test framework's own output capture, or a user's
|
|
162
|
+
# own redirection). disable()/enable() must always operate on whatever
|
|
163
|
+
# the CURRENT streams are, not a stale reference from the first call.
|
|
164
|
+
|
|
165
|
+
def test_enable_rewraps_a_stream_swapped_in_after_first_enable():
|
|
166
|
+
original_stdout = sys.stdout
|
|
167
|
+
try:
|
|
168
|
+
# Protection is already globally active at this point (it was
|
|
169
|
+
# enabled automatically on package import). Something else now
|
|
170
|
+
# swaps sys.stdout out -- simulating pytest's capsys, or a
|
|
171
|
+
# user redirecting output themselves.
|
|
172
|
+
new_buffer = io.StringIO()
|
|
173
|
+
sys.stdout = new_buffer
|
|
174
|
+
|
|
175
|
+
# Calling enable() again must wrap THIS new stream, not silently
|
|
176
|
+
# no-op just because protection was "already on" for a
|
|
177
|
+
# different, now-replaced stream.
|
|
178
|
+
secretshield.enable()
|
|
179
|
+
assert hasattr(sys.stdout, "_secretshield_wrapped")
|
|
180
|
+
|
|
181
|
+
secret = "sk-regressionTestFakeSecretSwap123456"
|
|
182
|
+
print("API key:", secret)
|
|
183
|
+
sys.stdout.flush()
|
|
184
|
+
|
|
185
|
+
output = new_buffer.getvalue()
|
|
186
|
+
assert secret not in output
|
|
187
|
+
assert "********" in output
|
|
188
|
+
finally:
|
|
189
|
+
secretshield.disable()
|
|
190
|
+
sys.stdout = original_stdout
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_disable_unwraps_whatever_stream_is_current_not_stale_one():
|
|
194
|
+
original_stdout = sys.stdout
|
|
195
|
+
try:
|
|
196
|
+
buffer_a = io.StringIO()
|
|
197
|
+
sys.stdout = buffer_a
|
|
198
|
+
secretshield.enable()
|
|
199
|
+
|
|
200
|
+
# Swap to a second buffer while still "enabled".
|
|
201
|
+
buffer_b = io.StringIO()
|
|
202
|
+
sys.stdout = buffer_b
|
|
203
|
+
secretshield.enable() # re-wrap the new current stream
|
|
204
|
+
assert sys.stdout is not buffer_b # now wrapped
|
|
205
|
+
|
|
206
|
+
secretshield.disable()
|
|
207
|
+
# Must unwrap back to buffer_b (the current stream), not jump
|
|
208
|
+
# back to buffer_a (a stale reference from the first enable()).
|
|
209
|
+
assert sys.stdout is buffer_b
|
|
210
|
+
finally:
|
|
211
|
+
secretshield.disable()
|
|
212
|
+
sys.stdout = original_stdout
|
|
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
|
|
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
|