secretshield 0.7.0__tar.gz → 0.8.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.7.0/secretshield.egg-info → secretshield-0.8.0}/PKG-INFO +17 -6
- {secretshield-0.7.0 → secretshield-0.8.0}/README.md +16 -5
- {secretshield-0.7.0 → secretshield-0.8.0}/pyproject.toml +1 -1
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/__init__.py +1 -1
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/cli.py +105 -4
- {secretshield-0.7.0 → secretshield-0.8.0/secretshield.egg-info}/PKG-INFO +17 -6
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield.egg-info/SOURCES.txt +2 -1
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_custom_rules_cli_integration.py +54 -1
- secretshield-0.8.0/tests/test_v080_self_detection_fix.py +341 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/LICENSE +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/autofix/__init__.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/autofix/env.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/autofix/fixer.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/autofix/gitignore.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/autofix/javascript.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/autofix/python.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/baseline.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/color.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/config.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/custom_rules.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/detector.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/git/__init__.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/git/hooks.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/github/__init__.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/github/actions.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/guardian.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/ignorefile.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/notifications.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/patterns.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/project_config.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield/redactor.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield.egg-info/dependency_links.txt +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield.egg-info/entry_points.txt +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield.egg-info/requires.txt +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/secretshield.egg-info/top_level.txt +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/setup.cfg +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_autofix.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_baseline.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_color.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_color_cli_integration.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_custom_rules.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_detector.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_git_hooks.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_github_action.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_ignorefile.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_ignorefile_cli_integration.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_javascript_autofix.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_logging.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_project_config.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_redactor.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_scan.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_ss_alias_and_creator.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_stdout.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.0}/tests/test_v030_cli_integration.py +0 -0
- {secretshield-0.7.0 → secretshield-0.8.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.8.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
|
|
@@ -32,10 +32,10 @@ Dynamic: license-file
|
|
|
32
32
|
|
|
33
33
|
[](https://pypi.org/project/secretshield/)
|
|
34
34
|
[](https://pypi.org/project/secretshield/)
|
|
35
|
-
[](https://pypi.org/project/secretshield/)
|
|
36
36
|
[](LICENSE)
|
|
37
37
|
|
|
38
|
-
[Install](#install) · [Demo](#demo) · [Try it](#try-it) · [Full usage guide →](https://github.com/Sam3360/secretshield/blob/main/USAGE.md)
|
|
38
|
+
[Install](#install) · [Demo](#demo) · [Try it](#try-it) · [Homepage 🌐](https://therealsecretshield.freebuff.app/) · [Full usage guide →](https://github.com/Sam3360/secretshield/blob/main/USAGE.md)
|
|
39
39
|
|
|
40
40
|
</div>
|
|
41
41
|
|
|
@@ -115,9 +115,12 @@ name = "My API Key"
|
|
|
115
115
|
pattern = "MYAPP_[A-Za-z0-9]{32}"
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
`secretshield scan .` picks it up automatically — no flag needed.
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
`secretshield scan .` picks it up automatically — no flag needed. The
|
|
119
|
+
rules file itself is never treated as project content to scan, so a
|
|
120
|
+
pattern that resembles a real secret won't flag its own definition.
|
|
121
|
+
Validate rules with `secretshield rules check`, create one
|
|
122
|
+
interactively with `secretshield rule create`, or try a pattern against
|
|
123
|
+
sample text with `secretshield rule test` before committing to it.
|
|
121
124
|
|
|
122
125
|
For the full command reference, configuration options, and exactly how
|
|
123
126
|
Auto-Fix decides what's safe to rewrite, see the
|
|
@@ -145,3 +148,11 @@ If you find this project useful, consider [supporting its development through Gi
|
|
|
145
148
|
## License
|
|
146
149
|
|
|
147
150
|
MIT — see [LICENSE](LICENSE).
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Homepage
|
|
155
|
+
|
|
156
|
+
Visit the official SecretShield homepage for an overview of the project, features, releases, and more:
|
|
157
|
+
|
|
158
|
+
**🌐 [therealsecretshield.freebuff.app](https://therealsecretshield.freebuff.app/)**
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://pypi.org/project/secretshield/)
|
|
8
8
|
[](https://pypi.org/project/secretshield/)
|
|
9
|
-
[](https://pypi.org/project/secretshield/)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
|
|
12
|
-
[Install](#install) · [Demo](#demo) · [Try it](#try-it) · [Full usage guide →](https://github.com/Sam3360/secretshield/blob/main/USAGE.md)
|
|
12
|
+
[Install](#install) · [Demo](#demo) · [Try it](#try-it) · [Homepage 🌐](https://therealsecretshield.freebuff.app/) · [Full usage guide →](https://github.com/Sam3360/secretshield/blob/main/USAGE.md)
|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
@@ -89,9 +89,12 @@ name = "My API Key"
|
|
|
89
89
|
pattern = "MYAPP_[A-Za-z0-9]{32}"
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
`secretshield scan .` picks it up automatically — no flag needed.
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
`secretshield scan .` picks it up automatically — no flag needed. The
|
|
93
|
+
rules file itself is never treated as project content to scan, so a
|
|
94
|
+
pattern that resembles a real secret won't flag its own definition.
|
|
95
|
+
Validate rules with `secretshield rules check`, create one
|
|
96
|
+
interactively with `secretshield rule create`, or try a pattern against
|
|
97
|
+
sample text with `secretshield rule test` before committing to it.
|
|
95
98
|
|
|
96
99
|
For the full command reference, configuration options, and exactly how
|
|
97
100
|
Auto-Fix decides what's safe to rewrite, see the
|
|
@@ -119,3 +122,11 @@ If you find this project useful, consider [supporting its development through Gi
|
|
|
119
122
|
## License
|
|
120
123
|
|
|
121
124
|
MIT — see [LICENSE](LICENSE).
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
## Homepage
|
|
129
|
+
|
|
130
|
+
Visit the official SecretShield homepage for an overview of the project, features, releases, and more:
|
|
131
|
+
|
|
132
|
+
**🌐 [therealsecretshield.freebuff.app](https://therealsecretshield.freebuff.app/)**
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "secretshield"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.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"
|
|
@@ -60,6 +60,32 @@ _SKIP_DIR_NAMES = {
|
|
|
60
60
|
".tox", "egg-info",
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
# SecretShield's own meta-files, skipped by default during a directory
|
|
64
|
+
# scan -- not because of their extension (a targeted, narrow exclusion
|
|
65
|
+
# by exact name, not "skip all .toml/.json files"), but because their
|
|
66
|
+
# job is to *configure* detection, not to hold project source that
|
|
67
|
+
# might leak a secret. A custom rule's pattern necessarily resembles
|
|
68
|
+
# the very thing it's meant to catch (that's the whole point of it),
|
|
69
|
+
# so without this, .secretshield-rules.toml ends up reporting itself
|
|
70
|
+
# as a finding the moment a rule's pattern looks enough like a real
|
|
71
|
+
# secret -- which defeats the purpose of writing the rule in the first
|
|
72
|
+
# place. secretshield.toml and .secretshieldignore are excluded for
|
|
73
|
+
# the same reason (settings/ignore globs aren't project secrets
|
|
74
|
+
# either), and the baseline file is excluded as a defensive measure:
|
|
75
|
+
# its SHA-256 hex hashes sit safely below the *default* entropy
|
|
76
|
+
# threshold, but a project that lowers entropy_threshold via
|
|
77
|
+
# secretshield.toml could otherwise start flagging its own baseline.
|
|
78
|
+
# --no-ignore re-includes all of these, same as it does for
|
|
79
|
+
# _SKIP_DIR_NAMES; explicitly scanning one of them by path (e.g.
|
|
80
|
+
# `secretshield scan .secretshield-rules.toml`) is unaffected -- this
|
|
81
|
+
# only changes what a directory *walk* picks up by default.
|
|
82
|
+
_SKIP_FILE_NAMES = {
|
|
83
|
+
".secretshield-rules.toml",
|
|
84
|
+
"secretshield.toml",
|
|
85
|
+
".secretshieldignore",
|
|
86
|
+
".secretshield-baseline.json",
|
|
87
|
+
}
|
|
88
|
+
|
|
63
89
|
# Bytes read from the start of a file to decide whether it looks binary.
|
|
64
90
|
_BINARY_SNIFF_SIZE = 1024
|
|
65
91
|
|
|
@@ -91,6 +117,10 @@ def _looks_binary(path: Path) -> bool:
|
|
|
91
117
|
return False
|
|
92
118
|
|
|
93
119
|
|
|
120
|
+
def _is_secretshield_meta_file(name: str) -> bool:
|
|
121
|
+
return name in _SKIP_FILE_NAMES
|
|
122
|
+
|
|
123
|
+
|
|
94
124
|
def _should_scan_file(
|
|
95
125
|
path: Path,
|
|
96
126
|
include_patterns: list[str],
|
|
@@ -140,6 +170,8 @@ def _iter_scan_files(
|
|
|
140
170
|
continue
|
|
141
171
|
if not no_ignore and any(part in _SKIP_DIR_NAMES for part in path.parts):
|
|
142
172
|
continue
|
|
173
|
+
if not no_ignore and _is_secretshield_meta_file(path.name):
|
|
174
|
+
continue
|
|
143
175
|
if not _should_scan_file(path, include_patterns, exclude_patterns):
|
|
144
176
|
continue
|
|
145
177
|
if _looks_binary(path):
|
|
@@ -168,6 +200,12 @@ _KIND_DISPLAY: dict[str, tuple[str, str]] = {
|
|
|
168
200
|
}
|
|
169
201
|
|
|
170
202
|
|
|
203
|
+
def _plural(count: int, word: str) -> str:
|
|
204
|
+
"""`f"{count} {word}"`, pluralized properly -- "1 rule" / "2 rules" --
|
|
205
|
+
instead of the awkward literal "rule(s)" regardless of count."""
|
|
206
|
+
return f"{count} {word}" if count == 1 else f"{count} {word}s"
|
|
207
|
+
|
|
208
|
+
|
|
171
209
|
def _kind_display(kind: str) -> tuple[str, str]:
|
|
172
210
|
return _KIND_DISPLAY.get(kind, (kind, "possible secret"))
|
|
173
211
|
|
|
@@ -372,8 +410,11 @@ def _iter_scan_sources(args: argparse.Namespace, target: Path, include_patterns:
|
|
|
372
410
|
if not is_git_repo():
|
|
373
411
|
print("secretshield: error: not inside a Git repository", file=sys.stderr)
|
|
374
412
|
return
|
|
413
|
+
no_ignore = getattr(args, "no_ignore", False)
|
|
375
414
|
for relpath in get_staged_files():
|
|
376
415
|
path = Path(relpath)
|
|
416
|
+
if not no_ignore and _is_secretshield_meta_file(path.name):
|
|
417
|
+
continue
|
|
377
418
|
if not _should_scan_file(path, include_patterns, exclude_patterns):
|
|
378
419
|
continue
|
|
379
420
|
content = get_staged_content(relpath)
|
|
@@ -389,8 +430,11 @@ def _iter_scan_sources(args: argparse.Namespace, target: Path, include_patterns:
|
|
|
389
430
|
if not is_git_repo():
|
|
390
431
|
print("secretshield: error: not inside a Git repository", file=sys.stderr)
|
|
391
432
|
return
|
|
433
|
+
no_ignore = getattr(args, "no_ignore", False)
|
|
392
434
|
for relpath in get_diff_files(diff_ref):
|
|
393
435
|
path = Path(relpath)
|
|
436
|
+
if not no_ignore and _is_secretshield_meta_file(path.name):
|
|
437
|
+
continue
|
|
394
438
|
if not _should_scan_file(path, include_patterns, exclude_patterns):
|
|
395
439
|
continue
|
|
396
440
|
if not path.exists() or _looks_binary(path):
|
|
@@ -437,8 +481,8 @@ def _run_scan(args: argparse.Namespace) -> int:
|
|
|
437
481
|
custom_rules, custom_rule_errors = load_custom_rules(Path.cwd())
|
|
438
482
|
if custom_rule_errors:
|
|
439
483
|
print(
|
|
440
|
-
f"secretshield: warning: {len(custom_rule_errors)
|
|
441
|
-
f"
|
|
484
|
+
f"secretshield: warning: {_plural(len(custom_rule_errors), 'invalid custom rule')} "
|
|
485
|
+
f"in .secretshield-rules.toml -- run `secretshield rules "
|
|
442
486
|
f"check` for details.",
|
|
443
487
|
file=sys.stderr,
|
|
444
488
|
)
|
|
@@ -592,6 +636,8 @@ def _run_scan_staged(args: argparse.Namespace) -> int:
|
|
|
592
636
|
findings: list[dict] = []
|
|
593
637
|
for relpath in get_staged_files():
|
|
594
638
|
path = Path(relpath)
|
|
639
|
+
if _is_secretshield_meta_file(path.name):
|
|
640
|
+
continue
|
|
595
641
|
if not _should_scan_file(path, [], []):
|
|
596
642
|
continue
|
|
597
643
|
content = get_staged_content(relpath)
|
|
@@ -787,7 +833,7 @@ def _cmd_rules_check(args: argparse.Namespace) -> int:
|
|
|
787
833
|
return 1
|
|
788
834
|
|
|
789
835
|
print(f"{green(chr(0x2713), use_color)} {path.name}")
|
|
790
|
-
print(f"{green(chr(0x2713), use_color)} {len(rules)
|
|
836
|
+
print(f"{green(chr(0x2713), use_color)} {_plural(len(rules), 'rule')} loaded")
|
|
791
837
|
print(f"{green(chr(0x2713), use_color)} All patterns are valid")
|
|
792
838
|
return 0
|
|
793
839
|
|
|
@@ -815,7 +861,7 @@ def _cmd_rules_list(args: argparse.Namespace) -> int:
|
|
|
815
861
|
if errors:
|
|
816
862
|
use_color = supports_color(sys.stdout)
|
|
817
863
|
print(
|
|
818
|
-
f"\n{red(chr(0x2717), use_color)} {len(errors)
|
|
864
|
+
f"\n{red(chr(0x2717), use_color)} {_plural(len(errors), 'rule')} failed to load "
|
|
819
865
|
f"-- run `secretshield rules check` for details."
|
|
820
866
|
)
|
|
821
867
|
return 0
|
|
@@ -877,6 +923,56 @@ def _cmd_rule_create(args: argparse.Namespace) -> int:
|
|
|
877
923
|
return 0
|
|
878
924
|
|
|
879
925
|
|
|
926
|
+
def _cmd_rule_test(args: argparse.Namespace) -> int:
|
|
927
|
+
from .custom_rules import load_custom_rules, rules_file_path
|
|
928
|
+
|
|
929
|
+
try:
|
|
930
|
+
interactive = sys.stdin.isatty()
|
|
931
|
+
except Exception:
|
|
932
|
+
interactive = False
|
|
933
|
+
|
|
934
|
+
if not interactive:
|
|
935
|
+
print("secretshield rule test requires an interactive terminal.")
|
|
936
|
+
return 1
|
|
937
|
+
|
|
938
|
+
path = rules_file_path(Path.cwd())
|
|
939
|
+
if not path.exists():
|
|
940
|
+
print(f"No {path.name} found. Create one with `secretshield rule create`.")
|
|
941
|
+
return 0
|
|
942
|
+
|
|
943
|
+
rules, errors = load_custom_rules(Path.cwd())
|
|
944
|
+
if errors:
|
|
945
|
+
print(
|
|
946
|
+
f"secretshield: warning: {_plural(len(errors), 'invalid custom rule')} "
|
|
947
|
+
f"skipped -- run `secretshield rules check` for details.",
|
|
948
|
+
file=sys.stderr,
|
|
949
|
+
)
|
|
950
|
+
if not rules:
|
|
951
|
+
print("No valid custom rules to test.")
|
|
952
|
+
return 0
|
|
953
|
+
|
|
954
|
+
use_color = supports_color(sys.stdout)
|
|
955
|
+
print("SecretShield \u2014 Test custom rules\n")
|
|
956
|
+
print(f"Loaded {_plural(len(rules), 'rule')}: {', '.join(r.name for r in rules)}\n")
|
|
957
|
+
|
|
958
|
+
sample = input("Text to test: ")
|
|
959
|
+
if not sample:
|
|
960
|
+
print("No text entered.")
|
|
961
|
+
return 0
|
|
962
|
+
|
|
963
|
+
print()
|
|
964
|
+
for rule in rules:
|
|
965
|
+
m = rule.regex.search(sample)
|
|
966
|
+
if m:
|
|
967
|
+
span = m.end() - m.start()
|
|
968
|
+
print(f"{green(chr(0x2713), use_color)} Matched: {rule.name}")
|
|
969
|
+
print(f" at position {m.start()}-{m.end()} ({span} character(s), value masked)")
|
|
970
|
+
else:
|
|
971
|
+
print(f"{red(chr(0x2717), use_color)} No match: {rule.name}")
|
|
972
|
+
|
|
973
|
+
return 0
|
|
974
|
+
|
|
975
|
+
|
|
880
976
|
# --- argparse wiring -----------------------------------------------------
|
|
881
977
|
|
|
882
978
|
|
|
@@ -1088,6 +1184,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1088
1184
|
"create", help="Interactively create a new rule in .secretshield-rules.toml."
|
|
1089
1185
|
)
|
|
1090
1186
|
rule_create_parser.set_defaults(func=_cmd_rule_create)
|
|
1187
|
+
rule_test_parser = rule_subparsers.add_parser(
|
|
1188
|
+
"test",
|
|
1189
|
+
help="Interactively test sample text against your custom rules without scanning any file.",
|
|
1190
|
+
)
|
|
1191
|
+
rule_test_parser.set_defaults(func=_cmd_rule_test)
|
|
1091
1192
|
|
|
1092
1193
|
return parser
|
|
1093
1194
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: secretshield
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -32,10 +32,10 @@ Dynamic: license-file
|
|
|
32
32
|
|
|
33
33
|
[](https://pypi.org/project/secretshield/)
|
|
34
34
|
[](https://pypi.org/project/secretshield/)
|
|
35
|
-
[](https://pypi.org/project/secretshield/)
|
|
36
36
|
[](LICENSE)
|
|
37
37
|
|
|
38
|
-
[Install](#install) · [Demo](#demo) · [Try it](#try-it) · [Full usage guide →](https://github.com/Sam3360/secretshield/blob/main/USAGE.md)
|
|
38
|
+
[Install](#install) · [Demo](#demo) · [Try it](#try-it) · [Homepage 🌐](https://therealsecretshield.freebuff.app/) · [Full usage guide →](https://github.com/Sam3360/secretshield/blob/main/USAGE.md)
|
|
39
39
|
|
|
40
40
|
</div>
|
|
41
41
|
|
|
@@ -115,9 +115,12 @@ name = "My API Key"
|
|
|
115
115
|
pattern = "MYAPP_[A-Za-z0-9]{32}"
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
`secretshield scan .` picks it up automatically — no flag needed.
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
`secretshield scan .` picks it up automatically — no flag needed. The
|
|
119
|
+
rules file itself is never treated as project content to scan, so a
|
|
120
|
+
pattern that resembles a real secret won't flag its own definition.
|
|
121
|
+
Validate rules with `secretshield rules check`, create one
|
|
122
|
+
interactively with `secretshield rule create`, or try a pattern against
|
|
123
|
+
sample text with `secretshield rule test` before committing to it.
|
|
121
124
|
|
|
122
125
|
For the full command reference, configuration options, and exactly how
|
|
123
126
|
Auto-Fix decides what's safe to rewrite, see the
|
|
@@ -145,3 +148,11 @@ If you find this project useful, consider [supporting its development through Gi
|
|
|
145
148
|
## License
|
|
146
149
|
|
|
147
150
|
MIT — see [LICENSE](LICENSE).
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Homepage
|
|
155
|
+
|
|
156
|
+
Visit the official SecretShield homepage for an overview of the project, features, releases, and more:
|
|
157
|
+
|
|
158
|
+
**🌐 [therealsecretshield.freebuff.app](https://therealsecretshield.freebuff.app/)**
|
|
@@ -129,10 +129,63 @@ def test_rules_check_success(tmp_path, monkeypatch, capsys):
|
|
|
129
129
|
exit_code = main(["rules", "check"])
|
|
130
130
|
assert exit_code == 0
|
|
131
131
|
out = capsys.readouterr().out
|
|
132
|
-
assert "2
|
|
132
|
+
assert "2 rules loaded" in out
|
|
133
133
|
assert "All patterns are valid" in out
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
# --- Regression tests: singular/plural grammar in rule-count output --------
|
|
137
|
+
# (0.7.1: these counts used to always print the literal "rule(s)"
|
|
138
|
+
# regardless of count, e.g. "1 rule(s) loaded" -- fixed to pluralize
|
|
139
|
+
# properly.)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_rules_check_singular_grammar(tmp_path, monkeypatch, capsys):
|
|
143
|
+
monkeypatch.chdir(tmp_path)
|
|
144
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
145
|
+
'[[rules]]\nname = "Only One"\npattern = "ONE_[0-9]{4}"\n'
|
|
146
|
+
)
|
|
147
|
+
main(["rules", "check"])
|
|
148
|
+
out = capsys.readouterr().out
|
|
149
|
+
assert "1 rule loaded" in out
|
|
150
|
+
assert "rule(s)" not in out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_rules_check_plural_grammar(tmp_path, monkeypatch, capsys):
|
|
154
|
+
monkeypatch.chdir(tmp_path)
|
|
155
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
156
|
+
'[[rules]]\nname = "A"\npattern = "AAA_[0-9]{4}"\n'
|
|
157
|
+
'[[rules]]\nname = "B"\npattern = "BBB_[0-9]{4}"\n'
|
|
158
|
+
'[[rules]]\nname = "C"\npattern = "CCC_[0-9]{4}"\n'
|
|
159
|
+
)
|
|
160
|
+
main(["rules", "check"])
|
|
161
|
+
out = capsys.readouterr().out
|
|
162
|
+
assert "3 rules loaded" in out
|
|
163
|
+
assert "rule(s)" not in out
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_rules_list_singular_error_grammar(tmp_path, monkeypatch, capsys):
|
|
167
|
+
monkeypatch.chdir(tmp_path)
|
|
168
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
169
|
+
'[[rules]]\nname = "Broken"\npattern = "["\n'
|
|
170
|
+
)
|
|
171
|
+
main(["rules"])
|
|
172
|
+
out = capsys.readouterr().out
|
|
173
|
+
assert "1 rule failed to load" in out
|
|
174
|
+
assert "rule(s)" not in out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_scan_warning_singular_grammar(tmp_path, monkeypatch, capsys):
|
|
178
|
+
monkeypatch.chdir(tmp_path)
|
|
179
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
180
|
+
'[[rules]]\nname = "Broken"\npattern = "["\n'
|
|
181
|
+
)
|
|
182
|
+
(tmp_path / "clean.py").write_text("print('hello')\n")
|
|
183
|
+
main(["scan", "."])
|
|
184
|
+
err = capsys.readouterr().err
|
|
185
|
+
assert "1 invalid custom rule in" in err
|
|
186
|
+
assert "rule(s)" not in err
|
|
187
|
+
|
|
188
|
+
|
|
136
189
|
def test_rules_check_reports_errors(tmp_path, monkeypatch, capsys):
|
|
137
190
|
monkeypatch.chdir(tmp_path)
|
|
138
191
|
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
v0.8.0: regression tests for the custom-rule self-detection bug fix,
|
|
3
|
+
plus tests for the new `secretshield rule test` command.
|
|
4
|
+
|
|
5
|
+
Root cause (confirmed by direct reproduction before the fix): a custom
|
|
6
|
+
rule's `pattern` field, by design, resembles the very secret shape
|
|
7
|
+
it's meant to catch. `.secretshield-rules.toml` is an ordinary `.toml`
|
|
8
|
+
file, so a plain directory scan swept it in as regular project
|
|
9
|
+
content -- meaning a rule whose pattern looked enough like a real
|
|
10
|
+
secret (or whose example value happened to trip built-in entropy
|
|
11
|
+
detection) caused the rules file to report itself as a finding.
|
|
12
|
+
|
|
13
|
+
The fix: SecretShield's own meta-files (.secretshield-rules.toml,
|
|
14
|
+
secretshield.toml, .secretshieldignore, .secretshield-baseline.json)
|
|
15
|
+
are now skipped by name during a default directory scan, the same way
|
|
16
|
+
_SKIP_DIR_NAMES already skips well-known directories -- not a broad
|
|
17
|
+
"skip all .toml files" exclusion. --no-ignore re-includes them;
|
|
18
|
+
explicitly scanning one by path is unaffected.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import subprocess
|
|
24
|
+
|
|
25
|
+
from secretshield.cli import main
|
|
26
|
+
|
|
27
|
+
SELF_RESEMBLING_PATTERN = "MYAPP_abc123def456ghi789jkl012mno345"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _init_repo(path):
|
|
31
|
+
subprocess.run(["git", "init", "-q"], cwd=path, check=True)
|
|
32
|
+
subprocess.run(["git", "config", "user.email", "t@t.com"], cwd=path, check=True)
|
|
33
|
+
subprocess.run(["git", "config", "user.name", "T"], cwd=path, check=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# --- 1 & 2: reproduce, then confirm fixed -----------------------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_custom_rule_pattern_in_source_file_is_still_detected(tmp_path, monkeypatch):
|
|
40
|
+
"""A custom rule must still catch its intended pattern in real
|
|
41
|
+
project files -- the fix must not weaken detection."""
|
|
42
|
+
monkeypatch.chdir(tmp_path)
|
|
43
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
44
|
+
f'[[rules]]\nname = "My Test Rule"\npattern = "{SELF_RESEMBLING_PATTERN}"\n'
|
|
45
|
+
)
|
|
46
|
+
(tmp_path / "app.py").write_text(f'API_KEY = "{SELF_RESEMBLING_PATTERN}"\n')
|
|
47
|
+
|
|
48
|
+
exit_code = main(["scan", "."])
|
|
49
|
+
assert exit_code == 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_rules_file_does_not_self_detect(tmp_path, monkeypatch, capsys):
|
|
53
|
+
"""The core bug: a rule whose pattern resembles a real secret must
|
|
54
|
+
not cause .secretshield-rules.toml to report itself."""
|
|
55
|
+
monkeypatch.chdir(tmp_path)
|
|
56
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
57
|
+
f'[[rules]]\nname = "My Test Rule"\npattern = "{SELF_RESEMBLING_PATTERN}"\n'
|
|
58
|
+
)
|
|
59
|
+
# No other files -- if anything is reported, it can only be the
|
|
60
|
+
# rules file self-matching.
|
|
61
|
+
exit_code = main(["scan", ".", "--json"])
|
|
62
|
+
assert exit_code == 0
|
|
63
|
+
|
|
64
|
+
import json
|
|
65
|
+
|
|
66
|
+
out = capsys.readouterr().out
|
|
67
|
+
payload = json.loads(out)
|
|
68
|
+
assert payload["secrets_found"] == 0
|
|
69
|
+
assert payload["files_scanned"] == 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_rules_file_excluded_even_from_files_scanned_count(tmp_path, monkeypatch, capsys):
|
|
73
|
+
monkeypatch.chdir(tmp_path)
|
|
74
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
75
|
+
'[[rules]]\nname = "X"\npattern = "AAA_[0-9]{6}"\n'
|
|
76
|
+
)
|
|
77
|
+
(tmp_path / "clean.py").write_text("print('hello')\n")
|
|
78
|
+
main(["scan", ".", "--json"])
|
|
79
|
+
|
|
80
|
+
import json
|
|
81
|
+
|
|
82
|
+
out = capsys.readouterr().out
|
|
83
|
+
payload = json.loads(out)
|
|
84
|
+
assert payload["files_scanned"] == 1 # only clean.py, not the rules file
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --- 4 & 5: multiple custom rules + real detection ---------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_multiple_custom_rules_no_self_detection_any_of_them(tmp_path, monkeypatch, capsys):
|
|
91
|
+
monkeypatch.chdir(tmp_path)
|
|
92
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
93
|
+
'[[rules]]\nname = "Rule A"\npattern = "AAA_bbbbbbbbbbbbbbbbbbbbbbbb"\n'
|
|
94
|
+
'[[rules]]\nname = "Rule B"\npattern = "BBB_cccccccccccccccccccccc"\n'
|
|
95
|
+
)
|
|
96
|
+
main(["scan", ".", "--json"])
|
|
97
|
+
|
|
98
|
+
import json
|
|
99
|
+
|
|
100
|
+
out = capsys.readouterr().out
|
|
101
|
+
payload = json.loads(out)
|
|
102
|
+
assert payload["secrets_found"] == 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_multiple_custom_rules_still_detect_real_matches(tmp_path, monkeypatch, capsys):
|
|
106
|
+
monkeypatch.chdir(tmp_path)
|
|
107
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
108
|
+
'[[rules]]\nname = "Rule A"\npattern = "AAA_[0-9]{6}"\n'
|
|
109
|
+
'[[rules]]\nname = "Rule B"\npattern = "BBB_[0-9]{6}"\n'
|
|
110
|
+
)
|
|
111
|
+
(tmp_path / "app.py").write_text('a = "AAA_123456"\nb = "BBB_654321"\n')
|
|
112
|
+
main(["scan", ".", "--json"])
|
|
113
|
+
|
|
114
|
+
import json
|
|
115
|
+
|
|
116
|
+
out = capsys.readouterr().out
|
|
117
|
+
payload = json.loads(out)
|
|
118
|
+
kinds = {m["kind"] for m in payload["matches"]}
|
|
119
|
+
assert {"Rule A", "Rule B"}.issubset(kinds)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --- 6: built-in rules unaffected --------------------------------------------
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_built_in_detection_still_works(tmp_path, monkeypatch, capsys):
|
|
126
|
+
monkeypatch.chdir(tmp_path)
|
|
127
|
+
aws_key = "AKIAABCDEFGHIJKLMNOP"
|
|
128
|
+
(tmp_path / "app.py").write_text(f'x = "{aws_key}"\n')
|
|
129
|
+
main(["scan", ".", "--json"])
|
|
130
|
+
|
|
131
|
+
import json
|
|
132
|
+
|
|
133
|
+
out = capsys.readouterr().out
|
|
134
|
+
payload = json.loads(out)
|
|
135
|
+
assert payload["matches"][0]["kind"] == "aws_access_key_id"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# --- 7: broken custom rules behave as before ---------------------------------
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_broken_custom_rule_still_reported_via_rules_check(tmp_path, monkeypatch, capsys):
|
|
142
|
+
monkeypatch.chdir(tmp_path)
|
|
143
|
+
(tmp_path / ".secretshield-rules.toml").write_text('[[rules]]\nname = "Bad"\npattern = "["\n')
|
|
144
|
+
exit_code = main(["rules", "check"])
|
|
145
|
+
assert exit_code == 1
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_broken_custom_rule_does_not_block_scan(tmp_path, monkeypatch):
|
|
149
|
+
monkeypatch.chdir(tmp_path)
|
|
150
|
+
(tmp_path / ".secretshield-rules.toml").write_text('[[rules]]\nname = "Bad"\npattern = "["\n')
|
|
151
|
+
(tmp_path / "clean.py").write_text("print('hello')\n")
|
|
152
|
+
assert main(["scan", "."]) == 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# --- --no-ignore re-includes meta-files --------------------------------------
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_no_ignore_flag_allows_scanning_meta_files_again(tmp_path, monkeypatch, capsys):
|
|
159
|
+
monkeypatch.chdir(tmp_path)
|
|
160
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
161
|
+
f'[[rules]]\nname = "X"\npattern = "{SELF_RESEMBLING_PATTERN}"\n'
|
|
162
|
+
)
|
|
163
|
+
main(["scan", ".", "--no-ignore", "--json"])
|
|
164
|
+
|
|
165
|
+
import json
|
|
166
|
+
|
|
167
|
+
out = capsys.readouterr().out
|
|
168
|
+
payload = json.loads(out)
|
|
169
|
+
# With --no-ignore, the rules file IS scanned again, and since its
|
|
170
|
+
# own pattern text resembles a real secret it's expected to
|
|
171
|
+
# self-match -- this proves --no-ignore genuinely re-includes it,
|
|
172
|
+
# rather than the exclusion being silently unconditional.
|
|
173
|
+
assert payload["files_scanned"] == 1
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_explicit_direct_scan_of_rules_file_still_works(tmp_path, monkeypatch):
|
|
177
|
+
monkeypatch.chdir(tmp_path)
|
|
178
|
+
rules_file = tmp_path / ".secretshield-rules.toml"
|
|
179
|
+
rules_file.write_text(f'[[rules]]\nname = "X"\npattern = "{SELF_RESEMBLING_PATTERN}"\n')
|
|
180
|
+
# Explicitly naming the file as the scan target still works --
|
|
181
|
+
# only the default directory *walk* excludes it.
|
|
182
|
+
exit_code = main(["scan", str(rules_file)])
|
|
183
|
+
assert exit_code == 1
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# --- secretshield.toml / .secretshieldignore / baseline also excluded -------
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_secretshield_toml_not_scanned_by_default(tmp_path, monkeypatch, capsys):
|
|
190
|
+
monkeypatch.chdir(tmp_path)
|
|
191
|
+
(tmp_path / "secretshield.toml").write_text("[scan]\nentropy_threshold = 4.2\n")
|
|
192
|
+
main(["scan", ".", "--json"])
|
|
193
|
+
|
|
194
|
+
import json
|
|
195
|
+
|
|
196
|
+
out = capsys.readouterr().out
|
|
197
|
+
payload = json.loads(out)
|
|
198
|
+
assert payload["files_scanned"] == 0
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_secretshieldignore_not_scanned_by_default(tmp_path, monkeypatch, capsys):
|
|
202
|
+
monkeypatch.chdir(tmp_path)
|
|
203
|
+
(tmp_path / ".secretshieldignore").write_text("vendor/\n")
|
|
204
|
+
main(["scan", ".", "--json"])
|
|
205
|
+
|
|
206
|
+
import json
|
|
207
|
+
|
|
208
|
+
out = capsys.readouterr().out
|
|
209
|
+
payload = json.loads(out)
|
|
210
|
+
assert payload["files_scanned"] == 0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_baseline_file_not_scanned_by_default(tmp_path, monkeypatch, capsys):
|
|
214
|
+
monkeypatch.chdir(tmp_path)
|
|
215
|
+
from secretshield.baseline import write_baseline
|
|
216
|
+
|
|
217
|
+
write_baseline(tmp_path, [{"id": "a" * 64, "file": "app.py", "kind": "x", "line": 1}])
|
|
218
|
+
main(["scan", ".", "--json"])
|
|
219
|
+
|
|
220
|
+
import json
|
|
221
|
+
|
|
222
|
+
out = capsys.readouterr().out
|
|
223
|
+
payload = json.loads(out)
|
|
224
|
+
assert payload["files_scanned"] == 0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# --- --staged / scan-staged (the pre-commit hook path) -----------------------
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_staged_rules_file_does_not_self_block(tmp_path, monkeypatch):
|
|
231
|
+
monkeypatch.chdir(tmp_path)
|
|
232
|
+
_init_repo(tmp_path)
|
|
233
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
234
|
+
f'[[rules]]\nname = "X"\npattern = "{SELF_RESEMBLING_PATTERN}"\n'
|
|
235
|
+
)
|
|
236
|
+
subprocess.run(["git", "add", ".secretshield-rules.toml"], cwd=tmp_path, check=True)
|
|
237
|
+
|
|
238
|
+
exit_code = main(["scan", "--staged"])
|
|
239
|
+
assert exit_code == 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_scan_staged_command_does_not_self_block(tmp_path, monkeypatch):
|
|
243
|
+
# This is the exact path the installed pre-commit hook invokes.
|
|
244
|
+
monkeypatch.chdir(tmp_path)
|
|
245
|
+
_init_repo(tmp_path)
|
|
246
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
247
|
+
f'[[rules]]\nname = "X"\npattern = "{SELF_RESEMBLING_PATTERN}"\n'
|
|
248
|
+
)
|
|
249
|
+
subprocess.run(["git", "add", ".secretshield-rules.toml"], cwd=tmp_path, check=True)
|
|
250
|
+
|
|
251
|
+
exit_code = main(["scan-staged"])
|
|
252
|
+
assert exit_code == 0
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_real_commit_of_rules_file_succeeds(tmp_path, monkeypatch):
|
|
256
|
+
"""The exact real-world scenario reported: committing an update to
|
|
257
|
+
your own custom-rules file must not get blocked by itself."""
|
|
258
|
+
monkeypatch.chdir(tmp_path)
|
|
259
|
+
_init_repo(tmp_path)
|
|
260
|
+
main(["install-hook"])
|
|
261
|
+
|
|
262
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
263
|
+
f'[[rules]]\nname = "X"\npattern = "{SELF_RESEMBLING_PATTERN}"\n'
|
|
264
|
+
)
|
|
265
|
+
subprocess.run(["git", "add", ".secretshield-rules.toml"], cwd=tmp_path, check=True)
|
|
266
|
+
|
|
267
|
+
result = subprocess.run(
|
|
268
|
+
["git", "commit", "-m", "add custom rule"],
|
|
269
|
+
cwd=tmp_path,
|
|
270
|
+
capture_output=True,
|
|
271
|
+
text=True,
|
|
272
|
+
)
|
|
273
|
+
assert result.returncode == 0
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# --- `secretshield rule test` ------------------------------------------------
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_rule_test_non_interactive_refuses(tmp_path, monkeypatch):
|
|
280
|
+
monkeypatch.chdir(tmp_path)
|
|
281
|
+
exit_code = main(["rule", "test"])
|
|
282
|
+
assert exit_code == 1
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_rule_test_no_rules_file(tmp_path, monkeypatch, capsys):
|
|
286
|
+
monkeypatch.chdir(tmp_path)
|
|
287
|
+
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
|
|
288
|
+
exit_code = main(["rule", "test"])
|
|
289
|
+
assert exit_code == 0
|
|
290
|
+
out = capsys.readouterr().out
|
|
291
|
+
assert "No .secretshield-rules.toml found" in out
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_rule_test_reports_match(tmp_path, monkeypatch, capsys):
|
|
295
|
+
monkeypatch.chdir(tmp_path)
|
|
296
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
297
|
+
f'[[rules]]\nname = "My Rule"\npattern = "{SELF_RESEMBLING_PATTERN}"\n'
|
|
298
|
+
)
|
|
299
|
+
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
|
|
300
|
+
monkeypatch.setattr("builtins.input", lambda prompt="": SELF_RESEMBLING_PATTERN)
|
|
301
|
+
|
|
302
|
+
exit_code = main(["rule", "test"])
|
|
303
|
+
assert exit_code == 0
|
|
304
|
+
out = capsys.readouterr().out
|
|
305
|
+
assert "Matched: My Rule" in out
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_rule_test_reports_no_match(tmp_path, monkeypatch, capsys):
|
|
309
|
+
monkeypatch.chdir(tmp_path)
|
|
310
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
311
|
+
'[[rules]]\nname = "My Rule"\npattern = "AAA_[0-9]{6}"\n'
|
|
312
|
+
)
|
|
313
|
+
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
|
|
314
|
+
monkeypatch.setattr("builtins.input", lambda prompt="": "totally unrelated text")
|
|
315
|
+
|
|
316
|
+
exit_code = main(["rule", "test"])
|
|
317
|
+
assert exit_code == 0
|
|
318
|
+
out = capsys.readouterr().out
|
|
319
|
+
assert "No match: My Rule" in out
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_rule_test_never_prints_raw_matched_value(tmp_path, monkeypatch, capsys):
|
|
323
|
+
monkeypatch.chdir(tmp_path)
|
|
324
|
+
(tmp_path / ".secretshield-rules.toml").write_text(
|
|
325
|
+
f'[[rules]]\nname = "My Rule"\npattern = "{SELF_RESEMBLING_PATTERN}"\n'
|
|
326
|
+
)
|
|
327
|
+
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
|
|
328
|
+
monkeypatch.setattr("builtins.input", lambda prompt="": SELF_RESEMBLING_PATTERN)
|
|
329
|
+
|
|
330
|
+
main(["rule", "test"])
|
|
331
|
+
out = capsys.readouterr().out
|
|
332
|
+
assert "value masked" in out
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# --- 8: existing suite remains green (spot-checked here too) ---------------
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_scan_with_no_config_files_at_all_still_works(tmp_path, monkeypatch):
|
|
339
|
+
monkeypatch.chdir(tmp_path)
|
|
340
|
+
(tmp_path / "clean.py").write_text("print('nothing here')\n")
|
|
341
|
+
assert main(["scan", "."]) == 0
|
|
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
|
|
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
|