credhunter 0.1.0__py3-none-any.whl
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.
- credhunter-0.1.0.dist-info/METADATA +279 -0
- credhunter-0.1.0.dist-info/RECORD +20 -0
- credhunter-0.1.0.dist-info/WHEEL +5 -0
- credhunter-0.1.0.dist-info/entry_points.txt +2 -0
- credhunter-0.1.0.dist-info/top_level.txt +1 -0
- secretguard/__init__.py +0 -0
- secretguard/cli.py +349 -0
- secretguard/detectors/__init__.py +0 -0
- secretguard/detectors/context.py +29 -0
- secretguard/detectors/entropy.py +55 -0
- secretguard/detectors/regex_detector.py +100 -0
- secretguard/masking.py +35 -0
- secretguard/models.py +46 -0
- secretguard/reporters/__init__.py +0 -0
- secretguard/reporters/html_reporter.py +248 -0
- secretguard/reporters/json_reporter.py +22 -0
- secretguard/reporters/pdf_reporter.py +85 -0
- secretguard/rules/secrets.yaml +78 -0
- secretguard/scanner.py +216 -0
- secretguard/scoring.py +64 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: credhunter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automated secrets detection and reporting tool
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: typer>=0.9.0
|
|
8
|
+
Requires-Dist: pyyaml>=6.0
|
|
9
|
+
Requires-Dist: fpdf2>=2.7.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# credhunter
|
|
14
|
+
|
|
15
|
+
credhunter is a Python command-line tool for scanning files and directories for potential hardcoded secrets. It combines pattern-based detection with context and entropy signals, assigns confidence and severity, masks detected values in output, and can generate JSON and HTML reports.
|
|
16
|
+
|
|
17
|
+
credhunter is a detection aid, not a guarantee that every secret will be found. Review findings before taking action, and never use real production credentials in test fixtures.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Scan a single file or directory.
|
|
22
|
+
- Detect configured secret patterns using rules packaged with credhunter.
|
|
23
|
+
- Use context analysis and Shannon entropy as detection signals.
|
|
24
|
+
- Assign confidence scores and severity levels.
|
|
25
|
+
- Filter configured placeholder values to reduce some false positives.
|
|
26
|
+
- Mask detected values in console output and reports.
|
|
27
|
+
- Generate JSON and HTML reports.
|
|
28
|
+
- Configure CI failure behavior with `--fail-on`.
|
|
29
|
+
- Display the installed package version with `--version`.
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- Python 3.10 or newer.
|
|
34
|
+
- Windows, Linux, or macOS with a compatible Python installation.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
### Install from PyPI
|
|
39
|
+
|
|
40
|
+
Once credhunter has been published to PyPI, install it with:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python -m pip install credhunter
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Verify the installation:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
credhunter --version
|
|
50
|
+
credhunter --help
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Install from source for development
|
|
54
|
+
|
|
55
|
+
If you have cloned or downloaded the source repository, open a terminal in the project directory.
|
|
56
|
+
|
|
57
|
+
**Windows PowerShell**
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
python -m venv .venv
|
|
61
|
+
.\.venv\Scripts\Activate.ps1
|
|
62
|
+
python -m pip install --upgrade pip
|
|
63
|
+
python -m pip install -e .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Linux/macOS**
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python3 -m venv .venv
|
|
70
|
+
source .venv/bin/activate
|
|
71
|
+
python -m pip install --upgrade pip
|
|
72
|
+
python -m pip install -e .
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
To install development and test dependencies:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python -m pip install -e ".[dev]"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The editable installation is intended for development. It allows changes to the source code to be used without rebuilding and reinstalling the package.
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
The command accepts a file or directory path as its positional argument.
|
|
86
|
+
|
|
87
|
+
### Show the installed version
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
credhunter --version
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Show help
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
credhunter --help
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Scan the current directory
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
credhunter .
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Scan a directory
|
|
106
|
+
|
|
107
|
+
**Windows PowerShell**
|
|
108
|
+
|
|
109
|
+
```powershell
|
|
110
|
+
credhunter .\examples\vulnerable_project
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Scan a single file
|
|
114
|
+
|
|
115
|
+
```powershell
|
|
116
|
+
credhunter .\path\to\file.py
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Generate JSON and HTML reports
|
|
120
|
+
|
|
121
|
+
```powershell
|
|
122
|
+
credhunter .\examples\vulnerable_project --json report.json --html report.html
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Choose report destinations that are writable. Review generated reports before sharing them, as they can contain sensitive repository details even when detected secret values are masked.
|
|
126
|
+
|
|
127
|
+
### Configure the CI/CD failure threshold
|
|
128
|
+
|
|
129
|
+
```powershell
|
|
130
|
+
credhunter .\examples\vulnerable_project --fail-on high
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Supported thresholds:
|
|
134
|
+
|
|
135
|
+
- `critical`
|
|
136
|
+
- `high`
|
|
137
|
+
- `medium`
|
|
138
|
+
- `low`
|
|
139
|
+
- `none`
|
|
140
|
+
|
|
141
|
+
The command returns exit code `1` when at least one finding meets or exceeds the selected threshold; otherwise, it returns `0`. Use `--fail-on none` to complete a scan without failing based on findings:
|
|
142
|
+
|
|
143
|
+
```powershell
|
|
144
|
+
credhunter .\examples\vulnerable_project --fail-on none
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## How Detection Works
|
|
148
|
+
|
|
149
|
+
credhunter uses several signals to evaluate potential secrets:
|
|
150
|
+
|
|
151
|
+
1. **Pattern matching:** configured regular expressions identify values that match known secret formats.
|
|
152
|
+
2. **Context analysis:** nearby names or text, such as `password`, `token`, and `api_key`, can help assess whether a value is a credential.
|
|
153
|
+
3. **Entropy analysis:** Shannon entropy helps assess how random-looking a string is.
|
|
154
|
+
4. **Confidence and severity:** findings include a confidence score and severity classification.
|
|
155
|
+
5. **Placeholder filtering:** configured placeholder markers can suppress common test or example values.
|
|
156
|
+
6. **Masking:** console output and reports use masked snippets rather than intentionally displaying the complete detected value.
|
|
157
|
+
7. **Fingerprinting:** SHA-256 fingerprints can help identify repeated values without storing the raw secret as the fingerprint.
|
|
158
|
+
|
|
159
|
+
These signals do not guarantee that every secret will be detected or that every finding is correct. False positives and false negatives are possible.
|
|
160
|
+
|
|
161
|
+
## Detection Rules
|
|
162
|
+
|
|
163
|
+
Detection rules are stored in the package at:
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
credhunter/rules/secrets.yaml
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The rule file defines patterns and related metadata used by the scanner. Configured rules may cover credential types such as cloud access keys, tokens, private-key blocks, database connection strings, passwords, and generic API keys. Actual detection depends on the rules and the scanner's filtering and scoring logic.
|
|
170
|
+
|
|
171
|
+
When adding a rule, use a unique ID, a descriptive name, a severity, a detection pattern, and any context keywords required by the rule format. Add or update tests to verify the expected detection and check for false positives.
|
|
172
|
+
|
|
173
|
+
## Reports
|
|
174
|
+
|
|
175
|
+
### Console output
|
|
176
|
+
|
|
177
|
+
The CLI displays findings with details such as severity, confidence, location, and a masked snippet.
|
|
178
|
+
|
|
179
|
+
### JSON report
|
|
180
|
+
|
|
181
|
+
Generate a machine-readable report with:
|
|
182
|
+
|
|
183
|
+
```powershell
|
|
184
|
+
credhunter .\examples\vulnerable_project --json report.json --fail-on none
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### HTML report
|
|
188
|
+
|
|
189
|
+
Generate a human-readable report with:
|
|
190
|
+
|
|
191
|
+
```powershell
|
|
192
|
+
credhunter .\examples\vulnerable_project --html report.html --fail-on none
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
To generate both formats in one scan:
|
|
196
|
+
|
|
197
|
+
```powershell
|
|
198
|
+
credhunter .\examples\vulnerable_project --json report.json --html report.html --fail-on none
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Testing
|
|
202
|
+
|
|
203
|
+
Install the development dependencies:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
python -m pip install -e ".[dev]"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Run the test suite:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
python -m pytest
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Before releasing a version, confirm that all tests pass. Also perform manual checks in a clean virtual environment:
|
|
216
|
+
|
|
217
|
+
1. Run `credhunter --version` and `credhunter --help`.
|
|
218
|
+
2. Scan a test project containing synthetic secrets.
|
|
219
|
+
3. Scan a clean test project and review unexpected findings.
|
|
220
|
+
4. Generate JSON and HTML reports.
|
|
221
|
+
5. Confirm the original synthetic secret is not exposed in console output or either report.
|
|
222
|
+
6. Check exit codes for `--fail-on none` and severity thresholds that should and should not be triggered.
|
|
223
|
+
7. Confirm an installed package can locate its bundled YAML rules.
|
|
224
|
+
|
|
225
|
+
Use only fake, nonfunctional test values in examples and tests.
|
|
226
|
+
|
|
227
|
+
## Security Response Guidance
|
|
228
|
+
|
|
229
|
+
If credhunter identifies a real credential:
|
|
230
|
+
|
|
231
|
+
1. Revoke or rotate the credential promptly.
|
|
232
|
+
2. Remove it from the source code and use an appropriate secrets-management solution.
|
|
233
|
+
3. Review repository history and access logs where appropriate.
|
|
234
|
+
4. Assess whether the credential may have been accessed by an unauthorized party.
|
|
235
|
+
5. Update tests or detection rules if the finding reveals a coverage gap.
|
|
236
|
+
|
|
237
|
+
Removing a secret from the latest version of a file does not necessarily remove it from version-control history.
|
|
238
|
+
|
|
239
|
+
## Known Limitations
|
|
240
|
+
|
|
241
|
+
- Detection depends on configured rules and may miss custom or proprietary credential formats.
|
|
242
|
+
- False positives and false negatives are possible.
|
|
243
|
+
- Placeholder filtering may suppress a value if it contains a configured placeholder marker.
|
|
244
|
+
- Masking should be verified for each output format.
|
|
245
|
+
- A successful scan does not prove that a repository contains no secrets.
|
|
246
|
+
|
|
247
|
+
## Project Structure
|
|
248
|
+
|
|
249
|
+
The core package follows this general layout:
|
|
250
|
+
|
|
251
|
+
```text
|
|
252
|
+
credhunter/
|
|
253
|
+
├── credhunter/
|
|
254
|
+
│ ├── cli.py
|
|
255
|
+
│ ├── scanner.py
|
|
256
|
+
│ ├── masking.py
|
|
257
|
+
│ ├── models.py
|
|
258
|
+
│ ├── scoring.py
|
|
259
|
+
│ ├── detectors/
|
|
260
|
+
│ │ ├── regex_detector.py
|
|
261
|
+
│ │ ├── entropy.py
|
|
262
|
+
│ │ └── context.py
|
|
263
|
+
│ ├── reporters/
|
|
264
|
+
│ │ ├── json_reporter.py
|
|
265
|
+
│ │ ├── html_reporter.py
|
|
266
|
+
│ │ └── pdf_reporter.py
|
|
267
|
+
│ └── rules/
|
|
268
|
+
│ └── secrets.yaml
|
|
269
|
+
├── tests/
|
|
270
|
+
├── examples/
|
|
271
|
+
├── pyproject.toml
|
|
272
|
+
└── README.md
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The exact structure may change as development continues.
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
Add a license file and the corresponding package metadata before publishing if you intend others to use, modify, or redistribute credhunter.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
secretguard/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
secretguard/cli.py,sha256=ludv2zZWtFpf69B1pknUKqgh7c19SgUR6sNhzUHQbRM,10851
|
|
3
|
+
secretguard/masking.py,sha256=v6UcEOc1JAknbtceaNS6N62qT9n8BocQYbJJjbhJe5c,1307
|
|
4
|
+
secretguard/models.py,sha256=zHKskI_nnzXmg30i2seQNg9Wy50f0PJqfxhGYISjiT4,1404
|
|
5
|
+
secretguard/scanner.py,sha256=-o42QKNe_1LknmfMdiLPU-xqhoV85rHwp_WhnYorChs,8456
|
|
6
|
+
secretguard/scoring.py,sha256=hhkkZRjvKhDCVxj9zr5-noeYsEzBRlz_KT_YmvoDEac,2041
|
|
7
|
+
secretguard/detectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
secretguard/detectors/context.py,sha256=h86EqMfrwn1Pc7oZhQapjF9WSjroKySvoAyUjkI_9oY,1184
|
|
9
|
+
secretguard/detectors/entropy.py,sha256=kjRcrJ6lXKi8_2_22P04inRF0YW9pQVv57g8eKzvFvM,1866
|
|
10
|
+
secretguard/detectors/regex_detector.py,sha256=e62ga7jmYAxwmOX0YPoWQjBci90vj9zKgh1tYdtMOKQ,2986
|
|
11
|
+
secretguard/reporters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
secretguard/reporters/html_reporter.py,sha256=1ycZmVOceCschZAE6PkZNDTGwAU8tHQQltGd0_wO128,9094
|
|
13
|
+
secretguard/reporters/json_reporter.py,sha256=hry9Owd2rNWgQmMIgqX-Ghx-WURYKCiCfEhug1zPyYc,667
|
|
14
|
+
secretguard/reporters/pdf_reporter.py,sha256=VJSve-yrHbTDt0i1nffE97oZMy7hS7nAmiB4OAL5Wa4,3041
|
|
15
|
+
secretguard/rules/secrets.yaml,sha256=oQ7LudxqTDYD5voujz1lq8mPAxC7eM3IJyRV-8IOptQ,2371
|
|
16
|
+
credhunter-0.1.0.dist-info/METADATA,sha256=VH3EtCPUYDAo1bPiDku0_btP5xni_ldMuaxqJyhaidU,8708
|
|
17
|
+
credhunter-0.1.0.dist-info/WHEEL,sha256=a-udL2m4V_6Yn8b-uKzWb12RjRxwwMo6ohYFM24tpaY,91
|
|
18
|
+
credhunter-0.1.0.dist-info/entry_points.txt,sha256=HI2PH0quT4turllh8XUMVkN4K9XGOED-_MW7TivDAgU,51
|
|
19
|
+
credhunter-0.1.0.dist-info/top_level.txt,sha256=VC8CbOxTsPOaEMygTmdSjebxOVGHe0aim9KmuJGFLik,12
|
|
20
|
+
credhunter-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
secretguard
|
secretguard/__init__.py
ADDED
|
File without changes
|
secretguard/cli.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Command-line interface for CredHunter.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
credhunter scan .
|
|
5
|
+
credhunter scan . --format json
|
|
6
|
+
credhunter scan . --format html
|
|
7
|
+
credhunter scan . --format both
|
|
8
|
+
credhunter scan . --format json --output my-report.json
|
|
9
|
+
credhunter scan . --format html --output security-report.html
|
|
10
|
+
credhunter scan . --fail-on high
|
|
11
|
+
credhunter --version
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import webbrowser
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from importlib.metadata import PackageNotFoundError, version as package_version
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
|
|
22
|
+
from .models import Severity
|
|
23
|
+
from .reporters.html_reporter import generate_html_report
|
|
24
|
+
from .reporters.json_reporter import generate_json_report
|
|
25
|
+
from .scanner import scan_path
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(
|
|
28
|
+
help="CredHunter: scan source code for hardcoded secrets.",
|
|
29
|
+
no_args_is_help=True,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Environment variables commonly set by CI/CD systems.
|
|
33
|
+
CI_ENV_VARS = (
|
|
34
|
+
"CI",
|
|
35
|
+
"GITHUB_ACTIONS",
|
|
36
|
+
"GITLAB_CI",
|
|
37
|
+
"JENKINS_URL",
|
|
38
|
+
"TF_BUILD",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def version_callback(value: bool) -> None:
|
|
43
|
+
"""Display the installed CredHunter version."""
|
|
44
|
+
if not value:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
current_version = package_version("credhunter")
|
|
49
|
+
except PackageNotFoundError:
|
|
50
|
+
current_version = "unknown"
|
|
51
|
+
|
|
52
|
+
typer.echo(f"credhunter version {current_version}")
|
|
53
|
+
raise typer.Exit()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_interactive_session() -> bool:
|
|
57
|
+
"""Return True for an interactive terminal outside CI/CD."""
|
|
58
|
+
running_in_ci = any(os.environ.get(name) for name in CI_ENV_VARS)
|
|
59
|
+
return sys.stdout.isatty() and not running_in_ci
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.callback()
|
|
63
|
+
def main(
|
|
64
|
+
version: bool = typer.Option(
|
|
65
|
+
False,
|
|
66
|
+
"--version",
|
|
67
|
+
callback=version_callback,
|
|
68
|
+
is_eager=True,
|
|
69
|
+
help="Show the installed CredHunter version and exit.",
|
|
70
|
+
),
|
|
71
|
+
) -> None:
|
|
72
|
+
"""CredHunter command-line interface."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command()
|
|
76
|
+
def scan(
|
|
77
|
+
path: str = typer.Argument(
|
|
78
|
+
".",
|
|
79
|
+
help="File or directory to scan. Defaults to the current directory.",
|
|
80
|
+
),
|
|
81
|
+
report_format: str = typer.Option(
|
|
82
|
+
"none",
|
|
83
|
+
"--format",
|
|
84
|
+
help="Report format: none, json, html, or both.",
|
|
85
|
+
case_sensitive=False,
|
|
86
|
+
),
|
|
87
|
+
output: str | None = typer.Option(
|
|
88
|
+
None,
|
|
89
|
+
"--output",
|
|
90
|
+
help="Custom report filename. Used with --format json or --format html.",
|
|
91
|
+
),
|
|
92
|
+
fail_on: str = typer.Option(
|
|
93
|
+
"high",
|
|
94
|
+
"--fail-on",
|
|
95
|
+
help=(
|
|
96
|
+
"Minimum severity that causes a non-zero exit code: "
|
|
97
|
+
"critical, high, medium, low, none."
|
|
98
|
+
),
|
|
99
|
+
case_sensitive=False,
|
|
100
|
+
),
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Scan PATH for hardcoded secrets and optionally generate reports."""
|
|
103
|
+
|
|
104
|
+
# Validate options before scanning.
|
|
105
|
+
report_format = report_format.lower()
|
|
106
|
+
fail_on = fail_on.lower()
|
|
107
|
+
|
|
108
|
+
allowed_formats = {"none", "json", "html", "both"}
|
|
109
|
+
if report_format not in allowed_formats:
|
|
110
|
+
typer.secho(
|
|
111
|
+
"Invalid --format value. Choose: none, json, html, or both.",
|
|
112
|
+
fg=typer.colors.RED,
|
|
113
|
+
err=True,
|
|
114
|
+
)
|
|
115
|
+
raise typer.Exit(code=2)
|
|
116
|
+
|
|
117
|
+
allowed_severities = {"critical", "high", "medium", "low", "none"}
|
|
118
|
+
if fail_on not in allowed_severities:
|
|
119
|
+
typer.secho(
|
|
120
|
+
"Invalid --fail-on value. Choose: critical, high, medium, low, or none.",
|
|
121
|
+
fg=typer.colors.RED,
|
|
122
|
+
err=True,
|
|
123
|
+
)
|
|
124
|
+
raise typer.Exit(code=2)
|
|
125
|
+
|
|
126
|
+
if output and report_format not in {"json", "html"}:
|
|
127
|
+
typer.secho(
|
|
128
|
+
"--output can only be used with --format json or --format html. "
|
|
129
|
+
"Use --format both to generate both default report files.",
|
|
130
|
+
fg=typer.colors.RED,
|
|
131
|
+
err=True,
|
|
132
|
+
)
|
|
133
|
+
raise typer.Exit(code=2)
|
|
134
|
+
|
|
135
|
+
# Run the existing scanner.
|
|
136
|
+
findings = scan_path(path)
|
|
137
|
+
|
|
138
|
+
# Preserve the existing terminal output, including context snippets.
|
|
139
|
+
if not findings:
|
|
140
|
+
typer.secho("✔ No secrets found.", fg=typer.colors.GREEN)
|
|
141
|
+
else:
|
|
142
|
+
typer.secho(
|
|
143
|
+
f"⚠ {len(findings)} finding(s) detected:\n",
|
|
144
|
+
fg=typer.colors.YELLOW,
|
|
145
|
+
bold=True,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
for finding in sorted(
|
|
149
|
+
findings,
|
|
150
|
+
key=lambda item: item.severity.rank,
|
|
151
|
+
reverse=True,
|
|
152
|
+
):
|
|
153
|
+
typer.echo(
|
|
154
|
+
f"[{finding.severity.value.upper():8}] "
|
|
155
|
+
f"{finding.rule_name} "
|
|
156
|
+
f"({finding.confidence}% confidence) - "
|
|
157
|
+
f"{finding.file_path}:{finding.line_number}"
|
|
158
|
+
)
|
|
159
|
+
# Retains the existing context display and masking behavior.
|
|
160
|
+
typer.echo(f" {finding.context_line}")
|
|
161
|
+
|
|
162
|
+
# Generate the requested report(s).
|
|
163
|
+
json_out: str | None = None
|
|
164
|
+
html_out: str | None = None
|
|
165
|
+
|
|
166
|
+
if report_format == "json":
|
|
167
|
+
json_out = output or "credhunter-report.json"
|
|
168
|
+
elif report_format == "html":
|
|
169
|
+
html_out = output or "credhunter-report.html"
|
|
170
|
+
elif report_format == "both":
|
|
171
|
+
json_out = "credhunter-report.json"
|
|
172
|
+
html_out = "credhunter-report.html"
|
|
173
|
+
|
|
174
|
+
if json_out:
|
|
175
|
+
generate_json_report(findings, json_out)
|
|
176
|
+
typer.echo(f"\nJSON report written to {json_out}")
|
|
177
|
+
|
|
178
|
+
if html_out:
|
|
179
|
+
generate_html_report(findings, html_out)
|
|
180
|
+
typer.echo(f"HTML report written to {html_out}")
|
|
181
|
+
|
|
182
|
+
# Preserve severity-based exit behavior for local use and CI/CD.
|
|
183
|
+
should_fail = False
|
|
184
|
+
|
|
185
|
+
if fail_on != "none":
|
|
186
|
+
threshold = Severity(fail_on)
|
|
187
|
+
should_fail = any(
|
|
188
|
+
finding.severity.rank >= threshold.rank
|
|
189
|
+
for finding in findings
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if should_fail:
|
|
193
|
+
typer.secho(
|
|
194
|
+
f"\n✘ Failing: findings at or above '{fail_on}' "
|
|
195
|
+
"severity were detected.",
|
|
196
|
+
fg=typer.colors.RED,
|
|
197
|
+
bold=True,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Open the generated HTML report only in an interactive local session.
|
|
201
|
+
# CI/CD runs and redirected/piped output will not trigger a browser.
|
|
202
|
+
if html_out and _is_interactive_session():
|
|
203
|
+
try:
|
|
204
|
+
report_path = Path(html_out).resolve()
|
|
205
|
+
webbrowser.open_new_tab(report_path.as_uri())
|
|
206
|
+
except Exception as exc:
|
|
207
|
+
typer.secho(
|
|
208
|
+
f"Warning: could not open the HTML report automatically: {exc}",
|
|
209
|
+
fg=typer.colors.YELLOW,
|
|
210
|
+
err=True,
|
|
211
|
+
)
|
|
212
|
+
typer.echo(f"You can open it manually: {Path(html_out).resolve()}")
|
|
213
|
+
|
|
214
|
+
# Keep CI/CD integration: return a non-zero status when the configured
|
|
215
|
+
# severity threshold is met or exceeded.
|
|
216
|
+
raise typer.Exit(code=1 if should_fail else 0)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if __name__ == "__main__":
|
|
220
|
+
app()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# """
|
|
231
|
+
# Command-line interface.
|
|
232
|
+
|
|
233
|
+
# Usage:
|
|
234
|
+
# python -m credhunter.cli scan <path>
|
|
235
|
+
# python -m credhunter.cli scan <path> --json report.json --html report.html
|
|
236
|
+
# python -m credhunter.cli scan <path> --fail-on high
|
|
237
|
+
# """
|
|
238
|
+
|
|
239
|
+
# import os
|
|
240
|
+
# import sys
|
|
241
|
+
# import webbrowser
|
|
242
|
+
# from pathlib import Path
|
|
243
|
+
|
|
244
|
+
# import typer
|
|
245
|
+
|
|
246
|
+
# from importlib.metadata import PackageNotFoundError, version as package_version
|
|
247
|
+
|
|
248
|
+
# from .models import Severity
|
|
249
|
+
# from .reporters.html_reporter import generate_html_report
|
|
250
|
+
# from .reporters.json_reporter import generate_json_report
|
|
251
|
+
# from .scanner import scan_path
|
|
252
|
+
|
|
253
|
+
# app = typer.Typer(help="credhunter: scan source code for hardcoded secrets.")
|
|
254
|
+
|
|
255
|
+
# # Environment variables that CI/CD systems set automatically.
|
|
256
|
+
# CI_ENV_VARS = ("CI", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL", "TF_BUILD")
|
|
257
|
+
|
|
258
|
+
# def version_callback(value: bool) -> None:
|
|
259
|
+
# if not value:
|
|
260
|
+
# return
|
|
261
|
+
|
|
262
|
+
# try:
|
|
263
|
+
# current_version = package_version("credhunter")
|
|
264
|
+
# except PackageNotFoundError:
|
|
265
|
+
# current_version = "unknown"
|
|
266
|
+
|
|
267
|
+
# typer.echo(f"credhunter version {current_version}")
|
|
268
|
+
# raise typer.Exit()
|
|
269
|
+
|
|
270
|
+
# def _is_interactive_session() -> bool:
|
|
271
|
+
# """
|
|
272
|
+
# True only when a person is sitting at a terminal. False in CI/CD, and
|
|
273
|
+
# false when the output is piped or redirected, so a pipeline never tries
|
|
274
|
+
# to open a browser.
|
|
275
|
+
# """
|
|
276
|
+
# running_in_ci = any(os.environ.get(name) for name in CI_ENV_VARS)
|
|
277
|
+
# return sys.stdout.isatty() and not running_in_ci
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# @app.command()
|
|
281
|
+
# def scan(
|
|
282
|
+
# path: str = typer.Argument(..., help="File or directory to scan."),
|
|
283
|
+
# json_out: str = typer.Option(None, "--json", help="Write a JSON report to this path."),
|
|
284
|
+
# html_out: str = typer.Option(None, "--html", help="Write an HTML report to this path."),
|
|
285
|
+
# fail_on: str = typer.Option(
|
|
286
|
+
# "high", "--fail-on",
|
|
287
|
+
# help="Minimum severity that causes a non-zero exit code: critical, high, medium, low, none.",
|
|
288
|
+
# ),
|
|
289
|
+
# version: bool = typer.Option(
|
|
290
|
+
# False,
|
|
291
|
+
# "--version",
|
|
292
|
+
# callback=version_callback,
|
|
293
|
+
# is_eager=True,
|
|
294
|
+
# help="Show the installed credhunter version and exit.",
|
|
295
|
+
# ),
|
|
296
|
+
# ):
|
|
297
|
+
# """Scan PATH for hardcoded secrets and print/report the findings."""
|
|
298
|
+
# findings = scan_path(path)
|
|
299
|
+
|
|
300
|
+
# if not findings:
|
|
301
|
+
# typer.secho("✔ No secrets found.", fg=typer.colors.GREEN)
|
|
302
|
+
# else:
|
|
303
|
+
# typer.secho(f"⚠ {len(findings)} finding(s) detected:\n", fg=typer.colors.YELLOW, bold=True)
|
|
304
|
+
# for f in sorted(findings, key=lambda x: x.severity.rank, reverse=True):
|
|
305
|
+
# typer.echo(
|
|
306
|
+
# f"[{f.severity.value.upper():8}] {f.rule_name} "
|
|
307
|
+
# f"({f.confidence}% confidence) - {f.file_path}:{f.line_number}"
|
|
308
|
+
# )
|
|
309
|
+
# typer.echo(f" {f.context_line}")
|
|
310
|
+
|
|
311
|
+
# if json_out:
|
|
312
|
+
# generate_json_report(findings, json_out)
|
|
313
|
+
# typer.echo(f"\nJSON report written to {json_out}")
|
|
314
|
+
|
|
315
|
+
# if html_out:
|
|
316
|
+
# generate_html_report(findings, html_out)
|
|
317
|
+
# typer.echo(f"HTML report written to {html_out}")
|
|
318
|
+
|
|
319
|
+
# should_fail = False
|
|
320
|
+
# if fail_on.lower() != "none":
|
|
321
|
+
# threshold = Severity(fail_on.lower())
|
|
322
|
+
# should_fail = any(f.severity.rank >= threshold.rank for f in findings)
|
|
323
|
+
# if should_fail:
|
|
324
|
+
# typer.secho(
|
|
325
|
+
# f"\n✘ Failing: findings at or above '{fail_on}' severity were detected.",
|
|
326
|
+
# fg=typer.colors.RED, bold=True,
|
|
327
|
+
# )
|
|
328
|
+
|
|
329
|
+
# # Show the HTML report in the default web browser. Only when a person ran
|
|
330
|
+
# # this at a terminal - never in CI/CD, so pipelines are not affected.
|
|
331
|
+
# if html_out and _is_interactive_session():
|
|
332
|
+
# webbrowser.open_new_tab(Path(html_out).resolve().as_uri())
|
|
333
|
+
|
|
334
|
+
# sys.exit(1 if should_fail else 0)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# if __name__ == "__main__":
|
|
338
|
+
# app()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context analysis: a regex match on its own is weak evidence. A line that
|
|
3
|
+
also contains a relevant keyword (password, api_key, token, secret, ...)
|
|
4
|
+
is much more likely to be a real credential than coincidental text.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
GENERIC_SECRET_KEYWORDS = [
|
|
8
|
+
"password", "passwd", "pwd",
|
|
9
|
+
"secret", "token", "api_key", "apikey",
|
|
10
|
+
"access_key", "access_token", "credential",
|
|
11
|
+
"authorization", "auth_token", "private_key",
|
|
12
|
+
"client_secret", "session_key",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def has_context_keywords(line: str, keywords: list[str] | None = None) -> bool:
|
|
17
|
+
"""Case-insensitive check for whether any relevant keyword appears on the line."""
|
|
18
|
+
line_lower = line.lower()
|
|
19
|
+
keyword_list = keywords if keywords else GENERIC_SECRET_KEYWORDS
|
|
20
|
+
return any(keyword.lower() in line_lower for keyword in keyword_list)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_placeholder(value: str, placeholder_markers: list[str]) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Check whether a matched value looks like a placeholder/example rather
|
|
26
|
+
than a real secret (e.g. 'your_api_key_here', 'AKIAFAKEEXAMPLE...').
|
|
27
|
+
"""
|
|
28
|
+
value_lower = value.lower()
|
|
29
|
+
return any(marker.lower() in value_lower for marker in placeholder_markers)
|