secretsieve 1.0.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.
- secretsieve/__init__.py +9 -0
- secretsieve/__main__.py +6 -0
- secretsieve/cli/__init__.py +1 -0
- secretsieve/cli/app.py +274 -0
- secretsieve/cli/output.py +22 -0
- secretsieve/cli/parser.py +83 -0
- secretsieve/config/__init__.py +1 -0
- secretsieve/config/loader.py +285 -0
- secretsieve/config/template.py +55 -0
- secretsieve/detectors/__init__.py +1 -0
- secretsieve/detectors/context.py +149 -0
- secretsieve/detectors/engine.py +663 -0
- secretsieve/detectors/entropy.py +187 -0
- secretsieve/detectors/fp_filters.py +134 -0
- secretsieve/detectors/heuristics.py +79 -0
- secretsieve/detectors/scoring.py +95 -0
- secretsieve/detectors/signatures.py +75 -0
- secretsieve/git/__init__.py +56 -0
- secretsieve/models/__init__.py +1 -0
- secretsieve/models/config.py +46 -0
- secretsieve/models/finding.py +133 -0
- secretsieve/models/rule.py +119 -0
- secretsieve/models/stats.py +40 -0
- secretsieve/reporting/__init__.py +1 -0
- secretsieve/reporting/human.py +141 -0
- secretsieve/reporting/json_report.py +52 -0
- secretsieve/rules/__init__.py +65 -0
- secretsieve/rules/aws.py +36 -0
- secretsieve/rules/azure.py +20 -0
- secretsieve/rules/database.py +23 -0
- secretsieve/rules/discord.py +21 -0
- secretsieve/rules/dotenv.py +25 -0
- secretsieve/rules/generic.py +69 -0
- secretsieve/rules/github.py +20 -0
- secretsieve/rules/gitlab.py +20 -0
- secretsieve/rules/google.py +36 -0
- secretsieve/rules/jwt.py +24 -0
- secretsieve/rules/privatekey.py +22 -0
- secretsieve/rules/registry.py +78 -0
- secretsieve/rules/shell.py +23 -0
- secretsieve/rules/slack.py +20 -0
- secretsieve/rules/stripe.py +22 -0
- secretsieve/scanner/__init__.py +1 -0
- secretsieve/scanner/discovery.py +165 -0
- secretsieve/scanner/filetypes.py +120 -0
- secretsieve/scanner/filters.py +82 -0
- secretsieve/scanner/gating.py +32 -0
- secretsieve/scanner/orchestrator.py +140 -0
- secretsieve/scanner/reader.py +56 -0
- secretsieve/utils/__init__.py +1 -0
- secretsieve/utils/paths.py +59 -0
- secretsieve/utils/redact.py +61 -0
- secretsieve/utils/sanitize.py +35 -0
- secretsieve/utils/timing.py +23 -0
- secretsieve-1.0.0.dist-info/METADATA +109 -0
- secretsieve-1.0.0.dist-info/RECORD +59 -0
- secretsieve-1.0.0.dist-info/WHEEL +4 -0
- secretsieve-1.0.0.dist-info/entry_points.txt +2 -0
- secretsieve-1.0.0.dist-info/licenses/LICENSE +21 -0
secretsieve/__init__.py
ADDED
secretsieve/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package (stdlib argparse only - no third-party CLI framework)."""
|
secretsieve/cli/app.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Scan controller: argv -> config -> engine -> reporter -> exit code.
|
|
2
|
+
|
|
3
|
+
Copyright (c) 3rabDev - https://3rabdev.online
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import traceback
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from secretsieve import __version__
|
|
14
|
+
from secretsieve.cli import output as outmod
|
|
15
|
+
from secretsieve.cli.parser import build_explicit_parser, build_scan_parser
|
|
16
|
+
from secretsieve.config import loader as config_loader
|
|
17
|
+
from secretsieve.config.template import TEMPLATE
|
|
18
|
+
from secretsieve.detectors import scoring
|
|
19
|
+
from secretsieve.scanner.orchestrator import run_scan
|
|
20
|
+
|
|
21
|
+
SUBCOMMANDS = ("scan", "rules", "explain", "config")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(argv: list[str] | None = None) -> int:
|
|
25
|
+
args_list = list(sys.argv[1:] if argv is None else argv)
|
|
26
|
+
try:
|
|
27
|
+
if args_list and args_list[0] in SUBCOMMANDS:
|
|
28
|
+
return _run_subcommand(args_list)
|
|
29
|
+
return _run_scan(args_list)
|
|
30
|
+
except (config_loader.ConfigError, FileNotFoundError) as exc:
|
|
31
|
+
print(f"secretsieve: error: {exc}", file=sys.stderr)
|
|
32
|
+
return 2
|
|
33
|
+
except BrokenPipeError:
|
|
34
|
+
return 2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------------ scanning
|
|
38
|
+
|
|
39
|
+
def _run_scan(argv: list[str]) -> int:
|
|
40
|
+
parser = build_scan_parser()
|
|
41
|
+
args = parser.parse_args(argv)
|
|
42
|
+
if getattr(args, "version", False):
|
|
43
|
+
_print_version()
|
|
44
|
+
return 0
|
|
45
|
+
return _execute_scan(args, targets=args.paths or ["."])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _execute_scan(args: object, targets: list[str]) -> int:
|
|
49
|
+
get = getattr
|
|
50
|
+
cfg, cfg_warnings = config_loader.discover_config(
|
|
51
|
+
get(args, "config", None), bool(get(args, "no_config", False)), Path.cwd()
|
|
52
|
+
)
|
|
53
|
+
cfg = config_loader.apply_cli(cfg, args)
|
|
54
|
+
if get(args, "min_confidence", None) is not None and not 0 <= int(get(args, "min_confidence")) <= 99:
|
|
55
|
+
print("secretsieve: error: --min-confidence must be 0-99", file=sys.stderr)
|
|
56
|
+
return 2
|
|
57
|
+
if get(args, "output", None) and not cfg.as_json:
|
|
58
|
+
print("secretsieve: error: --output requires --json", file=sys.stderr)
|
|
59
|
+
return 2
|
|
60
|
+
|
|
61
|
+
scan_targets = list(targets)
|
|
62
|
+
git_warnings: list[str] = []
|
|
63
|
+
if get(args, "staged", False) or get(args, "unstaged", False):
|
|
64
|
+
from secretsieve import git as gitmod
|
|
65
|
+
|
|
66
|
+
cwd = Path.cwd()
|
|
67
|
+
selected: list[str] = []
|
|
68
|
+
try:
|
|
69
|
+
if get(args, "staged", False):
|
|
70
|
+
selected += gitmod.get_staged_files(cwd)
|
|
71
|
+
if get(args, "unstaged", False):
|
|
72
|
+
selected += gitmod.get_unstaged_files(cwd)
|
|
73
|
+
except gitmod.GitError as exc:
|
|
74
|
+
print(f"secretsieve: error: {exc}", file=sys.stderr)
|
|
75
|
+
return 2
|
|
76
|
+
selected = [p for p in dict.fromkeys(selected) if (cwd / p).exists()]
|
|
77
|
+
if not selected:
|
|
78
|
+
git_warnings.append("git file list is empty; nothing to scan")
|
|
79
|
+
scan_targets = selected or []
|
|
80
|
+
if not scan_targets:
|
|
81
|
+
# Still emit a valid (empty) report for CI stability.
|
|
82
|
+
return _emit_empty(args, cfg, cfg_warnings + git_warnings)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
outcome = run_scan(scan_targets or ["."], cfg)
|
|
86
|
+
except FileNotFoundError as exc:
|
|
87
|
+
print(f"secretsieve: error: {exc}", file=sys.stderr)
|
|
88
|
+
return 2
|
|
89
|
+
|
|
90
|
+
warnings = cfg_warnings + git_warnings + outcome.warnings
|
|
91
|
+
|
|
92
|
+
gate = [f for f in outcome.findings if scoring.meets_fail_on(f.severity, cfg.fail_on)]
|
|
93
|
+
if gate:
|
|
94
|
+
exit_code = 1
|
|
95
|
+
elif outcome.stats.files_errored > 0:
|
|
96
|
+
exit_code = 2
|
|
97
|
+
else:
|
|
98
|
+
exit_code = 0
|
|
99
|
+
|
|
100
|
+
if cfg.as_json:
|
|
101
|
+
return _emit_json(args, cfg, outcome, warnings, exit_code)
|
|
102
|
+
|
|
103
|
+
display = [
|
|
104
|
+
f for f in outcome.findings
|
|
105
|
+
if scoring.meets_floor(f.severity, cfg.severity_floor) and f.confidence >= cfg.min_confidence
|
|
106
|
+
]
|
|
107
|
+
text = outmod.render_human(
|
|
108
|
+
findings=display,
|
|
109
|
+
stats=outcome.stats,
|
|
110
|
+
version=__version__,
|
|
111
|
+
targets=scan_targets or ["."],
|
|
112
|
+
duration_ms=outcome.duration_ms,
|
|
113
|
+
exit_code=exit_code,
|
|
114
|
+
use_color=outmod.should_use_color(cfg.no_color, False),
|
|
115
|
+
quiet=cfg.quiet,
|
|
116
|
+
verbose=cfg.verbose,
|
|
117
|
+
warnings=warnings,
|
|
118
|
+
file_errors=list(outcome.stats.errors),
|
|
119
|
+
timing=dict(outcome.timing),
|
|
120
|
+
)
|
|
121
|
+
_write_stdout(text)
|
|
122
|
+
for w in warnings:
|
|
123
|
+
if not cfg.verbose:
|
|
124
|
+
print(f"secretsieve: warning: {w}", file=sys.stderr)
|
|
125
|
+
for e in outcome.stats.errors:
|
|
126
|
+
print(f"secretsieve: warning: {e}", file=sys.stderr)
|
|
127
|
+
return exit_code
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _emit_json(args: object, cfg: object, outcome: object, warnings: list[str], exit_code: int) -> int:
|
|
131
|
+
from secretsieve.reporting.json_report import build_report
|
|
132
|
+
|
|
133
|
+
get = getattr
|
|
134
|
+
report = build_report(
|
|
135
|
+
version=__version__,
|
|
136
|
+
scan_root=str(get(outcome, "scan_root", ".")),
|
|
137
|
+
duration_ms=int(get(outcome, "duration_ms", 0)),
|
|
138
|
+
stats=get(outcome, "stats"),
|
|
139
|
+
findings=list(get(outcome, "findings", [])),
|
|
140
|
+
config_snapshot=get(cfg, "snapshot")(),
|
|
141
|
+
exit_code=exit_code,
|
|
142
|
+
warnings=warnings,
|
|
143
|
+
)
|
|
144
|
+
payload = json.dumps(report, ensure_ascii=True, indent=2, sort_keys=False)
|
|
145
|
+
out_path = get(cfg, "output_path", None)
|
|
146
|
+
if out_path:
|
|
147
|
+
try:
|
|
148
|
+
Path(out_path).write_text(payload + "\n", encoding="utf-8")
|
|
149
|
+
except OSError as exc:
|
|
150
|
+
print(f"secretsieve: error: cannot write --output file: {exc}", file=sys.stderr)
|
|
151
|
+
return 2
|
|
152
|
+
else:
|
|
153
|
+
_write_stdout(payload + "\n")
|
|
154
|
+
for w in warnings:
|
|
155
|
+
print(f"secretsieve: warning: {w}", file=sys.stderr)
|
|
156
|
+
for e in get(get(outcome, "stats"), "errors", []):
|
|
157
|
+
print(f"secretsieve: warning: {e}", file=sys.stderr)
|
|
158
|
+
return exit_code
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _emit_empty(args: object, cfg: object, warnings: list[str]) -> int:
|
|
162
|
+
"""Emit a valid empty report (used when git file lists are empty)."""
|
|
163
|
+
from secretsieve.models.stats import ScanStats
|
|
164
|
+
from types import SimpleNamespace
|
|
165
|
+
|
|
166
|
+
outcome = SimpleNamespace(findings=[], stats=ScanStats(), warnings=[], scan_root=Path.cwd(), duration_ms=0, timing={})
|
|
167
|
+
if bool(getattr(cfg, "as_json", False)):
|
|
168
|
+
return _emit_json(args, cfg, outcome, warnings, 0)
|
|
169
|
+
text = outmod.render_human(
|
|
170
|
+
findings=[], stats=outcome.stats, version=__version__, targets=["(git file list: empty)"],
|
|
171
|
+
duration_ms=0, exit_code=0, use_color=False, quiet=bool(getattr(cfg, "quiet", False)),
|
|
172
|
+
verbose=bool(getattr(cfg, "verbose", False)), warnings=warnings, file_errors=[],
|
|
173
|
+
)
|
|
174
|
+
_write_stdout(text)
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _write_stdout(text: str) -> None:
|
|
179
|
+
try:
|
|
180
|
+
sys.stdout.write(text)
|
|
181
|
+
except BrokenPipeError:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# -------------------------------------------------------------- subcommands
|
|
186
|
+
|
|
187
|
+
def _run_subcommand(argv: list[str]) -> int:
|
|
188
|
+
name = argv[0]
|
|
189
|
+
if name == "scan":
|
|
190
|
+
parser = build_scan_parser()
|
|
191
|
+
args = parser.parse_args(argv[1:])
|
|
192
|
+
if getattr(args, "version", False):
|
|
193
|
+
_print_version()
|
|
194
|
+
return 0
|
|
195
|
+
return _execute_scan(args, targets=args.paths or ["."])
|
|
196
|
+
parser = build_explicit_parser()
|
|
197
|
+
args = parser.parse_args(argv)
|
|
198
|
+
cmd = getattr(args, "command", None)
|
|
199
|
+
if cmd == "rules":
|
|
200
|
+
return _cmd_rules(args)
|
|
201
|
+
if cmd == "explain":
|
|
202
|
+
return _cmd_explain(str(getattr(args, "rule_id", "")))
|
|
203
|
+
if cmd == "config":
|
|
204
|
+
return _cmd_config(args)
|
|
205
|
+
parser.print_help()
|
|
206
|
+
return 2
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _cmd_rules(args: object) -> int:
|
|
210
|
+
from secretsieve.rules import RULE_REGISTRY, RULES_BY_ID
|
|
211
|
+
from secretsieve.utils.sanitize import sanitize_for_terminal
|
|
212
|
+
|
|
213
|
+
if getattr(args, "list", False):
|
|
214
|
+
for rule in sorted(RULE_REGISTRY, key=lambda r: r.id):
|
|
215
|
+
print(f"{rule.id:<22} {rule.severity:<8} {sanitize_for_terminal(rule.name)}")
|
|
216
|
+
print(f"\n{len(RULE_REGISTRY)} rules - https://3rabdev.online")
|
|
217
|
+
return 0
|
|
218
|
+
show = getattr(args, "show", None)
|
|
219
|
+
if show:
|
|
220
|
+
return _cmd_explain(str(show))
|
|
221
|
+
return 2
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _cmd_explain(rule_id: str) -> int:
|
|
225
|
+
from secretsieve.rules import RULES_BY_ID
|
|
226
|
+
from secretsieve.utils.sanitize import sanitize_for_terminal
|
|
227
|
+
|
|
228
|
+
rid = rule_id.strip().upper()
|
|
229
|
+
# Accept the documented alias.
|
|
230
|
+
if rid == "SS-GENERIC-JWT-001":
|
|
231
|
+
rid = "SS-JWT-001"
|
|
232
|
+
rule = RULES_BY_ID.get(rid)
|
|
233
|
+
if rule is None:
|
|
234
|
+
print(f"secretsieve: error: unknown rule '{rule_id.strip()}' (try: secretsieve rules --list)", file=sys.stderr)
|
|
235
|
+
return 2
|
|
236
|
+
print(f"{sanitize_for_terminal(rule.id)} - {sanitize_for_terminal(rule.name)}")
|
|
237
|
+
print(f"Provider: {sanitize_for_terminal(rule.provider)}")
|
|
238
|
+
print(f"Category: {sanitize_for_terminal(rule.category)}")
|
|
239
|
+
print(f"Severity: {rule.severity}")
|
|
240
|
+
print(f"Confidence: base {rule.base_confidence} (adjusted by context, entropy, structure)")
|
|
241
|
+
print(f"Needs context: {'yes' if rule.context_required else 'no (standalone signature)'}")
|
|
242
|
+
print(f"Keywords: {sanitize_for_terminal(', '.join(rule.keywords) or '-')}")
|
|
243
|
+
print(f"About: {sanitize_for_terminal(rule.description)}")
|
|
244
|
+
print(f"Fix: {sanitize_for_terminal(rule.remediation)}")
|
|
245
|
+
print(f"FP notes: {sanitize_for_terminal(rule.fp_notes)}")
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _cmd_config(args: object) -> int:
|
|
250
|
+
if getattr(args, "init", False):
|
|
251
|
+
_write_stdout(TEMPLATE)
|
|
252
|
+
return 0
|
|
253
|
+
# --validate
|
|
254
|
+
try:
|
|
255
|
+
cfg, warnings = config_loader.discover_config(
|
|
256
|
+
getattr(args, "config", None), bool(getattr(args, "no_config", False)), Path.cwd()
|
|
257
|
+
)
|
|
258
|
+
except config_loader.ConfigError as exc:
|
|
259
|
+
print(f"secretsieve: error: {exc}", file=sys.stderr)
|
|
260
|
+
return 2
|
|
261
|
+
unknown = config_loader.validate_rule_ids(list(cfg.disabled_rules))
|
|
262
|
+
if unknown:
|
|
263
|
+
print(f"secretsieve: error: unknown rule ID(s): {', '.join(unknown)}", file=sys.stderr)
|
|
264
|
+
return 2
|
|
265
|
+
where = cfg.config_path or "(defaults; no config file found)"
|
|
266
|
+
print(f"config OK - {where}")
|
|
267
|
+
for w in warnings:
|
|
268
|
+
print(f"warning: {w}")
|
|
269
|
+
return 0
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _print_version() -> None:
|
|
273
|
+
print(f"secretsieve {__version__}")
|
|
274
|
+
print("Copyright (c) 3rabDev - https://3rabdev.online")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Output helpers: TTY color detection + renderer re-exports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from secretsieve.reporting.human import render_human # noqa: F401 - public re-export
|
|
9
|
+
from secretsieve.reporting.json_report import build_report # noqa: F401 - public re-export
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def should_use_color(no_color_flag: bool, json_mode: bool) -> bool:
|
|
13
|
+
if no_color_flag or json_mode:
|
|
14
|
+
return False
|
|
15
|
+
if os.environ.get("NO_COLOR"):
|
|
16
|
+
return False
|
|
17
|
+
if os.environ.get("TERM") == "dumb":
|
|
18
|
+
return False
|
|
19
|
+
try:
|
|
20
|
+
return sys.stdout.isatty()
|
|
21
|
+
except Exception:
|
|
22
|
+
return False
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Argument parsing (PLAN Sec. 24).
|
|
2
|
+
|
|
3
|
+
Dispatch model: ``scan`` (explicit or implicit), ``rules``, ``explain``,
|
|
4
|
+
``config``. The implicit form (``secretsieve [PATH] [options]``) is parsed
|
|
5
|
+
with the same scan options as ``secretsieve scan ...``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
|
|
12
|
+
SEVERITY_CHOICES = ("info", "low", "medium", "high", "critical")
|
|
13
|
+
FAIL_CHOICES = ("low", "medium", "high", "critical")
|
|
14
|
+
|
|
15
|
+
_EPILOG = (
|
|
16
|
+
"Privacy: SecretSieve is local-first - scanning performs no network I/O, "
|
|
17
|
+
"uploads nothing, and verifies nothing against third-party services. "
|
|
18
|
+
"Output is redacted by default. See https://3rabdev.online"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def add_scan_options(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
|
23
|
+
parser.add_argument("paths", nargs="*", default=[], help="files or directories to scan (default: .)")
|
|
24
|
+
parser.add_argument("--json", action="store_true", help="machine-readable JSON report on stdout")
|
|
25
|
+
parser.add_argument("-q", "--quiet", action="store_true", help="findings only (one line per finding)")
|
|
26
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="detail without secrets: stats, suppress counts, skips")
|
|
27
|
+
parser.add_argument("--no-color", action="store_true", help="disable ANSI color")
|
|
28
|
+
parser.add_argument("--severity", choices=SEVERITY_CHOICES, default=None,
|
|
29
|
+
help="minimum DISPLAY severity (default: low; info shows INFO)")
|
|
30
|
+
parser.add_argument("--fail-on", choices=FAIL_CHOICES, default=None,
|
|
31
|
+
help="minimum severity that fails CI (default: low; INFO never fails)")
|
|
32
|
+
parser.add_argument("--min-confidence", type=int, default=None, help="hide findings below N (0-99)")
|
|
33
|
+
parser.add_argument("--exclude", action="append", default=[], metavar="GLOB",
|
|
34
|
+
help="extra exclude glob (repeatable)")
|
|
35
|
+
parser.add_argument("--include", action="append", default=[], metavar="GLOB",
|
|
36
|
+
help="restrict to matching paths (repeatable)")
|
|
37
|
+
parser.add_argument("--no-default-excludes", action="store_true", help="drop default directory exclusions")
|
|
38
|
+
parser.add_argument("--follow-symlinks", action="store_true", help="follow directory symlinks during descent")
|
|
39
|
+
parser.add_argument("--staged", action="store_true", help="scan files staged in the git index")
|
|
40
|
+
parser.add_argument("--unstaged", action="store_true", help="scan tracked-modified + untracked files")
|
|
41
|
+
parser.add_argument("--config", default=None, metavar="PATH", help="config file path")
|
|
42
|
+
parser.add_argument("--no-config", action="store_true", help="ignore config files (hermetic CI)")
|
|
43
|
+
parser.add_argument("--disable-rule", action="append", default=[], metavar="ID",
|
|
44
|
+
help="rule kill-switch, e.g. SS-DISCORD-001 (repeatable)")
|
|
45
|
+
parser.add_argument("--max-file-bytes", type=int, default=None, metavar="N", help="override size gate")
|
|
46
|
+
parser.add_argument("--output", default=None, metavar="PATH", help="write JSON report to file (with --json)")
|
|
47
|
+
return parser
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_scan_parser() -> argparse.ArgumentParser:
|
|
51
|
+
parser = argparse.ArgumentParser(prog="secretsieve", description="SecretSieve - sieve the secrets out of your source.", epilog=_EPILOG)
|
|
52
|
+
parser.add_argument("--version", action="store_true", help="print version and exit")
|
|
53
|
+
return add_scan_options(parser)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_explicit_parser() -> argparse.ArgumentParser:
|
|
57
|
+
parser = argparse.ArgumentParser(prog="secretsieve", description="SecretSieve - sieve the secrets out of your source.", epilog=_EPILOG)
|
|
58
|
+
parser.add_argument("--version", action="store_true", help="print version and exit")
|
|
59
|
+
sub = parser.add_subparsers(dest="command")
|
|
60
|
+
|
|
61
|
+
scan_p = sub.add_parser("scan", help="scan files or directories (explicit form)")
|
|
62
|
+
add_scan_options(scan_p)
|
|
63
|
+
|
|
64
|
+
rules_p = sub.add_parser("rules", help="list / show detection rules")
|
|
65
|
+
group = rules_p.add_mutually_exclusive_group(required=True)
|
|
66
|
+
group.add_argument("--list", action="store_true", help="list all rules")
|
|
67
|
+
group.add_argument("--show", metavar="ID", help="show one rule card")
|
|
68
|
+
|
|
69
|
+
explain_p = sub.add_parser("explain", help="explain a rule ID")
|
|
70
|
+
explain_p.add_argument("rule_id", help="e.g. SS-GITHUB-001")
|
|
71
|
+
|
|
72
|
+
config_p = sub.add_parser("config", help="config helpers")
|
|
73
|
+
cgroup = config_p.add_mutually_exclusive_group(required=True)
|
|
74
|
+
cgroup.add_argument("--init", action="store_true", help="print a starter secretsieve.toml")
|
|
75
|
+
cgroup.add_argument("--validate", action="store_true", help="validate the resolved config file")
|
|
76
|
+
config_p.add_argument("--config", default=None, metavar="PATH", help="config file path")
|
|
77
|
+
config_p.add_argument("--no-config", action="store_true", help="ignore config files")
|
|
78
|
+
return parser
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def parse_scan_args(argv: list[str]) -> argparse.Namespace:
|
|
82
|
+
"""Parse implicit-scan argv (testable without I/O)."""
|
|
83
|
+
return build_scan_parser().parse_args(argv)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Config package: TOML loading + template generation."""
|