secretshield 0.6.0__tar.gz → 0.7.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.
Files changed (57) hide show
  1. secretshield-0.7.0/PKG-INFO +147 -0
  2. secretshield-0.7.0/README.md +121 -0
  3. {secretshield-0.6.0 → secretshield-0.7.0}/pyproject.toml +1 -1
  4. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/__init__.py +1 -1
  5. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/autofix/fixer.py +13 -1
  6. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/cli.py +165 -1
  7. secretshield-0.7.0/secretshield/custom_rules.py +163 -0
  8. secretshield-0.7.0/secretshield.egg-info/PKG-INFO +147 -0
  9. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield.egg-info/SOURCES.txt +3 -0
  10. secretshield-0.7.0/tests/test_custom_rules.py +240 -0
  11. secretshield-0.7.0/tests/test_custom_rules_cli_integration.py +305 -0
  12. secretshield-0.6.0/PKG-INFO +0 -428
  13. secretshield-0.6.0/README.md +0 -402
  14. secretshield-0.6.0/secretshield.egg-info/PKG-INFO +0 -428
  15. {secretshield-0.6.0 → secretshield-0.7.0}/LICENSE +0 -0
  16. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/autofix/__init__.py +0 -0
  17. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/autofix/env.py +0 -0
  18. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/autofix/gitignore.py +0 -0
  19. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/autofix/javascript.py +0 -0
  20. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/autofix/python.py +0 -0
  21. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/baseline.py +0 -0
  22. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/color.py +0 -0
  23. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/config.py +0 -0
  24. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/detector.py +0 -0
  25. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/git/__init__.py +0 -0
  26. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/git/hooks.py +0 -0
  27. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/github/__init__.py +0 -0
  28. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/github/actions.py +0 -0
  29. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/guardian.py +0 -0
  30. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/ignorefile.py +0 -0
  31. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/notifications.py +0 -0
  32. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/patterns.py +0 -0
  33. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/project_config.py +0 -0
  34. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/redactor.py +0 -0
  35. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield.egg-info/dependency_links.txt +0 -0
  36. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield.egg-info/entry_points.txt +0 -0
  37. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield.egg-info/requires.txt +0 -0
  38. {secretshield-0.6.0 → secretshield-0.7.0}/secretshield.egg-info/top_level.txt +0 -0
  39. {secretshield-0.6.0 → secretshield-0.7.0}/setup.cfg +0 -0
  40. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_autofix.py +0 -0
  41. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_baseline.py +0 -0
  42. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_color.py +0 -0
  43. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_color_cli_integration.py +0 -0
  44. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_detector.py +0 -0
  45. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_git_hooks.py +0 -0
  46. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_github_action.py +0 -0
  47. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_ignorefile.py +0 -0
  48. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_ignorefile_cli_integration.py +0 -0
  49. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_javascript_autofix.py +0 -0
  50. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_logging.py +0 -0
  51. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_project_config.py +0 -0
  52. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_redactor.py +0 -0
  53. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_scan.py +0 -0
  54. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_ss_alias_and_creator.py +0 -0
  55. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_stdout.py +0 -0
  56. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_v030_cli_integration.py +0 -0
  57. {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_v040_cli_integration.py +0 -0
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: secretshield
3
+ Version: 0.7.0
4
+ Summary: Stop secrets before they leak -- runtime redaction, multi-language scanning, auto-fix, Git hooks, and CI, all in one CLI.
5
+ Author: Samarth Chugh (Sam3360)
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Sam3360/secretshield
8
+ Project-URL: Repository, https://github.com/Sam3360/secretshield
9
+ Project-URL: Issues, https://github.com/Sam3360/secretshield/issues
10
+ Keywords: security,secrets,redaction,logging,stdout,credentials
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Security
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ <div align="center">
28
+
29
+ # 🛡️ SecretShield
30
+
31
+ **Your secrets shouldn't end up in your terminal, your logs, or your commit history.**
32
+
33
+ [![PyPI version](https://img.shields.io/pypi/v/secretshield?color=blue)](https://pypi.org/project/secretshield/)
34
+ [![Python versions](https://img.shields.io/pypi/pyversions/secretshield)](https://pypi.org/project/secretshield/)
35
+ [![PyPI downloads](https://img.shields.io/pypi/dm/secretshield)](https://pypi.org/project/secretshield/)
36
+ [![License: MIT](https://img.shields.io/pypi/l/secretshield)](LICENSE)
37
+
38
+ [Install](#install) · [Demo](#demo) · [Try it](#try-it) · [Full usage guide →](https://github.com/Sam3360/secretshield/blob/main/USAGE.md)
39
+
40
+ </div>
41
+
42
+ ---
43
+
44
+ You've done this. Everyone has: a stray `print()` left in from
45
+ debugging, a log line that dumps a config dict, a hardcoded key that
46
+ slips past review. SecretShield catches it before it leaves your
47
+ machine — redacted from your terminal output automatically, scanned
48
+ out of your codebase on demand, and blocked from your commits if it
49
+ gets that far.
50
+
51
+ ## Demo
52
+
53
+ [![SecretShield Demo](https://img.youtube.com/vi/g95lNIhWsXM/maxresdefault.jpg)](https://youtu.be/g95lNIhWsXM)
54
+
55
+ **[▶ Watch the full demo on YouTube](https://youtu.be/g95lNIhWsXM)**
56
+
57
+ ## Install
58
+
59
+ ```bash
60
+ pip install secretshield
61
+ ```
62
+
63
+ ## Try it
64
+
65
+ ```python
66
+ import secretshield
67
+
68
+ api_key = "sk-example1234567890abcdefFAKEKEY"
69
+ print("API key:", api_key)
70
+ ```
71
+
72
+ ```text
73
+ API key: ********
74
+ ⚠ secretshield: Potential secret detected and redacted.
75
+ ```
76
+
77
+ No config, no code changes — the moment you import it, stdout, stderr,
78
+ and `logging` are protected.
79
+
80
+ Scan a project:
81
+
82
+ ```bash
83
+ secretshield scan .
84
+ ```
85
+
86
+ Set a project up in one step — config file, Git hook, CI workflow:
87
+
88
+ ```bash
89
+ secretshield init
90
+ ```
91
+
92
+ *(Every command also works via the shorter `ss` alias.)*
93
+
94
+ ## What it does
95
+
96
+ | | |
97
+ |---|---|
98
+ | 🖥️ **Runtime protection** | `import secretshield` redacts secrets from `stdout`, `stderr`, and `logging` automatically |
99
+ | 🔍 **Static scanning** | `secretshield scan .` finds hardcoded secrets across Python, JS/TS, HTML, YAML, `.env`, and more |
100
+ | 🔧 **Auto-Fix** | `scan . --fix` moves a hardcoded Python or JS/TS secret into `.env` and rewrites the code — only when it's unambiguous |
101
+ | 🪝 **Git hook** | `install-hook` blocks a commit before a secret reaches your history |
102
+ | ⚙️ **GitHub Actions** | `github-action` generates a workflow that scans every push and PR |
103
+
104
+ No required dependencies, no telemetry, no network calls. Everything
105
+ runs locally, in your own process.
106
+
107
+ ## Custom rules
108
+
109
+ Have a credential format of your own? Drop a
110
+ `.secretshield-rules.toml` in your project root:
111
+
112
+ ```toml
113
+ [[rules]]
114
+ name = "My API Key"
115
+ pattern = "MYAPP_[A-Za-z0-9]{32}"
116
+ ```
117
+
118
+ `secretshield scan .` picks it up automatically — no flag needed.
119
+ Validate the file with `secretshield rules check`, or create a rule
120
+ interactively with `secretshield rule create`.
121
+
122
+ For the full command reference, configuration options, and exactly how
123
+ Auto-Fix decides what's safe to rewrite, see the
124
+ **[usage guide](https://github.com/Sam3360/secretshield/blob/main/USAGE.md)**.
125
+
126
+ ## Limitations
127
+
128
+ Runtime protection covers this Python process's `stdout`/`stderr`/
129
+ `logging` — not screenshots, the clipboard, or other applications.
130
+ Auto-Fix only rewrites Python and JS/TS, and only unambiguous
131
+ assignments; anything less certain is reported, not modified. Treat
132
+ SecretShield as a strong safety net, not a replacement for proper
133
+ secret management. Details in the [usage guide](https://github.com/Sam3360/secretshield/blob/main/USAGE.md#limitations).
134
+
135
+ ## Contributing
136
+
137
+ Issues and PRs welcome. Add tests for new detection patterns or
138
+ behavior changes, use only fake credentials in tests/examples, and run
139
+ `pytest` before opening a PR.
140
+
141
+ ## [☕](https://github.com/sponsors/Sam3360/) Get me a coffee
142
+
143
+ If you find this project useful, consider [supporting its development through GitHub Sponsors](https://github.com/sponsors/Sam3360/).
144
+
145
+ ## License
146
+
147
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,121 @@
1
+ <div align="center">
2
+
3
+ # 🛡️ SecretShield
4
+
5
+ **Your secrets shouldn't end up in your terminal, your logs, or your commit history.**
6
+
7
+ [![PyPI version](https://img.shields.io/pypi/v/secretshield?color=blue)](https://pypi.org/project/secretshield/)
8
+ [![Python versions](https://img.shields.io/pypi/pyversions/secretshield)](https://pypi.org/project/secretshield/)
9
+ [![PyPI downloads](https://img.shields.io/pypi/dm/secretshield)](https://pypi.org/project/secretshield/)
10
+ [![License: MIT](https://img.shields.io/pypi/l/secretshield)](LICENSE)
11
+
12
+ [Install](#install) · [Demo](#demo) · [Try it](#try-it) · [Full usage guide →](https://github.com/Sam3360/secretshield/blob/main/USAGE.md)
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ You've done this. Everyone has: a stray `print()` left in from
19
+ debugging, a log line that dumps a config dict, a hardcoded key that
20
+ slips past review. SecretShield catches it before it leaves your
21
+ machine — redacted from your terminal output automatically, scanned
22
+ out of your codebase on demand, and blocked from your commits if it
23
+ gets that far.
24
+
25
+ ## Demo
26
+
27
+ [![SecretShield Demo](https://img.youtube.com/vi/g95lNIhWsXM/maxresdefault.jpg)](https://youtu.be/g95lNIhWsXM)
28
+
29
+ **[▶ Watch the full demo on YouTube](https://youtu.be/g95lNIhWsXM)**
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install secretshield
35
+ ```
36
+
37
+ ## Try it
38
+
39
+ ```python
40
+ import secretshield
41
+
42
+ api_key = "sk-example1234567890abcdefFAKEKEY"
43
+ print("API key:", api_key)
44
+ ```
45
+
46
+ ```text
47
+ API key: ********
48
+ ⚠ secretshield: Potential secret detected and redacted.
49
+ ```
50
+
51
+ No config, no code changes — the moment you import it, stdout, stderr,
52
+ and `logging` are protected.
53
+
54
+ Scan a project:
55
+
56
+ ```bash
57
+ secretshield scan .
58
+ ```
59
+
60
+ Set a project up in one step — config file, Git hook, CI workflow:
61
+
62
+ ```bash
63
+ secretshield init
64
+ ```
65
+
66
+ *(Every command also works via the shorter `ss` alias.)*
67
+
68
+ ## What it does
69
+
70
+ | | |
71
+ |---|---|
72
+ | 🖥️ **Runtime protection** | `import secretshield` redacts secrets from `stdout`, `stderr`, and `logging` automatically |
73
+ | 🔍 **Static scanning** | `secretshield scan .` finds hardcoded secrets across Python, JS/TS, HTML, YAML, `.env`, and more |
74
+ | 🔧 **Auto-Fix** | `scan . --fix` moves a hardcoded Python or JS/TS secret into `.env` and rewrites the code — only when it's unambiguous |
75
+ | 🪝 **Git hook** | `install-hook` blocks a commit before a secret reaches your history |
76
+ | ⚙️ **GitHub Actions** | `github-action` generates a workflow that scans every push and PR |
77
+
78
+ No required dependencies, no telemetry, no network calls. Everything
79
+ runs locally, in your own process.
80
+
81
+ ## Custom rules
82
+
83
+ Have a credential format of your own? Drop a
84
+ `.secretshield-rules.toml` in your project root:
85
+
86
+ ```toml
87
+ [[rules]]
88
+ name = "My API Key"
89
+ pattern = "MYAPP_[A-Za-z0-9]{32}"
90
+ ```
91
+
92
+ `secretshield scan .` picks it up automatically — no flag needed.
93
+ Validate the file with `secretshield rules check`, or create a rule
94
+ interactively with `secretshield rule create`.
95
+
96
+ For the full command reference, configuration options, and exactly how
97
+ Auto-Fix decides what's safe to rewrite, see the
98
+ **[usage guide](https://github.com/Sam3360/secretshield/blob/main/USAGE.md)**.
99
+
100
+ ## Limitations
101
+
102
+ Runtime protection covers this Python process's `stdout`/`stderr`/
103
+ `logging` — not screenshots, the clipboard, or other applications.
104
+ Auto-Fix only rewrites Python and JS/TS, and only unambiguous
105
+ assignments; anything less certain is reported, not modified. Treat
106
+ SecretShield as a strong safety net, not a replacement for proper
107
+ secret management. Details in the [usage guide](https://github.com/Sam3360/secretshield/blob/main/USAGE.md#limitations).
108
+
109
+ ## Contributing
110
+
111
+ Issues and PRs welcome. Add tests for new detection patterns or
112
+ behavior changes, use only fake credentials in tests/examples, and run
113
+ `pytest` before opening a PR.
114
+
115
+ ## [☕](https://github.com/sponsors/Sam3360/) Get me a coffee
116
+
117
+ If you find this project useful, consider [supporting its development through GitHub Sponsors](https://github.com/sponsors/Sam3360/).
118
+
119
+ ## License
120
+
121
+ MIT — see [LICENSE](LICENSE).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "secretshield"
7
- version = "0.6.0"
7
+ version = "0.7.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"
@@ -23,7 +23,7 @@ from .detector import Match, detect
23
23
  from .guardian import disable, enable, is_enabled
24
24
  from .redactor import redact
25
25
 
26
- __version__ = "0.6.0"
26
+ __version__ = "0.7.0"
27
27
 
28
28
  __all__ = [
29
29
  "__version__",
@@ -14,6 +14,7 @@ from pathlib import Path
14
14
  from typing import Callable
15
15
 
16
16
  from ..detector import detect
17
+ from ..custom_rules import scan_text_with_rules
17
18
  from . import javascript as jsfix
18
19
  from . import python as pyfix
19
20
  from .env import append_env_example_var, append_env_var, unique_env_key
@@ -86,6 +87,7 @@ def run_autofix(
86
87
  entropy_threshold: float = 4.2,
87
88
  print_func: Callable[[str], None] = _default_print,
88
89
  input_func: Callable[[str], str] = _default_input,
90
+ custom_rules: list | None = None,
89
91
  ) -> FixSummary:
90
92
  """
91
93
  Run the interactive Auto-Fix flow.
@@ -94,6 +96,12 @@ def run_autofix(
94
96
  `secretshield.detector.Match` objects found in that file (already
95
97
  filtered for placeholders by the caller). Files are processed in
96
98
  sorted order for deterministic output.
99
+
100
+ `custom_rules` (optional): the same `.secretshield-rules.toml`
101
+ rules used to build `findings_by_file`, if any. Needed here too so
102
+ that the internal re-scan (used to get fresh, accurate line numbers
103
+ after an `import os` insertion) can re-locate a match that only a
104
+ custom rule -- not a built-in pattern -- would find.
97
105
  """
98
106
  summary = FixSummary()
99
107
  env_path = project_root / ".env"
@@ -163,9 +171,13 @@ def run_autofix(
163
171
  source = pyfix.insert_import_os(source)
164
172
 
165
173
  # Re-scan the (possibly import-adjusted) source fresh, so line
166
- # numbers reflect the current content exactly.
174
+ # numbers reflect the current content exactly. Custom rules are
175
+ # merged in too, so a match only a custom rule (not a built-in
176
+ # pattern) would find isn't lost here.
167
177
  try:
168
178
  current_matches = detect(source, entropy_threshold=entropy_threshold)
179
+ if custom_rules:
180
+ current_matches = current_matches + scan_text_with_rules(source, custom_rules)
169
181
  except Exception:
170
182
  current_matches = []
171
183
 
@@ -408,6 +408,7 @@ def _iter_scan_sources(args: argparse.Namespace, target: Path, include_patterns:
408
408
 
409
409
  def _run_scan(args: argparse.Namespace) -> int:
410
410
  from .baseline import baseline_exists, compute_finding_id, load_baseline_ids, write_baseline
411
+ from .custom_rules import load_custom_rules, scan_text_with_rules
411
412
  from .project_config import load_project_config
412
413
 
413
414
  project_cfg = load_project_config()
@@ -428,6 +429,20 @@ def _run_scan(args: argparse.Namespace) -> int:
428
429
 
429
430
  exclude_patterns.extend(load_ignore_patterns(Path.cwd()))
430
431
 
432
+ # Custom rules (.secretshield-rules.toml), if present, become
433
+ # additional detectors alongside the built-in patterns -- no flag
434
+ # required. A broken rules file never blocks the scan (valid rules
435
+ # in the same file still load and run); it's surfaced as a single
436
+ # warning pointing at `rules check` rather than silently ignored.
437
+ custom_rules, custom_rule_errors = load_custom_rules(Path.cwd())
438
+ if custom_rule_errors:
439
+ print(
440
+ f"secretshield: warning: {len(custom_rule_errors)} invalid custom "
441
+ f"rule(s) in .secretshield-rules.toml -- run `secretshield rules "
442
+ f"check` for details.",
443
+ file=sys.stderr,
444
+ )
445
+
431
446
  json_output = bool(args.json) or (project_cfg.output_format == "json")
432
447
 
433
448
  target = Path(args.path)
@@ -455,6 +470,11 @@ def _run_scan(args: argparse.Namespace) -> int:
455
470
  _progress_update(show_progress, files_scanned)
456
471
 
457
472
  file_matches = _scan_text_matches(text, entropy_threshold=entropy_threshold)
473
+ if custom_rules:
474
+ custom_matches = [
475
+ m for m in scan_text_with_rules(text, custom_rules) if not _is_placeholder(m.value)
476
+ ]
477
+ file_matches = file_matches + custom_matches
458
478
  if file_matches:
459
479
  matches_by_file[display_path] = file_matches
460
480
  for match in file_matches:
@@ -503,7 +523,12 @@ def _run_scan(args: argparse.Namespace) -> int:
503
523
  if getattr(args, "fix", False):
504
524
  from .autofix import run_autofix
505
525
 
506
- summary = run_autofix(project_root, matches_by_file, entropy_threshold=entropy_threshold)
526
+ summary = run_autofix(
527
+ project_root,
528
+ matches_by_file,
529
+ entropy_threshold=entropy_threshold,
530
+ custom_rules=custom_rules,
531
+ )
507
532
  return 1 if summary.fixed < summary.detected else 0
508
533
 
509
534
  if json_output:
@@ -734,6 +759,124 @@ def _cmd_init(args: argparse.Namespace) -> int:
734
759
  return 0
735
760
 
736
761
 
762
+ # --- `rules` / `rule create` commands -----------------------------------
763
+
764
+
765
+ def _cmd_rules_check(args: argparse.Namespace) -> int:
766
+ from .custom_rules import load_custom_rules, rules_file_path
767
+
768
+ use_color = supports_color(sys.stdout)
769
+ path = rules_file_path(Path.cwd())
770
+
771
+ if not path.exists():
772
+ print(f"No {path.name} found.")
773
+ return 0
774
+
775
+ rules, errors = load_custom_rules(Path.cwd())
776
+
777
+ print("SecretShield Rules\n")
778
+
779
+ if errors:
780
+ for err in errors:
781
+ if err.name:
782
+ print(f'{red(chr(0x2717), use_color)} Invalid custom rule "{err.name}"')
783
+ print(f" {err.message}")
784
+ else:
785
+ label = f"rule #{err.rule_index + 1}" if err.rule_index is not None else path.name
786
+ print(f"{red(chr(0x2717), use_color)} {label}: {err.message}")
787
+ return 1
788
+
789
+ print(f"{green(chr(0x2713), use_color)} {path.name}")
790
+ print(f"{green(chr(0x2713), use_color)} {len(rules)} rule(s) loaded")
791
+ print(f"{green(chr(0x2713), use_color)} All patterns are valid")
792
+ return 0
793
+
794
+
795
+ def _cmd_rules_list(args: argparse.Namespace) -> int:
796
+ from .custom_rules import load_custom_rules, rules_file_path
797
+
798
+ path = rules_file_path(Path.cwd())
799
+ if not path.exists():
800
+ print(f"No {path.name} found.")
801
+ return 0
802
+
803
+ rules, errors = load_custom_rules(Path.cwd())
804
+
805
+ if not rules and not errors:
806
+ print("No custom rules defined.")
807
+ return 0
808
+
809
+ if rules:
810
+ print("SecretShield Custom Rules\n")
811
+ for i, rule in enumerate(rules, start=1):
812
+ print(f"{i}. {rule.name}")
813
+ print(f" {rule.pattern}")
814
+
815
+ if errors:
816
+ use_color = supports_color(sys.stdout)
817
+ print(
818
+ f"\n{red(chr(0x2717), use_color)} {len(errors)} rule(s) failed to load "
819
+ f"-- run `secretshield rules check` for details."
820
+ )
821
+ return 0
822
+
823
+
824
+ def _cmd_rules_no_subcommand(args: argparse.Namespace) -> int:
825
+ # `secretshield rules` with no further action -> list.
826
+ return _cmd_rules_list(args)
827
+
828
+
829
+ def _cmd_rule_no_subcommand(args: argparse.Namespace) -> int:
830
+ print("secretshield rule: missing subcommand. Try `secretshield rule create`.", file=sys.stderr)
831
+ return 1
832
+
833
+
834
+ def _cmd_rule_create(args: argparse.Namespace) -> int:
835
+ import re as _re
836
+
837
+ from .custom_rules import append_rule, rules_file_path
838
+
839
+ try:
840
+ interactive = sys.stdin.isatty()
841
+ except Exception:
842
+ interactive = False
843
+
844
+ if not interactive:
845
+ print("secretshield rule create requires an interactive terminal.")
846
+ print("No rule was created.")
847
+ return 1
848
+
849
+ print("SecretShield \u2014 Create custom rule\n")
850
+
851
+ name = input("Rule name: ").strip()
852
+ if not name:
853
+ print("Rule name cannot be empty. No rule was created.", file=sys.stderr)
854
+ return 1
855
+
856
+ while True:
857
+ pattern = input("Pattern: ").strip()
858
+ if not pattern:
859
+ print("Pattern cannot be empty.", file=sys.stderr)
860
+ continue
861
+ try:
862
+ _re.compile(pattern)
863
+ except _re.error as exc:
864
+ print(f"Invalid regular expression: {exc}", file=sys.stderr)
865
+ print("Please try again.\n")
866
+ continue
867
+ break
868
+
869
+ path = rules_file_path(Path.cwd())
870
+ try:
871
+ append_rule(path, name, pattern)
872
+ except Exception as exc:
873
+ print(f"\u2717 Could not write {path.name}: {exc}", file=sys.stderr)
874
+ return 1
875
+
876
+ print(f"{green(chr(0x2713), supports_color(sys.stdout))} Added rule to {path.name}")
877
+ return 0
878
+
879
+
737
880
  # --- argparse wiring -----------------------------------------------------
738
881
 
739
882
 
@@ -925,6 +1068,27 @@ def build_parser() -> argparse.ArgumentParser:
925
1068
  )
926
1069
  init_parser.set_defaults(func=_cmd_init)
927
1070
 
1071
+ rules_parser = subparsers.add_parser(
1072
+ "rules",
1073
+ help="List or validate .secretshield-rules.toml custom detection rules.",
1074
+ )
1075
+ rules_subparsers = rules_parser.add_subparsers(dest="rules_action")
1076
+ rules_check_parser = rules_subparsers.add_parser(
1077
+ "check", help="Validate .secretshield-rules.toml (TOML syntax, required fields, regex validity)."
1078
+ )
1079
+ rules_check_parser.set_defaults(func=_cmd_rules_check)
1080
+ rules_parser.set_defaults(func=_cmd_rules_list)
1081
+
1082
+ rule_parser = subparsers.add_parser(
1083
+ "rule", help="Create a custom detection rule."
1084
+ )
1085
+ rule_parser.set_defaults(func=_cmd_rule_no_subcommand)
1086
+ rule_subparsers = rule_parser.add_subparsers(dest="rule_action")
1087
+ rule_create_parser = rule_subparsers.add_parser(
1088
+ "create", help="Interactively create a new rule in .secretshield-rules.toml."
1089
+ )
1090
+ rule_create_parser.set_defaults(func=_cmd_rule_create)
1091
+
928
1092
  return parser
929
1093
 
930
1094