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.
- secretshield-0.7.0/PKG-INFO +147 -0
- secretshield-0.7.0/README.md +121 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/pyproject.toml +1 -1
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/__init__.py +1 -1
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/autofix/fixer.py +13 -1
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/cli.py +165 -1
- secretshield-0.7.0/secretshield/custom_rules.py +163 -0
- secretshield-0.7.0/secretshield.egg-info/PKG-INFO +147 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield.egg-info/SOURCES.txt +3 -0
- secretshield-0.7.0/tests/test_custom_rules.py +240 -0
- secretshield-0.7.0/tests/test_custom_rules_cli_integration.py +305 -0
- secretshield-0.6.0/PKG-INFO +0 -428
- secretshield-0.6.0/README.md +0 -402
- secretshield-0.6.0/secretshield.egg-info/PKG-INFO +0 -428
- {secretshield-0.6.0 → secretshield-0.7.0}/LICENSE +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/autofix/__init__.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/autofix/env.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/autofix/gitignore.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/autofix/javascript.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/autofix/python.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/baseline.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/color.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/config.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/detector.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/git/__init__.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/git/hooks.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/github/__init__.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/github/actions.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/guardian.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/ignorefile.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/notifications.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/patterns.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/project_config.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield/redactor.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield.egg-info/dependency_links.txt +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield.egg-info/entry_points.txt +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield.egg-info/requires.txt +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/secretshield.egg-info/top_level.txt +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/setup.cfg +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_autofix.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_baseline.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_color.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_color_cli_integration.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_detector.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_git_hooks.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_github_action.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_ignorefile.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_ignorefile_cli_integration.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_javascript_autofix.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_logging.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_project_config.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_redactor.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_scan.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_ss_alias_and_creator.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_stdout.py +0 -0
- {secretshield-0.6.0 → secretshield-0.7.0}/tests/test_v030_cli_integration.py +0 -0
- {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
|
+
[](https://pypi.org/project/secretshield/)
|
|
34
|
+
[](https://pypi.org/project/secretshield/)
|
|
35
|
+
[](https://pypi.org/project/secretshield/)
|
|
36
|
+
[](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
|
+
[](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
|
+
[](https://pypi.org/project/secretshield/)
|
|
8
|
+
[](https://pypi.org/project/secretshield/)
|
|
9
|
+
[](https://pypi.org/project/secretshield/)
|
|
10
|
+
[](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
|
+
[](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.
|
|
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"
|
|
@@ -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(
|
|
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
|
|