get-report 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.
- get_report-0.1.0.dist-info/METADATA +128 -0
- get_report-0.1.0.dist-info/RECORD +35 -0
- get_report-0.1.0.dist-info/WHEEL +5 -0
- get_report-0.1.0.dist-info/entry_points.txt +2 -0
- get_report-0.1.0.dist-info/top_level.txt +1 -0
- secretscan/__init__.py +1 -0
- secretscan/__main__.py +3 -0
- secretscan/cli.py +243 -0
- secretscan/config.py +187 -0
- secretscan/model.py +409 -0
- secretscan/redact.py +43 -0
- secretscan/report/__init__.py +6 -0
- secretscan/report/csv_report.py +26 -0
- secretscan/report/html_report.py +235 -0
- secretscan/report/json_report.py +15 -0
- secretscan/report/table_report.py +145 -0
- secretscan/rules/__init__.py +14 -0
- secretscan/rules/default.yaml +132 -0
- secretscan/rules/entropy.py +17 -0
- secretscan/rules/loader.py +34 -0
- secretscan/rules/placeholder.py +66 -0
- secretscan/rules/rule.py +70 -0
- secretscan/scanner/__init__.py +3 -0
- secretscan/scanner/chrome.py +518 -0
- secretscan/scanner/content.py +104 -0
- secretscan/scanner/docker.py +81 -0
- secretscan/scanner/engine.py +186 -0
- secretscan/scanner/filesystem.py +240 -0
- secretscan/scanner/internet.py +48 -0
- secretscan/scanner/keychain.py +116 -0
- secretscan/scanner/lan.py +167 -0
- secretscan/scanner/network.py +156 -0
- secretscan/scanner/process_env.py +132 -0
- secretscan/scanner/shell_history.py +70 -0
- secretscan/scanner/tools.py +174 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: get-report
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Scan your computer for secrets and generate a report.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: pyyaml>=6.0
|
|
9
|
+
Requires-Dist: websocket-client>=1.6.0
|
|
10
|
+
Provides-Extra: crypto
|
|
11
|
+
Requires-Dist: cryptography>=41.0; extra == "crypto"
|
|
12
|
+
|
|
13
|
+
# secretscan-report
|
|
14
|
+
|
|
15
|
+
A personal security-hygiene tool: scans your own machine for accidentally
|
|
16
|
+
exposed secrets (API keys, tokens, passwords, private keys), audits what's
|
|
17
|
+
installed and what's exposed (Docker containers, listening ports, Chrome
|
|
18
|
+
browser cookies), and can optionally check your local network and internet
|
|
19
|
+
connectivity.
|
|
20
|
+
|
|
21
|
+
**Everything runs locally. No secrets ever leave your machine.**
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pip install secretscan-report[all]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The `[all]` extra installs `websocket-client`, which is used **only** to talk
|
|
30
|
+
to Chrome's DevTools Protocol on `localhost:9222` for headless browser auditing
|
|
31
|
+
(the same mechanism as [chromedp](https://github.com/chromedp/chromedp) in Go).
|
|
32
|
+
It never makes outbound network connections.
|
|
33
|
+
|
|
34
|
+
Without `[all]`:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install secretscan-report
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This installs the core scanner (filesystem, shell history, keychain, processes,
|
|
41
|
+
network, Docker). Chrome headless auditing is skipped if `websocket-client` is
|
|
42
|
+
not installed.
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
# Full scan — all modules including Chrome, LAN, Internet
|
|
48
|
+
# Generates HTML + JSON reports with a progress bar
|
|
49
|
+
secretscan fullscan
|
|
50
|
+
|
|
51
|
+
# Default scan (home dir, default modules)
|
|
52
|
+
secretscan run
|
|
53
|
+
|
|
54
|
+
# Scan a specific directory, filesystem module only
|
|
55
|
+
secretscan run --paths ~/Projects --modules filesystem
|
|
56
|
+
|
|
57
|
+
# Full scan with custom output directory
|
|
58
|
+
secretscan fullscan --output-dir ./report
|
|
59
|
+
|
|
60
|
+
# List all detection rules
|
|
61
|
+
secretscan rules list
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Modules
|
|
65
|
+
|
|
66
|
+
| Module | Default | What it does |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `filesystem` | yes | Scans files under `--paths` (default `$HOME`) for secret patterns |
|
|
69
|
+
| `shell-history` | yes | Scans `.zsh_history`, `.bash_history`, and other history files |
|
|
70
|
+
| `keychain` | yes | Audits macOS Keychain metadata (never decrypts values) |
|
|
71
|
+
| `process-env` | yes | Inspects your own running processes' command lines |
|
|
72
|
+
| `installed-tools` | yes | Inventories security/dev tools and packages |
|
|
73
|
+
| `network` | yes | Lists TCP/UDP ports this machine is listening on |
|
|
74
|
+
| `docker` | yes | Lists running containers, checks for exposed Docker API |
|
|
75
|
+
| `chrome` | yes | Audits Chrome browser: launches headless, reads cookies via CDP, takes screenshots |
|
|
76
|
+
| `lan` | opt-in | Probes other devices on your local network (`--yes-lan`) |
|
|
77
|
+
| `internet` | opt-in | Tests outbound connectivity to public endpoints |
|
|
78
|
+
|
|
79
|
+
## Output formats
|
|
80
|
+
|
|
81
|
+
- `table` — colored terminal output (default)
|
|
82
|
+
- `json` — machine-readable `{meta, findings}` document
|
|
83
|
+
- `html` — self-contained HTML report with dark/light theme
|
|
84
|
+
- `csv` — flat CSV of findings
|
|
85
|
+
|
|
86
|
+
## Detection
|
|
87
|
+
|
|
88
|
+
15 built-in rules covering AWS, GitHub, GitLab, Slack, Stripe, Google, npm,
|
|
89
|
+
PEM private keys, and generic password/secret/token assignments. Each rule
|
|
90
|
+
uses regex matching with optional Shannon entropy scoring and keyword
|
|
91
|
+
pre-filtering for performance.
|
|
92
|
+
|
|
93
|
+
Values that look like placeholders (`{{password}}`, `changeme`, `your_api_key_here`)
|
|
94
|
+
are detected and downgraded rather than competing with real findings.
|
|
95
|
+
|
|
96
|
+
## Security & privacy
|
|
97
|
+
|
|
98
|
+
- **No outbound network calls** from secret-detection modules — ever.
|
|
99
|
+
- The `chrome` module connects to `localhost:9222` (Chrome DevTools Protocol)
|
|
100
|
+
to audit your own browser. This is the same protocol used by Selenium,
|
|
101
|
+
Playwright, Puppeteer, and chromedp. It copies essential Chrome profile
|
|
102
|
+
files to a temp directory, launches headless Chrome against the copy,
|
|
103
|
+
reads cookies and takes a screenshot, then deletes the copy.
|
|
104
|
+
- The optional `lan` module sends pings to your local subnet (requires `--yes-lan`).
|
|
105
|
+
- The optional `internet` module makes TCP connections to well-known public
|
|
106
|
+
endpoints (e.g. `google.com:443`) to test connectivity.
|
|
107
|
+
- All scan results stay on disk in the output directory you choose.
|
|
108
|
+
|
|
109
|
+
## Cross-platform
|
|
110
|
+
|
|
111
|
+
Works on macOS, Linux, and Windows. Platform-specific features:
|
|
112
|
+
- **macOS**: Keychain audit, Chrome cookie decryption via Keychain
|
|
113
|
+
- **Linux**: `/proc`-based process inspection
|
|
114
|
+
- **Windows**: `netstat`/`wmic`/`ipconfig` fallbacks
|
|
115
|
+
|
|
116
|
+
## What this tool does NOT do
|
|
117
|
+
|
|
118
|
+
- No git-history scanning (use Gitleaks/TruffleHog for that)
|
|
119
|
+
- No live credential verification
|
|
120
|
+
- No bypassing macOS security prompts
|
|
121
|
+
- No outbound transmission of findings
|
|
122
|
+
- Secret detection modules never make outbound network calls
|
|
123
|
+
|
|
124
|
+
## Dependencies
|
|
125
|
+
|
|
126
|
+
- `pyyaml` — for loading rules YAML files
|
|
127
|
+
- `websocket-client` (optional, via `[all]`) — localhost-only CDP communication for Chrome auditing
|
|
128
|
+
- `cryptography` (optional, via `[crypto]`) — macOS Chrome cookie decryption from SQLite fallback
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
secretscan/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
secretscan/__main__.py,sha256=Txo6AmotoWSA88hTtJWGXhBq57Litv0NlL8BbL-pO_g,40
|
|
3
|
+
secretscan/cli.py,sha256=gpZmaHHF8qVUNMXNePjVkWyAHsloL2DtLXSB9ldzQo4,9812
|
|
4
|
+
secretscan/config.py,sha256=qG7iLGv_1QB6noUu5yRqZ8t1BQd3PQd9Rhdd25n7-5g,5617
|
|
5
|
+
secretscan/model.py,sha256=6xdtZWHigu4uXS-cb3I56xcidnqSDyptEBbAmt3fMbY,11284
|
|
6
|
+
secretscan/redact.py,sha256=rXVsCbhk9D3T-a6TsSINHzSrrL3SoHAsces2bmg4FPs,966
|
|
7
|
+
secretscan/report/__init__.py,sha256=ayQzC52-LvEjv5i_JvRsavFpfsopfOhEMMljhbWIWgs,288
|
|
8
|
+
secretscan/report/csv_report.py,sha256=1h_1KT4Qqpsnt9TLu6mcGQpBy9cRn-pACavqI-eHRCk,770
|
|
9
|
+
secretscan/report/html_report.py,sha256=9ro63ptpip3HGxGywijMq4GMcH2C3uZT4GQlaXJmxKI,10016
|
|
10
|
+
secretscan/report/json_report.py,sha256=nqDur5XHBVgrZWTYL5bZBmxHjH1aCPUhSysJWvnTMxA,349
|
|
11
|
+
secretscan/report/table_report.py,sha256=Sw4JooHWm4QwJhadsRSXF_32IZl3WlVKNoF-ez2pLlY,5924
|
|
12
|
+
secretscan/rules/__init__.py,sha256=WYIZyRuVRokHAduEw099sw8tBAAO_nmBYY85qdznLVQ,411
|
|
13
|
+
secretscan/rules/default.yaml,sha256=R7rkb0zpOen6qn2oeqzC_WrpCUaRwgOF64yY3xW59Ss,3762
|
|
14
|
+
secretscan/rules/entropy.py,sha256=rnSHtaE2-Lk36bQOSz5svcxo9ffYVMWKocNmINLVvqk,359
|
|
15
|
+
secretscan/rules/loader.py,sha256=PpeirWUIMTvYPLAHTqrEuNr8sUbpnOFelz8Sb84JIPU,995
|
|
16
|
+
secretscan/rules/placeholder.py,sha256=bSuyIvzv8kJ5xj6FCOVcE8O9SY-zbcL8CfKZE7TN-sU,1984
|
|
17
|
+
secretscan/rules/rule.py,sha256=vrvkZyQdwOuTBIXov2Lo_XlY3y0U5sZrnrVksO8FlFg,2119
|
|
18
|
+
secretscan/scanner/__init__.py,sha256=EqA1Am5FgeskkVYk_409EiwpIfPHLLuJZbDytUPeyAU,71
|
|
19
|
+
secretscan/scanner/chrome.py,sha256=xYGwM28eRVMb8xmkmSK9CGcxGjQGeiiWsXuZZyuRmyA,18286
|
|
20
|
+
secretscan/scanner/content.py,sha256=aYLh9o0VnTlwmlCjZgpQzL36qi75V-XG_fy8KnGXuio,3206
|
|
21
|
+
secretscan/scanner/docker.py,sha256=XjEqVxJGDZnb2MZB5-KzF8m6kFRlwQf4PGgQeZX19XM,2597
|
|
22
|
+
secretscan/scanner/engine.py,sha256=EMDK92kzuXNfC_bxAnwqqf6sNb1Vts1Gi6JFm2Qx1RQ,5730
|
|
23
|
+
secretscan/scanner/filesystem.py,sha256=5-53zkzrtEEl0YLrlJO8RnmGFPQLq8OhISVoiNPTjIE,8431
|
|
24
|
+
secretscan/scanner/internet.py,sha256=r2wxfnZAC6Yt0pwJgRzBBaYqNrZihvbkKUpXIOTwDDk,1629
|
|
25
|
+
secretscan/scanner/keychain.py,sha256=sCiswBE8cfH37YjbrDjt2Ojd5asmpjlgvOCbWYNWjqk,3962
|
|
26
|
+
secretscan/scanner/lan.py,sha256=_nscxBONOwOeO497bkbH5GwrjvCOVphV3QZgCpBSpcc,6047
|
|
27
|
+
secretscan/scanner/network.py,sha256=bTXqV-rJpjQ69y6m-zik_rTjW5mwh_JrQpG9_UHzdu8,4513
|
|
28
|
+
secretscan/scanner/process_env.py,sha256=mysqt68Al_6XeUvTSJs3a80dNGchfdNb9YB71ta4vYo,4023
|
|
29
|
+
secretscan/scanner/shell_history.py,sha256=GbnvENHNBxWyssx-dDOkTpN6GJi31m0p4Vd8riq7HKY,1941
|
|
30
|
+
secretscan/scanner/tools.py,sha256=F7BB823E0Eyhb0dnm37Y_VjTi3QRXPBBEahFZxU61gg,5269
|
|
31
|
+
get_report-0.1.0.dist-info/METADATA,sha256=QWsKhdZLF7oi2tgexHOen4xhuRa3GRzb73w76Cf3TC8,4820
|
|
32
|
+
get_report-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
33
|
+
get_report-0.1.0.dist-info/entry_points.txt,sha256=sN0WNeqqwlhtqW4JeXpFhvTbL1NWCC3XamQwJ4lbBTc,51
|
|
34
|
+
get_report-0.1.0.dist-info/top_level.txt,sha256=zslsakZH5pTIXMMAMAfZm8DyVItULqPac_Dxi-rDaL4,11
|
|
35
|
+
get_report-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
secretscan
|
secretscan/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
secretscan/__main__.py
ADDED
secretscan/cli.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from secretscan import __version__
|
|
10
|
+
from secretscan.config import Config
|
|
11
|
+
from secretscan.model import Confidence, Module, Severity, DEFAULT_MODULES, OPT_IN_MODULES
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
MODULE_NAMES = {
|
|
15
|
+
"filesystem": Module.FILESYSTEM,
|
|
16
|
+
"shell-history": Module.SHELL_HISTORY,
|
|
17
|
+
"keychain": Module.KEYCHAIN,
|
|
18
|
+
"process-env": Module.PROCESS_ENV,
|
|
19
|
+
"installed-tools": Module.INSTALLED_TOOLS,
|
|
20
|
+
"network": Module.NETWORK,
|
|
21
|
+
"docker": Module.DOCKER,
|
|
22
|
+
"chrome": Module.CHROME,
|
|
23
|
+
"lan": Module.LAN,
|
|
24
|
+
"internet": Module.INTERNET,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
BAR_WIDTH = 30
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _progress_bar(done: int, total: int, module_name: str = "") -> None:
|
|
31
|
+
if total == 0:
|
|
32
|
+
return
|
|
33
|
+
pct = int(done / total * 100)
|
|
34
|
+
filled = int(BAR_WIDTH * done / total)
|
|
35
|
+
bar = "█" * filled + "░" * (BAR_WIDTH - filled)
|
|
36
|
+
label = f" {module_name}" if module_name else ""
|
|
37
|
+
sys.stderr.write(f"\r [{bar}] {pct:3d}% ({done}/{total}){label} ")
|
|
38
|
+
sys.stderr.flush()
|
|
39
|
+
if done == total:
|
|
40
|
+
sys.stderr.write("\n")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _render_outputs(cfg: Config, result) -> None:
|
|
44
|
+
for fmt in cfg.formats:
|
|
45
|
+
if fmt == "json":
|
|
46
|
+
from secretscan.report.json_report import render_json
|
|
47
|
+
if cfg.output_dir:
|
|
48
|
+
os.makedirs(cfg.output_dir, exist_ok=True)
|
|
49
|
+
with open(os.path.join(cfg.output_dir, "report.json"), "w") as f:
|
|
50
|
+
render_json(f, result.findings, result.meta)
|
|
51
|
+
else:
|
|
52
|
+
render_json(sys.stdout, result.findings, result.meta)
|
|
53
|
+
elif fmt == "html":
|
|
54
|
+
from secretscan.report.html_report import render_html
|
|
55
|
+
if cfg.output_dir:
|
|
56
|
+
os.makedirs(cfg.output_dir, exist_ok=True)
|
|
57
|
+
with open(os.path.join(cfg.output_dir, "report.html"), "w") as f:
|
|
58
|
+
render_html(f, result.findings, result.meta)
|
|
59
|
+
sys.stderr.write(f" HTML report written to {cfg.output_dir}/report.html\n")
|
|
60
|
+
else:
|
|
61
|
+
render_html(sys.stdout, result.findings, result.meta)
|
|
62
|
+
elif fmt == "csv":
|
|
63
|
+
from secretscan.report.csv_report import render_csv
|
|
64
|
+
if cfg.output_dir:
|
|
65
|
+
os.makedirs(cfg.output_dir, exist_ok=True)
|
|
66
|
+
with open(os.path.join(cfg.output_dir, "report.csv"), "w") as f:
|
|
67
|
+
render_csv(f, result.findings, result.meta)
|
|
68
|
+
else:
|
|
69
|
+
render_csv(sys.stdout, result.findings, result.meta)
|
|
70
|
+
elif fmt == "table":
|
|
71
|
+
from secretscan.report.table_report import render_table
|
|
72
|
+
render_table(sys.stderr, result.findings, result.meta, no_color=False)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _run(args: argparse.Namespace) -> int:
|
|
76
|
+
from secretscan.scanner.engine import run_scan
|
|
77
|
+
|
|
78
|
+
modules: list[Module] = []
|
|
79
|
+
if args.modules:
|
|
80
|
+
for name in args.modules.split(","):
|
|
81
|
+
name = name.strip()
|
|
82
|
+
try:
|
|
83
|
+
modules.append(Module(name))
|
|
84
|
+
except ValueError:
|
|
85
|
+
sys.stderr.write(f"unknown module: {name}\n")
|
|
86
|
+
return 2
|
|
87
|
+
else:
|
|
88
|
+
modules = list(DEFAULT_MODULES)
|
|
89
|
+
|
|
90
|
+
if args.yes_lan and Module.LAN not in modules:
|
|
91
|
+
modules.append(Module.LAN)
|
|
92
|
+
|
|
93
|
+
if Module.LAN in modules and not args.yes_lan:
|
|
94
|
+
sys.stderr.write(
|
|
95
|
+
" The 'lan' module actively probes other devices on your network.\n"
|
|
96
|
+
" Pass --yes-lan to confirm you're authorized to scan that network.\n"
|
|
97
|
+
)
|
|
98
|
+
modules = [m for m in modules if m != Module.LAN]
|
|
99
|
+
|
|
100
|
+
if Module.INTERNET in modules:
|
|
101
|
+
sys.stderr.write(" The 'internet' module makes real outbound calls to public endpoints.\n")
|
|
102
|
+
|
|
103
|
+
paths = args.paths.split(",") if args.paths else [str(Path.home())]
|
|
104
|
+
|
|
105
|
+
cfg = Config(
|
|
106
|
+
paths=paths,
|
|
107
|
+
modules=modules,
|
|
108
|
+
exclude_globs=args.exclude.split(",") if args.exclude else [],
|
|
109
|
+
no_default_excludes=args.no_default_excludes,
|
|
110
|
+
include_volumes=args.include_volumes,
|
|
111
|
+
min_severity=Severity(args.min_severity),
|
|
112
|
+
min_confidence=Confidence(args.min_confidence),
|
|
113
|
+
rules_file=args.rules_file or "",
|
|
114
|
+
concurrency=args.concurrency,
|
|
115
|
+
reveal_length=args.reveal_length,
|
|
116
|
+
formats=args.format.split(",") if args.format else ["table"],
|
|
117
|
+
output_dir=args.output_dir or "",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
module_order = list(cfg.modules)
|
|
121
|
+
|
|
122
|
+
def progress(done: int, total: int) -> None:
|
|
123
|
+
mod_name = module_order[done - 1].value if done <= len(module_order) else ""
|
|
124
|
+
_progress_bar(done, total, mod_name)
|
|
125
|
+
|
|
126
|
+
result = run_scan(cfg, progress=progress)
|
|
127
|
+
_render_outputs(cfg, result)
|
|
128
|
+
return 1 if result.findings else 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _fullscan(args: argparse.Namespace) -> int:
|
|
132
|
+
from secretscan.scanner.engine import run_scan
|
|
133
|
+
|
|
134
|
+
all_modules = list(Module)
|
|
135
|
+
|
|
136
|
+
output_dir = args.output_dir
|
|
137
|
+
if not output_dir:
|
|
138
|
+
output_dir = str(Path("reports") / datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
|
|
139
|
+
|
|
140
|
+
paths = args.paths.split(",") if args.paths else [str(Path.home())]
|
|
141
|
+
|
|
142
|
+
cfg = Config(
|
|
143
|
+
paths=paths,
|
|
144
|
+
modules=all_modules,
|
|
145
|
+
rules_file=args.rules_file or "",
|
|
146
|
+
concurrency=args.concurrency,
|
|
147
|
+
reveal_length=args.reveal_length,
|
|
148
|
+
formats=["html", "json", "table"],
|
|
149
|
+
output_dir=output_dir,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
sys.stderr.write(f"\n secretscan fullscan\n")
|
|
153
|
+
sys.stderr.write(f" Paths: {', '.join(paths)}\n")
|
|
154
|
+
sys.stderr.write(f" Modules: {', '.join(m.value for m in all_modules)}\n")
|
|
155
|
+
sys.stderr.write(f" Output: {output_dir}\n\n")
|
|
156
|
+
|
|
157
|
+
module_order = list(cfg.modules)
|
|
158
|
+
|
|
159
|
+
def progress(done: int, total: int) -> None:
|
|
160
|
+
mod_name = module_order[done - 1].value if done <= len(module_order) else ""
|
|
161
|
+
_progress_bar(done, total, mod_name)
|
|
162
|
+
|
|
163
|
+
result = run_scan(cfg, progress=progress)
|
|
164
|
+
_render_outputs(cfg, result)
|
|
165
|
+
|
|
166
|
+
html_path = os.path.join(output_dir, "report.html")
|
|
167
|
+
if os.path.isfile(html_path):
|
|
168
|
+
sys.stderr.write(f"\n Report: {html_path}\n")
|
|
169
|
+
|
|
170
|
+
return 1 if result.findings else 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _rules_list(args: argparse.Namespace) -> int:
|
|
174
|
+
from secretscan.rules.loader import load_default_rules, load_rules_file
|
|
175
|
+
|
|
176
|
+
if args.rules_file:
|
|
177
|
+
rules = load_rules_file(args.rules_file)
|
|
178
|
+
else:
|
|
179
|
+
rules = load_default_rules()
|
|
180
|
+
|
|
181
|
+
fmt = "{:<35s} {:<10s} {:<50s}"
|
|
182
|
+
sys.stdout.write(fmt.format("ID", "SEVERITY", "DESCRIPTION") + "\n")
|
|
183
|
+
sys.stdout.write("─" * 95 + "\n")
|
|
184
|
+
for r in rules:
|
|
185
|
+
sys.stdout.write(
|
|
186
|
+
fmt.format(r.rule.id, r.rule.severity, r.rule.description[:50]) + "\n"
|
|
187
|
+
)
|
|
188
|
+
sys.stdout.write(f"\n{len(rules)} rules loaded.\n")
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main(argv: list[str] | None = None) -> None:
|
|
193
|
+
parser = argparse.ArgumentParser(
|
|
194
|
+
prog="secretscan",
|
|
195
|
+
description="Scan your machine for accidentally exposed secrets.",
|
|
196
|
+
)
|
|
197
|
+
parser.add_argument("--version", action="version", version=f"secretscan {__version__}")
|
|
198
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
199
|
+
|
|
200
|
+
# --- run ---
|
|
201
|
+
run_parser = subparsers.add_parser("run", help="Run a scan with selected modules")
|
|
202
|
+
run_parser.add_argument("--paths", default=None, help="Comma-separated roots to scan (default: $HOME)")
|
|
203
|
+
run_parser.add_argument("--modules", default=None, help="Comma-separated modules to run")
|
|
204
|
+
run_parser.add_argument("--exclude", default=None, help="Additional glob patterns to exclude")
|
|
205
|
+
run_parser.add_argument("--no-default-excludes", action="store_true")
|
|
206
|
+
run_parser.add_argument("--include-volumes", action="store_true", help="Include /Volumes mounts")
|
|
207
|
+
run_parser.add_argument("--yes-lan", action="store_true", help="Confirm LAN probing authorization")
|
|
208
|
+
run_parser.add_argument("--min-severity", default="low", choices=["low", "medium", "high", "critical"])
|
|
209
|
+
run_parser.add_argument("--min-confidence", default="low", choices=["low", "medium", "high"])
|
|
210
|
+
run_parser.add_argument("--rules-file", default=None, help="Custom rules YAML file")
|
|
211
|
+
run_parser.add_argument("--format", default="table", help="Output formats: table,json,html,csv")
|
|
212
|
+
run_parser.add_argument("--output-dir", default=None, help="Write file reports to this directory")
|
|
213
|
+
run_parser.add_argument("--reveal-length", action="store_true", help="Show coarse length hint on redacted values")
|
|
214
|
+
run_parser.add_argument("--concurrency", type=int, default=0, help="Worker threads (default: CPU count)")
|
|
215
|
+
run_parser.add_argument("--no-color", action="store_true", help="Disable ANSI colors in table output")
|
|
216
|
+
|
|
217
|
+
# --- fullscan ---
|
|
218
|
+
full_parser = subparsers.add_parser("fullscan", help="Run ALL modules (incl. Chrome, LAN, Internet) and generate HTML+JSON reports")
|
|
219
|
+
full_parser.add_argument("--paths", default=None, help="Comma-separated roots to scan (default: $HOME)")
|
|
220
|
+
full_parser.add_argument("--output-dir", default=None, help="Write reports here (default: reports/<timestamp>)")
|
|
221
|
+
full_parser.add_argument("--rules-file", default=None, help="Custom rules YAML file")
|
|
222
|
+
full_parser.add_argument("--reveal-length", action="store_true")
|
|
223
|
+
full_parser.add_argument("--concurrency", type=int, default=0)
|
|
224
|
+
|
|
225
|
+
# --- rules ---
|
|
226
|
+
rules_parser = subparsers.add_parser("rules", help="Manage detection rules")
|
|
227
|
+
rules_sub = rules_parser.add_subparsers(dest="rules_command")
|
|
228
|
+
list_parser = rules_sub.add_parser("list", help="List all detection rules")
|
|
229
|
+
list_parser.add_argument("--rules-file", default=None)
|
|
230
|
+
|
|
231
|
+
parsed = parser.parse_args(argv)
|
|
232
|
+
|
|
233
|
+
if parsed.command == "run":
|
|
234
|
+
code = _run(parsed)
|
|
235
|
+
elif parsed.command == "fullscan":
|
|
236
|
+
code = _fullscan(parsed)
|
|
237
|
+
elif parsed.command == "rules" and getattr(parsed, "rules_command", None) == "list":
|
|
238
|
+
code = _rules_list(parsed)
|
|
239
|
+
else:
|
|
240
|
+
parser.print_help()
|
|
241
|
+
code = 0
|
|
242
|
+
|
|
243
|
+
sys.exit(code)
|
secretscan/config.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from secretscan.model import Confidence, Module, Severity
|
|
8
|
+
|
|
9
|
+
_COMMON_EXCLUDES = [
|
|
10
|
+
"**/.git/**",
|
|
11
|
+
"**/node_modules/**",
|
|
12
|
+
"**/vendor/**",
|
|
13
|
+
"**/.venv/**",
|
|
14
|
+
"**/__pycache__/**",
|
|
15
|
+
"**/.Trash/**",
|
|
16
|
+
"**/.claude/file-history/**",
|
|
17
|
+
"**/.claude/projects/**",
|
|
18
|
+
"**/.claude/shell-snapshots/**",
|
|
19
|
+
"**/.claude/todos/**",
|
|
20
|
+
"**/.claude/plugins/**",
|
|
21
|
+
"**/.codex/.tmp/**",
|
|
22
|
+
"**/go/pkg/mod/**",
|
|
23
|
+
"**/google-cloud-sdk/**",
|
|
24
|
+
"**/.cache/uv/**",
|
|
25
|
+
"**/.cache/pip/**",
|
|
26
|
+
"**/.cache/yarn/**",
|
|
27
|
+
"**/.cache/pnpm/**",
|
|
28
|
+
"**/.cache/go-build/**",
|
|
29
|
+
"**/.cache/go-mod/**",
|
|
30
|
+
"**/.cache/Homebrew/**",
|
|
31
|
+
"**/.cache/huggingface/**",
|
|
32
|
+
"**/.cache/torch/**",
|
|
33
|
+
"**/.cache/matplotlib/**",
|
|
34
|
+
"**/.cache/mesa_shader_cache/**",
|
|
35
|
+
"**/.cache/JetBrains/**",
|
|
36
|
+
"**/.cache/Google/**",
|
|
37
|
+
"**/.cache/mozilla/**",
|
|
38
|
+
"**/.cache/chromium/**",
|
|
39
|
+
"**/.cache/thumbnails/**",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
_MACOS_EXCLUDES = [
|
|
43
|
+
"**/.Spotlight-V100/**",
|
|
44
|
+
"**/.fseventsd/**",
|
|
45
|
+
"**/.DocumentRevisions-V100/**",
|
|
46
|
+
"**/.TemporaryItems/**",
|
|
47
|
+
"**/Library/Caches/**",
|
|
48
|
+
"**/Library/Developer/Xcode/DerivedData/**",
|
|
49
|
+
"**/Library/Developer/CoreSimulator/**",
|
|
50
|
+
"**/Library/Application Support/Google/**",
|
|
51
|
+
"**/Library/Application Support/Firefox/**",
|
|
52
|
+
"**/Library/Application Support/Slack/**",
|
|
53
|
+
"**/Library/Application Support/Discord/**",
|
|
54
|
+
"**/Library/Application Support/Microsoft/**",
|
|
55
|
+
"**/Library/Application Support/Code/**",
|
|
56
|
+
"**/Library/Application Support/com.apple.*/**",
|
|
57
|
+
"**/Library/Application Support/Electron/**",
|
|
58
|
+
"**/Library/Application Support/Steam/**",
|
|
59
|
+
"**/Library/Application Support/Spotify/**",
|
|
60
|
+
"**/Library/Application Support/zoom.us/**",
|
|
61
|
+
"**/Library/Application Support/obsidian/**",
|
|
62
|
+
"**/Library/Application Support/Brave Browser/**",
|
|
63
|
+
"**/Library/Application Support/Arc/**",
|
|
64
|
+
"**/Library/Application Support/JetBrains/**",
|
|
65
|
+
"**/Library/Application Support/Adobe/**",
|
|
66
|
+
"**/Library/Application Support/iCloud/**",
|
|
67
|
+
"**/Library/Application Support/CallHistoryDB/**",
|
|
68
|
+
"**/Library/Application Support/Knowledge/**",
|
|
69
|
+
"**/Library/Application Support/AddressBook/**",
|
|
70
|
+
"**/Library/Application Support/NotificationCenter/**",
|
|
71
|
+
"**/Library/Mail/**",
|
|
72
|
+
"**/Library/Messages/**",
|
|
73
|
+
"**/Library/Containers/**",
|
|
74
|
+
"**/Library/Group Containers/**",
|
|
75
|
+
"**/Library/Saved Application State/**",
|
|
76
|
+
"**/Library/WebKit/**",
|
|
77
|
+
"**/Library/Safari/**",
|
|
78
|
+
"**/Library/Metadata/**",
|
|
79
|
+
"**/Library/Logs/**",
|
|
80
|
+
"**/Library/Fonts/**",
|
|
81
|
+
"**/Library/ColorSync/**",
|
|
82
|
+
"**/Library/Autosave Information/**",
|
|
83
|
+
"**/Library/HomeKit/**",
|
|
84
|
+
"**/Library/CoreData/**",
|
|
85
|
+
"**/Photos Library.photoslibrary/**",
|
|
86
|
+
"**/Music/Music/**",
|
|
87
|
+
"**/Movies/**",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
_LINUX_EXCLUDES = [
|
|
91
|
+
"**/proc/**",
|
|
92
|
+
"**/sys/**",
|
|
93
|
+
"**/dev/**",
|
|
94
|
+
"**/run/**",
|
|
95
|
+
"**/snap/**",
|
|
96
|
+
"**/lost+found/**",
|
|
97
|
+
"**/var/log/**",
|
|
98
|
+
"**/var/cache/**",
|
|
99
|
+
"**/var/lib/dpkg/**",
|
|
100
|
+
"**/var/lib/apt/**",
|
|
101
|
+
"**/var/lib/rpm/**",
|
|
102
|
+
"**/var/lib/pacman/**",
|
|
103
|
+
"**/var/lib/docker/**",
|
|
104
|
+
"**/var/lib/containerd/**",
|
|
105
|
+
"**/var/lib/snapd/**",
|
|
106
|
+
"**/var/lib/flatpak/**",
|
|
107
|
+
"**/var/tmp/**",
|
|
108
|
+
"**/tmp/**",
|
|
109
|
+
"**/.local/share/Trash/**",
|
|
110
|
+
"**/.local/share/Steam/**",
|
|
111
|
+
"**/.config/google-chrome/Default/Service Worker/**",
|
|
112
|
+
"**/.config/google-chrome/Default/Cache/**",
|
|
113
|
+
"**/.mozilla/**",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
_WINDOWS_EXCLUDES = [
|
|
117
|
+
"**/Windows/**",
|
|
118
|
+
"**/Program Files/**",
|
|
119
|
+
"**/Program Files (x86)/**",
|
|
120
|
+
"**/ProgramData/**",
|
|
121
|
+
"**/AppData/Local/Temp/**",
|
|
122
|
+
"**/AppData/Local/Microsoft/**",
|
|
123
|
+
"**/AppData/Local/Google/Chrome/User Data/Default/Service Worker/**",
|
|
124
|
+
"**/AppData/Local/Google/Chrome/User Data/Default/Cache/**",
|
|
125
|
+
"**/AppData/Local/Packages/**",
|
|
126
|
+
"**/AppData/Local/Programs/**",
|
|
127
|
+
"**/$Recycle.Bin/**",
|
|
128
|
+
"**/System Volume Information/**",
|
|
129
|
+
"**/Recovery/**",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _platform_excludes() -> list[str]:
|
|
134
|
+
system = platform.system()
|
|
135
|
+
if system == "Darwin":
|
|
136
|
+
return _MACOS_EXCLUDES
|
|
137
|
+
elif system == "Linux":
|
|
138
|
+
return _LINUX_EXCLUDES
|
|
139
|
+
elif system == "Windows":
|
|
140
|
+
return _WINDOWS_EXCLUDES
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
DEFAULT_EXCLUDE_GLOBS = _COMMON_EXCLUDES + _platform_excludes()
|
|
145
|
+
|
|
146
|
+
DEFAULT_SHELL_HISTORY_FILES = [
|
|
147
|
+
".zsh_history",
|
|
148
|
+
".bash_history",
|
|
149
|
+
".python_history",
|
|
150
|
+
".psql_history",
|
|
151
|
+
".mysql_history",
|
|
152
|
+
".node_repl_history",
|
|
153
|
+
".local/share/fish/fish_history",
|
|
154
|
+
".lesshst",
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class Config:
|
|
162
|
+
paths: list[str] = field(default_factory=lambda: [str(Path.home())])
|
|
163
|
+
modules: list[Module] = field(default_factory=lambda: list(Module))
|
|
164
|
+
exclude_globs: list[str] = field(default_factory=list)
|
|
165
|
+
no_default_excludes: bool = False
|
|
166
|
+
include_volumes: bool = False
|
|
167
|
+
max_file_size: int = DEFAULT_MAX_FILE_SIZE
|
|
168
|
+
min_severity: Severity = Severity.LOW
|
|
169
|
+
min_confidence: Confidence = Confidence.LOW
|
|
170
|
+
rules_file: str = ""
|
|
171
|
+
concurrency: int = 0
|
|
172
|
+
reveal_length: bool = False
|
|
173
|
+
output_dir: str = ""
|
|
174
|
+
formats: list[str] = field(default_factory=lambda: ["table"])
|
|
175
|
+
|
|
176
|
+
def effective_excludes(self) -> list[str]:
|
|
177
|
+
if self.no_default_excludes:
|
|
178
|
+
return list(self.exclude_globs)
|
|
179
|
+
return DEFAULT_EXCLUDE_GLOBS + list(self.exclude_globs)
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def default() -> Config:
|
|
183
|
+
from secretscan.model import DEFAULT_MODULES
|
|
184
|
+
return Config(
|
|
185
|
+
paths=[str(Path.home())],
|
|
186
|
+
modules=list(DEFAULT_MODULES),
|
|
187
|
+
)
|