secretshield 0.4.2__tar.gz → 0.5.2__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.2}/PKG-INFO +25 -3
- {secretshield-0.4.2 → secretshield-0.5.2}/README.md +24 -2
- {secretshield-0.4.2 → secretshield-0.5.2}/pyproject.toml +1 -1
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/__init__.py +1 -1
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/cli.py +30 -19
- secretshield-0.5.2/secretshield/color.py +42 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/guardian.py +24 -25
- secretshield-0.5.2/secretshield/ignorefile.py +42 -0
- {secretshield-0.4.2 → secretshield-0.5.2/secretshield.egg-info}/PKG-INFO +25 -3
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield.egg-info/SOURCES.txt +6 -0
- secretshield-0.5.2/tests/test_color.py +77 -0
- secretshield-0.5.2/tests/test_color_cli_integration.py +69 -0
- secretshield-0.5.2/tests/test_ignorefile.py +44 -0
- secretshield-0.5.2/tests/test_ignorefile_cli_integration.py +62 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/tests/test_stdout.py +56 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/LICENSE +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/autofix/__init__.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/autofix/env.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/autofix/fixer.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/autofix/gitignore.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/autofix/python.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/baseline.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/config.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/detector.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/git/__init__.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/git/hooks.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/github/__init__.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/github/actions.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/notifications.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/patterns.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/project_config.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield/redactor.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield.egg-info/dependency_links.txt +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield.egg-info/entry_points.txt +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield.egg-info/requires.txt +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/secretshield.egg-info/top_level.txt +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/setup.cfg +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/tests/test_autofix.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/tests/test_baseline.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/tests/test_detector.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/tests/test_git_hooks.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/tests/test_github_action.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/tests/test_logging.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/tests/test_project_config.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/tests/test_redactor.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/tests/test_scan.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/tests/test_ss_alias_and_creator.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/tests/test_v030_cli_integration.py +0 -0
- {secretshield-0.4.2 → secretshield-0.5.2}/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.2
|
|
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)
|
|
@@ -185,6 +185,12 @@ redact("aws_key=AKIAABCDEFGHIJKLMNOP")
|
|
|
185
185
|
|
|
186
186
|
*(Every command below also works via the shorter `ss` alias — `ss scan .` is identical to `secretshield scan .`.)*
|
|
187
187
|
|
|
188
|
+
Output is colored (red `✗` / green `✓`) automatically when running in a
|
|
189
|
+
real terminal, and plain everywhere else — piped output, CI logs, and
|
|
190
|
+
`--json` are never colored, so nothing downstream ever has to deal with
|
|
191
|
+
stray escape codes. Force it either way with the standard `NO_COLOR=1`
|
|
192
|
+
/ `FORCE_COLOR=1` environment variables.
|
|
193
|
+
|
|
188
194
|
### `secretshield init` — set a project up in one step
|
|
189
195
|
|
|
190
196
|
```bash
|
|
@@ -272,6 +278,20 @@ format = "text"
|
|
|
272
278
|
|
|
273
279
|
CLI flags always override the file.
|
|
274
280
|
|
|
281
|
+
Prefer a plain `.gitignore`-style file instead of editing TOML? Drop a
|
|
282
|
+
`.secretshieldignore` in the project root — one glob pattern per line,
|
|
283
|
+
`#` comments and blank lines ignored, same idea as `.gitignore`:
|
|
284
|
+
|
|
285
|
+
```text
|
|
286
|
+
# .secretshieldignore
|
|
287
|
+
vendor/
|
|
288
|
+
*.min.js
|
|
289
|
+
tests/fixtures/
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Both `secretshield.toml`'s `[scan.ignore]` and `.secretshieldignore`
|
|
293
|
+
can be used together — their patterns combine.
|
|
294
|
+
|
|
275
295
|
## Runtime configuration
|
|
276
296
|
|
|
277
297
|
```python
|
|
@@ -304,7 +324,8 @@ secretshield/
|
|
|
304
324
|
├── patterns.py, detector.py, redactor.py # detection & redaction engine
|
|
305
325
|
├── guardian.py # stdout/stderr + logging protection
|
|
306
326
|
├── config.py, notifications.py # runtime settings & safe warnings
|
|
307
|
-
├── project_config.py, baseline.py
|
|
327
|
+
├── project_config.py, baseline.py, ignorefile.py # secretshield.toml, --baseline, .secretshieldignore
|
|
328
|
+
├── color.py # dependency-free ANSI colors, TTY-aware
|
|
308
329
|
├── cli.py # command-line interface
|
|
309
330
|
├── autofix/ # interactive scan --fix
|
|
310
331
|
├── git/ # pre-commit hook
|
|
@@ -357,3 +378,4 @@ If you find this project useful, consider [supporting its development through Gi
|
|
|
357
378
|
## License
|
|
358
379
|
|
|
359
380
|
MIT — see [LICENSE](LICENSE).
|
|
381
|
+
|
|
@@ -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)
|
|
@@ -159,6 +159,12 @@ redact("aws_key=AKIAABCDEFGHIJKLMNOP")
|
|
|
159
159
|
|
|
160
160
|
*(Every command below also works via the shorter `ss` alias — `ss scan .` is identical to `secretshield scan .`.)*
|
|
161
161
|
|
|
162
|
+
Output is colored (red `✗` / green `✓`) automatically when running in a
|
|
163
|
+
real terminal, and plain everywhere else — piped output, CI logs, and
|
|
164
|
+
`--json` are never colored, so nothing downstream ever has to deal with
|
|
165
|
+
stray escape codes. Force it either way with the standard `NO_COLOR=1`
|
|
166
|
+
/ `FORCE_COLOR=1` environment variables.
|
|
167
|
+
|
|
162
168
|
### `secretshield init` — set a project up in one step
|
|
163
169
|
|
|
164
170
|
```bash
|
|
@@ -246,6 +252,20 @@ format = "text"
|
|
|
246
252
|
|
|
247
253
|
CLI flags always override the file.
|
|
248
254
|
|
|
255
|
+
Prefer a plain `.gitignore`-style file instead of editing TOML? Drop a
|
|
256
|
+
`.secretshieldignore` in the project root — one glob pattern per line,
|
|
257
|
+
`#` comments and blank lines ignored, same idea as `.gitignore`:
|
|
258
|
+
|
|
259
|
+
```text
|
|
260
|
+
# .secretshieldignore
|
|
261
|
+
vendor/
|
|
262
|
+
*.min.js
|
|
263
|
+
tests/fixtures/
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Both `secretshield.toml`'s `[scan.ignore]` and `.secretshieldignore`
|
|
267
|
+
can be used together — their patterns combine.
|
|
268
|
+
|
|
249
269
|
## Runtime configuration
|
|
250
270
|
|
|
251
271
|
```python
|
|
@@ -278,7 +298,8 @@ secretshield/
|
|
|
278
298
|
├── patterns.py, detector.py, redactor.py # detection & redaction engine
|
|
279
299
|
├── guardian.py # stdout/stderr + logging protection
|
|
280
300
|
├── config.py, notifications.py # runtime settings & safe warnings
|
|
281
|
-
├── project_config.py, baseline.py
|
|
301
|
+
├── project_config.py, baseline.py, ignorefile.py # secretshield.toml, --baseline, .secretshieldignore
|
|
302
|
+
├── color.py # dependency-free ANSI colors, TTY-aware
|
|
282
303
|
├── cli.py # command-line interface
|
|
283
304
|
├── autofix/ # interactive scan --fix
|
|
284
305
|
├── git/ # pre-commit hook
|
|
@@ -331,3 +352,4 @@ If you find this project useful, consider [supporting its development through Gi
|
|
|
331
352
|
## License
|
|
332
353
|
|
|
333
354
|
MIT — see [LICENSE](LICENSE).
|
|
355
|
+
|
|
@@ -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.2"
|
|
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"
|
|
@@ -27,6 +27,7 @@ import sys
|
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
|
|
29
29
|
from . import __version__, disable, enable, is_enabled
|
|
30
|
+
from .color import green, red, supports_color
|
|
30
31
|
from .detector import detect
|
|
31
32
|
|
|
32
33
|
# --- File type support -----------------------------------------------------
|
|
@@ -423,6 +424,9 @@ def _run_scan(args: argparse.Namespace) -> int:
|
|
|
423
424
|
# A trailing "/" means "this directory and everything in it" --
|
|
424
425
|
# translate that into a glob so it matches nested files too.
|
|
425
426
|
exclude_patterns.append(raw if not raw.endswith("/") else raw + "*")
|
|
427
|
+
from .ignorefile import load_ignore_patterns
|
|
428
|
+
|
|
429
|
+
exclude_patterns.extend(load_ignore_patterns(Path.cwd()))
|
|
426
430
|
|
|
427
431
|
json_output = bool(args.json) or (project_cfg.output_format == "json")
|
|
428
432
|
|
|
@@ -513,23 +517,27 @@ def _run_scan(args: argparse.Namespace) -> int:
|
|
|
513
517
|
print(json.dumps(payload, indent=2))
|
|
514
518
|
return 1 if all_findings else 0
|
|
515
519
|
|
|
520
|
+
use_color = supports_color(sys.stdout)
|
|
516
521
|
print("SecretShield scan\n")
|
|
517
522
|
|
|
518
523
|
for finding in all_findings:
|
|
519
524
|
label, category = _kind_display(finding["kind"])
|
|
520
|
-
print(f"
|
|
525
|
+
print(f"{red(chr(0x2717), use_color)} {finding['file']}:{finding['line']}")
|
|
521
526
|
print(f" Potential secret: {label}")
|
|
522
527
|
print(f" Type: {category}\n")
|
|
523
528
|
|
|
524
529
|
if ignored_by_baseline:
|
|
525
|
-
print(
|
|
530
|
+
print(
|
|
531
|
+
f"{green(chr(0x2713), use_color)} "
|
|
532
|
+
f"{ignored_by_baseline} existing finding(s) ignored by baseline"
|
|
533
|
+
)
|
|
526
534
|
|
|
527
535
|
if all_findings:
|
|
528
|
-
print(f"
|
|
529
|
-
print(f"
|
|
536
|
+
print(f"{green(chr(0x2713), use_color)} {files_scanned} files scanned")
|
|
537
|
+
print(f"{red(chr(0x2717), use_color)} {len(all_findings)} potential secret(s) found")
|
|
530
538
|
else:
|
|
531
|
-
print(f"
|
|
532
|
-
print("
|
|
539
|
+
print(f"{green(chr(0x2713), use_color)} {files_scanned} files scanned")
|
|
540
|
+
print(f"{green(chr(0x2713), use_color)} No potential secrets found")
|
|
533
541
|
|
|
534
542
|
exit_code = 1 if all_findings else 0
|
|
535
543
|
print(f"\nExit code: {exit_code}")
|
|
@@ -580,12 +588,14 @@ def _run_scan_staged(args: argparse.Namespace) -> int:
|
|
|
580
588
|
}
|
|
581
589
|
)
|
|
582
590
|
|
|
591
|
+
use_color = supports_color(sys.stdout)
|
|
592
|
+
|
|
583
593
|
if not findings:
|
|
584
|
-
print("
|
|
594
|
+
print(f"{green(chr(0x2713), use_color)} No secrets detected.")
|
|
585
595
|
return 0
|
|
586
596
|
|
|
587
597
|
for finding in findings:
|
|
588
|
-
print("
|
|
598
|
+
print(f"{red(chr(0x2717), use_color)} Potential secret detected\n")
|
|
589
599
|
print(f"File: {finding['file']}")
|
|
590
600
|
print(f"Line: {finding['line']}")
|
|
591
601
|
print(f"Type: {finding['label']}\n")
|
|
@@ -605,7 +615,7 @@ def _cmd_install_hook(args: argparse.Namespace) -> int:
|
|
|
605
615
|
if result.status == "error":
|
|
606
616
|
print(f"secretshield: error: {result.message}", file=sys.stderr)
|
|
607
617
|
return 1
|
|
608
|
-
print(f"
|
|
618
|
+
print(f"{green(chr(0x2713), supports_color(sys.stdout))} {result.message}")
|
|
609
619
|
return 0
|
|
610
620
|
|
|
611
621
|
|
|
@@ -616,7 +626,7 @@ def _cmd_uninstall_hook(args: argparse.Namespace) -> int:
|
|
|
616
626
|
if result.status == "error":
|
|
617
627
|
print(f"secretshield: error: {result.message}", file=sys.stderr)
|
|
618
628
|
return 1
|
|
619
|
-
print(f"
|
|
629
|
+
print(f"{green(chr(0x2713), supports_color(sys.stdout))} {result.message}")
|
|
620
630
|
return 0
|
|
621
631
|
|
|
622
632
|
|
|
@@ -633,7 +643,7 @@ def _cmd_github_action(args: argparse.Namespace) -> int:
|
|
|
633
643
|
file=sys.stderr,
|
|
634
644
|
)
|
|
635
645
|
return 1
|
|
636
|
-
print(f"
|
|
646
|
+
print(f"{green(chr(0x2713), supports_color(sys.stdout))} Created {path}")
|
|
637
647
|
return 0
|
|
638
648
|
|
|
639
649
|
|
|
@@ -670,10 +680,11 @@ def _cmd_init(args: argparse.Namespace) -> int:
|
|
|
670
680
|
)
|
|
671
681
|
in_git_repo = is_git_repo()
|
|
672
682
|
|
|
683
|
+
use_color = supports_color(sys.stdout)
|
|
673
684
|
print("SecretShield project setup\n")
|
|
674
|
-
print(f"{
|
|
685
|
+
print(f"{green(chr(0x2713), use_color) if is_python_project else red(chr(0x2717), use_color)} "
|
|
675
686
|
f"{'Detected Python project' if is_python_project else 'No Python project markers found'}")
|
|
676
|
-
print(f"{
|
|
687
|
+
print(f"{green(chr(0x2713), use_color) if in_git_repo else red(chr(0x2717), use_color)} "
|
|
677
688
|
f"{'Detected Git repository' if in_git_repo else 'Not a Git repository'}")
|
|
678
689
|
print()
|
|
679
690
|
|
|
@@ -696,28 +707,28 @@ def _cmd_init(args: argparse.Namespace) -> int:
|
|
|
696
707
|
if want_config:
|
|
697
708
|
existing = find_config_file(project_root)
|
|
698
709
|
if existing is not None:
|
|
699
|
-
print(f"
|
|
710
|
+
print(f"{green(chr(0x2713), use_color)} secretshield.toml already exists, leaving it unchanged")
|
|
700
711
|
else:
|
|
701
712
|
config_path = project_root / "secretshield.toml"
|
|
702
713
|
config_path.write_text(render_default_toml(), encoding="utf-8")
|
|
703
|
-
print(f"
|
|
714
|
+
print(f"{green(chr(0x2713), use_color)} Created {config_path.name}")
|
|
704
715
|
created.append(config_path.name)
|
|
705
716
|
|
|
706
717
|
if want_hook:
|
|
707
718
|
result = install_hook()
|
|
708
719
|
if result.status == "error":
|
|
709
|
-
print(f"
|
|
720
|
+
print(f"{red(chr(0x2717), use_color)} {result.message}")
|
|
710
721
|
else:
|
|
711
|
-
print(f"
|
|
722
|
+
print(f"{green(chr(0x2713), use_color)} {result.message}")
|
|
712
723
|
created.append("Git pre-commit hook")
|
|
713
724
|
|
|
714
725
|
if want_actions:
|
|
715
726
|
written, path = generate_workflow(project_root)
|
|
716
727
|
if written:
|
|
717
|
-
print(f"
|
|
728
|
+
print(f"{green(chr(0x2713), use_color)} Created {path.relative_to(project_root)}")
|
|
718
729
|
created.append(str(path.relative_to(project_root)))
|
|
719
730
|
else:
|
|
720
|
-
print(f"
|
|
731
|
+
print(f"{green(chr(0x2713), use_color)} {path.relative_to(project_root)} already exists, leaving it unchanged")
|
|
721
732
|
|
|
722
733
|
print("\nSecretShield is ready.")
|
|
723
734
|
return 0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Minimal, dependency-free ANSI color support for CLI output.
|
|
3
|
+
|
|
4
|
+
Colors are only applied when the target stream is a real interactive
|
|
5
|
+
terminal -- never when output is piped, redirected, going into CI logs,
|
|
6
|
+
or serialized as `--json`. Respects the informal-but-widely-followed
|
|
7
|
+
`NO_COLOR` (disable) and `FORCE_COLOR` (force on) environment variable
|
|
8
|
+
conventions.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from typing import TextIO
|
|
16
|
+
|
|
17
|
+
_RED = "\033[31m"
|
|
18
|
+
_GREEN = "\033[32m"
|
|
19
|
+
_RESET = "\033[0m"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def supports_color(stream: TextIO | None = None) -> bool:
|
|
23
|
+
"""Whether ANSI colors should be used when writing to `stream`
|
|
24
|
+
(default: sys.stdout)."""
|
|
25
|
+
if os.environ.get("NO_COLOR") is not None:
|
|
26
|
+
return False
|
|
27
|
+
if os.environ.get("FORCE_COLOR") is not None:
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
target = stream if stream is not None else sys.stdout
|
|
31
|
+
try:
|
|
32
|
+
return bool(target.isatty())
|
|
33
|
+
except Exception:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def red(text: str, enabled: bool) -> str:
|
|
38
|
+
return f"{_RED}{text}{_RESET}" if enabled else text
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def green(text: str, enabled: bool) -> str:
|
|
42
|
+
return f"{_GREEN}{text}{_RESET}" if enabled else text
|
|
@@ -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.2
|
|
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)
|
|
@@ -185,6 +185,12 @@ redact("aws_key=AKIAABCDEFGHIJKLMNOP")
|
|
|
185
185
|
|
|
186
186
|
*(Every command below also works via the shorter `ss` alias — `ss scan .` is identical to `secretshield scan .`.)*
|
|
187
187
|
|
|
188
|
+
Output is colored (red `✗` / green `✓`) automatically when running in a
|
|
189
|
+
real terminal, and plain everywhere else — piped output, CI logs, and
|
|
190
|
+
`--json` are never colored, so nothing downstream ever has to deal with
|
|
191
|
+
stray escape codes. Force it either way with the standard `NO_COLOR=1`
|
|
192
|
+
/ `FORCE_COLOR=1` environment variables.
|
|
193
|
+
|
|
188
194
|
### `secretshield init` — set a project up in one step
|
|
189
195
|
|
|
190
196
|
```bash
|
|
@@ -272,6 +278,20 @@ format = "text"
|
|
|
272
278
|
|
|
273
279
|
CLI flags always override the file.
|
|
274
280
|
|
|
281
|
+
Prefer a plain `.gitignore`-style file instead of editing TOML? Drop a
|
|
282
|
+
`.secretshieldignore` in the project root — one glob pattern per line,
|
|
283
|
+
`#` comments and blank lines ignored, same idea as `.gitignore`:
|
|
284
|
+
|
|
285
|
+
```text
|
|
286
|
+
# .secretshieldignore
|
|
287
|
+
vendor/
|
|
288
|
+
*.min.js
|
|
289
|
+
tests/fixtures/
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Both `secretshield.toml`'s `[scan.ignore]` and `.secretshieldignore`
|
|
293
|
+
can be used together — their patterns combine.
|
|
294
|
+
|
|
275
295
|
## Runtime configuration
|
|
276
296
|
|
|
277
297
|
```python
|
|
@@ -304,7 +324,8 @@ secretshield/
|
|
|
304
324
|
├── patterns.py, detector.py, redactor.py # detection & redaction engine
|
|
305
325
|
├── guardian.py # stdout/stderr + logging protection
|
|
306
326
|
├── config.py, notifications.py # runtime settings & safe warnings
|
|
307
|
-
├── project_config.py, baseline.py
|
|
327
|
+
├── project_config.py, baseline.py, ignorefile.py # secretshield.toml, --baseline, .secretshieldignore
|
|
328
|
+
├── color.py # dependency-free ANSI colors, TTY-aware
|
|
308
329
|
├── cli.py # command-line interface
|
|
309
330
|
├── autofix/ # interactive scan --fix
|
|
310
331
|
├── git/ # pre-commit hook
|
|
@@ -357,3 +378,4 @@ If you find this project useful, consider [supporting its development through Gi
|
|
|
357
378
|
## License
|
|
358
379
|
|
|
359
380
|
MIT — see [LICENSE](LICENSE).
|
|
381
|
+
|
|
@@ -4,9 +4,11 @@ pyproject.toml
|
|
|
4
4
|
secretshield/__init__.py
|
|
5
5
|
secretshield/baseline.py
|
|
6
6
|
secretshield/cli.py
|
|
7
|
+
secretshield/color.py
|
|
7
8
|
secretshield/config.py
|
|
8
9
|
secretshield/detector.py
|
|
9
10
|
secretshield/guardian.py
|
|
11
|
+
secretshield/ignorefile.py
|
|
10
12
|
secretshield/notifications.py
|
|
11
13
|
secretshield/patterns.py
|
|
12
14
|
secretshield/project_config.py
|
|
@@ -28,9 +30,13 @@ secretshield/github/__init__.py
|
|
|
28
30
|
secretshield/github/actions.py
|
|
29
31
|
tests/test_autofix.py
|
|
30
32
|
tests/test_baseline.py
|
|
33
|
+
tests/test_color.py
|
|
34
|
+
tests/test_color_cli_integration.py
|
|
31
35
|
tests/test_detector.py
|
|
32
36
|
tests/test_git_hooks.py
|
|
33
37
|
tests/test_github_action.py
|
|
38
|
+
tests/test_ignorefile.py
|
|
39
|
+
tests/test_ignorefile_cli_integration.py
|
|
34
40
|
tests/test_logging.py
|
|
35
41
|
tests/test_project_config.py
|
|
36
42
|
tests/test_redactor.py
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Tests for secretshield.color -- dependency-free ANSI color support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
|
|
7
|
+
from secretshield.color import green, red, supports_color
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _FakeTTY(io.StringIO):
|
|
11
|
+
def isatty(self):
|
|
12
|
+
return True
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _FakeNonTTY(io.StringIO):
|
|
16
|
+
def isatty(self):
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_red_wraps_with_ansi_when_enabled():
|
|
21
|
+
result = red("X", True)
|
|
22
|
+
assert result.startswith("\033[31m")
|
|
23
|
+
assert result.endswith("\033[0m")
|
|
24
|
+
assert "X" in result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_red_plain_when_disabled():
|
|
28
|
+
assert red("X", False) == "X"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_green_wraps_with_ansi_when_enabled():
|
|
32
|
+
result = green("Y", True)
|
|
33
|
+
assert result.startswith("\033[32m")
|
|
34
|
+
assert result.endswith("\033[0m")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_green_plain_when_disabled():
|
|
38
|
+
assert green("Y", False) == "Y"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_supports_color_true_for_tty(monkeypatch):
|
|
42
|
+
monkeypatch.delenv("NO_COLOR", raising=False)
|
|
43
|
+
monkeypatch.delenv("FORCE_COLOR", raising=False)
|
|
44
|
+
assert supports_color(_FakeTTY()) is True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_supports_color_false_for_non_tty(monkeypatch):
|
|
48
|
+
monkeypatch.delenv("NO_COLOR", raising=False)
|
|
49
|
+
monkeypatch.delenv("FORCE_COLOR", raising=False)
|
|
50
|
+
assert supports_color(_FakeNonTTY()) is False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_no_color_env_disables_even_for_tty(monkeypatch):
|
|
54
|
+
monkeypatch.setenv("NO_COLOR", "1")
|
|
55
|
+
assert supports_color(_FakeTTY()) is False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_force_color_env_enables_even_for_non_tty(monkeypatch):
|
|
59
|
+
monkeypatch.delenv("NO_COLOR", raising=False)
|
|
60
|
+
monkeypatch.setenv("FORCE_COLOR", "1")
|
|
61
|
+
assert supports_color(_FakeNonTTY()) is True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_no_color_takes_precedence_over_force_color(monkeypatch):
|
|
65
|
+
monkeypatch.setenv("NO_COLOR", "1")
|
|
66
|
+
monkeypatch.setenv("FORCE_COLOR", "1")
|
|
67
|
+
assert supports_color(_FakeTTY()) is False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_supports_color_handles_stream_without_isatty(monkeypatch):
|
|
71
|
+
monkeypatch.delenv("NO_COLOR", raising=False)
|
|
72
|
+
monkeypatch.delenv("FORCE_COLOR", raising=False)
|
|
73
|
+
|
|
74
|
+
class _Weird:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
assert supports_color(_Weird()) is False
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI-level integration tests confirming colored output actually applies
|
|
3
|
+
(or doesn't) in `scan`'s real report, based on TTY status.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import io
|
|
9
|
+
|
|
10
|
+
from secretshield.cli import main
|
|
11
|
+
|
|
12
|
+
FAKE_SECRET = "colorIntegrationFakeSecret123456789"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _FakeTTY(io.StringIO):
|
|
16
|
+
def isatty(self):
|
|
17
|
+
return True
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_scan_output_has_no_ansi_codes_by_default_under_pytest(tmp_path, monkeypatch, capsys):
|
|
21
|
+
# Under pytest, stdout is never a real TTY -- output must be plain.
|
|
22
|
+
monkeypatch.chdir(tmp_path)
|
|
23
|
+
monkeypatch.delenv("FORCE_COLOR", raising=False)
|
|
24
|
+
(tmp_path / "app.py").write_text(f'API_KEY = "{FAKE_SECRET}"\n')
|
|
25
|
+
|
|
26
|
+
main(["scan", "."])
|
|
27
|
+
out = capsys.readouterr().out
|
|
28
|
+
assert "\033[" not in out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_scan_json_output_never_contains_ansi_codes(tmp_path, monkeypatch, capsys):
|
|
32
|
+
monkeypatch.chdir(tmp_path)
|
|
33
|
+
monkeypatch.setenv("FORCE_COLOR", "1") # even if forced on, --json must stay clean
|
|
34
|
+
(tmp_path / "app.py").write_text(f'API_KEY = "{FAKE_SECRET}"\n')
|
|
35
|
+
|
|
36
|
+
main(["scan", ".", "--json"])
|
|
37
|
+
out = capsys.readouterr().out
|
|
38
|
+
assert "\033[" not in out
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_scan_with_force_color_env_produces_ansi_codes(tmp_path, monkeypatch, capsys):
|
|
42
|
+
monkeypatch.chdir(tmp_path)
|
|
43
|
+
monkeypatch.setenv("FORCE_COLOR", "1")
|
|
44
|
+
(tmp_path / "app.py").write_text(f'API_KEY = "{FAKE_SECRET}"\n')
|
|
45
|
+
|
|
46
|
+
main(["scan", "."])
|
|
47
|
+
out = capsys.readouterr().out
|
|
48
|
+
assert "\033[31m" in out # red for the finding
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_scan_clean_with_force_color_produces_green(tmp_path, monkeypatch, capsys):
|
|
52
|
+
monkeypatch.chdir(tmp_path)
|
|
53
|
+
monkeypatch.setenv("FORCE_COLOR", "1")
|
|
54
|
+
(tmp_path / "app.py").write_text("print('clean')\n")
|
|
55
|
+
|
|
56
|
+
main(["scan", "."])
|
|
57
|
+
out = capsys.readouterr().out
|
|
58
|
+
assert "\033[32m" in out # green for "No potential secrets found"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_no_color_env_suppresses_even_with_force_color_tty(tmp_path, monkeypatch, capsys):
|
|
62
|
+
monkeypatch.chdir(tmp_path)
|
|
63
|
+
monkeypatch.setenv("FORCE_COLOR", "1")
|
|
64
|
+
monkeypatch.setenv("NO_COLOR", "1")
|
|
65
|
+
(tmp_path / "app.py").write_text(f'API_KEY = "{FAKE_SECRET}"\n')
|
|
66
|
+
|
|
67
|
+
main(["scan", "."])
|
|
68
|
+
out = capsys.readouterr().out
|
|
69
|
+
assert "\033[" not in out
|
|
@@ -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
|