scan4secrets 2.1.2__tar.gz → 2.1.3__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.
- {scan4secrets-2.1.2/scan4secrets.egg-info → scan4secrets-2.1.3}/PKG-INFO +1 -1
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/pyproject.toml +1 -1
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/__init__.py +1 -1
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/cli.py +83 -14
- {scan4secrets-2.1.2 → scan4secrets-2.1.3/scan4secrets.egg-info}/PKG-INFO +1 -1
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/LICENSE +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/README.md +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/__main__.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/rules.yaml +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/CloudProvider-Service.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/Docker-Compose-Kubernetes.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/Keys-SSH-Certificate.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/Node.js-Express-JS.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/OtherConfig-CI-DevOps.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/Python-Django-Flask.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/React-Next.js-Vite-Frontend.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/admin-panels.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/api-paths.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/backup-files.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/common.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/database-dumps.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/env.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/php-laravel-symfony-drupal.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/wordpress.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/__init__.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/crawler.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/entropy.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/findings.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/rules.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/scanner.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/sourcemap.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/verifier.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/wordlists.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/__init__.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/csv_.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/excel.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/html.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/json_.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/jsonl.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/pdf.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/sarif.py +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets.egg-info/SOURCES.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets.egg-info/dependency_links.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets.egg-info/entry_points.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets.egg-info/requires.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets.egg-info/top_level.txt +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/setup.cfg +0 -0
- {scan4secrets-2.1.2 → scan4secrets-2.1.3}/tests/test_rules.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scan4secrets"
|
|
7
|
-
version = "2.1.
|
|
7
|
+
version = "2.1.3"
|
|
8
8
|
description = "DAST + SAST secret scanner with live verification, source-map parsing, and CI-native reporting"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -15,8 +15,12 @@ from collections import Counter
|
|
|
15
15
|
|
|
16
16
|
from rich.console import Console
|
|
17
17
|
from rich.table import Table
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn, MofNCompleteColumn
|
|
18
20
|
from rich import box
|
|
19
21
|
|
|
22
|
+
AUTHOR_LINE = "by m14r41 - github.com/m14r41"
|
|
23
|
+
|
|
20
24
|
from scan4secrets import __version__
|
|
21
25
|
from scan4secrets.engine.rules import load_rules
|
|
22
26
|
from scan4secrets.engine.scanner import scan_path, DEFAULT_SKIP_DIRS, DEFAULT_MAX_BYTES
|
|
@@ -157,6 +161,23 @@ def _print_findings(findings, console: Console, *, mask: bool = False):
|
|
|
157
161
|
console.print(tbl)
|
|
158
162
|
|
|
159
163
|
|
|
164
|
+
def _print_banner(console: Console) -> None:
|
|
165
|
+
body = (
|
|
166
|
+
f"[bold cyan]scan4secrets[/] [dim]v{__version__}[/]\n"
|
|
167
|
+
"[dim]DAST + SAST secret scanner -- verify findings against vendor APIs[/]\n"
|
|
168
|
+
f"[dim]{AUTHOR_LINE}[/]"
|
|
169
|
+
)
|
|
170
|
+
console.print(Panel(body, box=box.ROUNDED, border_style="cyan", padding=(0, 2)))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _ok(console: Console, msg: str) -> None:
|
|
174
|
+
console.print(f"[bold green][+][/] {msg}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _info(console: Console, msg: str) -> None:
|
|
178
|
+
console.print(f"[bold cyan][*][/] {msg}")
|
|
179
|
+
|
|
180
|
+
|
|
160
181
|
def main(argv=None) -> int:
|
|
161
182
|
args = _parser().parse_args(argv)
|
|
162
183
|
|
|
@@ -169,10 +190,13 @@ def main(argv=None) -> int:
|
|
|
169
190
|
|
|
170
191
|
console = Console(quiet=args.quiet, no_color=args.no_color)
|
|
171
192
|
|
|
193
|
+
if not args.quiet:
|
|
194
|
+
_print_banner(console)
|
|
195
|
+
|
|
172
196
|
rules = load_rules(args.rules)
|
|
173
197
|
rules = _filter_rules(rules, args.rule_id, args.disable_rule, args.entropy_min)
|
|
174
198
|
if not args.quiet:
|
|
175
|
-
console
|
|
199
|
+
_ok(console, f"Loaded [bold]{len(rules)}[/] rules" + (f" from [dim]{args.rules}[/]" if args.rules else ""))
|
|
176
200
|
|
|
177
201
|
findings = []
|
|
178
202
|
|
|
@@ -180,23 +204,34 @@ def main(argv=None) -> int:
|
|
|
180
204
|
from scan4secrets.engine.scanner import scan_text
|
|
181
205
|
from scan4secrets.engine.rules import KeywordIndex
|
|
182
206
|
idx = KeywordIndex(rules)
|
|
207
|
+
if not args.quiet:
|
|
208
|
+
_info(console, "Reading stdin...")
|
|
183
209
|
text = sys.stdin.read()
|
|
184
210
|
findings.extend(scan_text(text, "<stdin>", rules, idx))
|
|
211
|
+
if not args.quiet:
|
|
212
|
+
_ok(console, f"stdin scanned ({len(text)} bytes)")
|
|
185
213
|
|
|
186
214
|
if args.path:
|
|
187
215
|
if not args.quiet:
|
|
188
|
-
console
|
|
216
|
+
_info(console, f"[bold]SAST[/] scanning [bold]{args.path}[/]")
|
|
189
217
|
exclude_dirs = DEFAULT_SKIP_DIRS | set(args.exclude_dir)
|
|
218
|
+
before = len(findings)
|
|
190
219
|
findings.extend(scan_path(
|
|
191
220
|
Path(args.path), rules,
|
|
192
221
|
exclude_dirs=exclude_dirs,
|
|
193
222
|
exclude_globs=args.exclude,
|
|
194
223
|
max_bytes=_parse_size(args.max_size),
|
|
195
224
|
))
|
|
225
|
+
if not args.quiet:
|
|
226
|
+
_ok(console, f"SAST complete -- [bold]{len(findings) - before}[/] raw findings")
|
|
196
227
|
|
|
197
228
|
if args.url:
|
|
229
|
+
target = normalize_url(args.url)
|
|
198
230
|
if not args.quiet:
|
|
199
|
-
console
|
|
231
|
+
_info(console, f"[bold]DAST[/] target: [bold]{target}[/]")
|
|
232
|
+
_info(console, f"threads={args.threads} max_urls={args.max_urls} max_depth={args.max_depth} timeout={args.timeout}s"
|
|
233
|
+
+ (" [yellow]proxy=" + args.proxy + "[/]" if args.proxy else "")
|
|
234
|
+
+ (" [yellow]insecure-tls[/]" if args.insecure else ""))
|
|
200
235
|
session = build_session(
|
|
201
236
|
user_agent=args.user_agent or None,
|
|
202
237
|
headers=_parse_headers(args.header),
|
|
@@ -209,18 +244,20 @@ def main(argv=None) -> int:
|
|
|
209
244
|
if args.no_wordlist:
|
|
210
245
|
scope_label = "disabled"
|
|
211
246
|
elif args.wordlist:
|
|
212
|
-
extra_seeds = seed_urls_from_files(
|
|
247
|
+
extra_seeds = seed_urls_from_files(target, args.wordlist)
|
|
213
248
|
scope_label = f"user:{','.join(args.wordlist)}"
|
|
214
249
|
elif args.wordlist_only:
|
|
215
|
-
extra_seeds = seed_urls_from_wordlists(
|
|
250
|
+
extra_seeds = seed_urls_from_wordlists(target, only=args.wordlist_only)
|
|
216
251
|
scope_label = f"bundled:{','.join(args.wordlist_only)}"
|
|
217
252
|
else:
|
|
218
|
-
extra_seeds = seed_urls_from_wordlists(
|
|
253
|
+
extra_seeds = seed_urls_from_wordlists(target)
|
|
219
254
|
scope_label = "bundled:all"
|
|
220
255
|
if not args.quiet and not args.no_wordlist:
|
|
221
|
-
console
|
|
222
|
-
|
|
223
|
-
|
|
256
|
+
_ok(console, f"Wordlist [bold]{scope_label}[/] seeded [bold]{len(extra_seeds)}[/] candidate URLs")
|
|
257
|
+
|
|
258
|
+
before = len(findings)
|
|
259
|
+
crawl_kwargs = dict(
|
|
260
|
+
rules=rules,
|
|
224
261
|
session=session,
|
|
225
262
|
max_urls=args.max_urls,
|
|
226
263
|
max_depth=args.max_depth,
|
|
@@ -230,7 +267,31 @@ def main(argv=None) -> int:
|
|
|
230
267
|
parse_sourcemaps=not args.no_sourcemaps,
|
|
231
268
|
extract_js_endpoints=not args.no_js_endpoints,
|
|
232
269
|
extra_seeds=extra_seeds,
|
|
233
|
-
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if args.quiet:
|
|
273
|
+
findings.extend(crawl_and_scan(target, **crawl_kwargs))
|
|
274
|
+
else:
|
|
275
|
+
progress = Progress(
|
|
276
|
+
SpinnerColumn(),
|
|
277
|
+
TextColumn("[bold cyan][+][/] Crawling"),
|
|
278
|
+
BarColumn(),
|
|
279
|
+
MofNCompleteColumn(),
|
|
280
|
+
TextColumn("[dim]URLs[/]"),
|
|
281
|
+
TimeElapsedColumn(),
|
|
282
|
+
console=console,
|
|
283
|
+
transient=True,
|
|
284
|
+
)
|
|
285
|
+
with progress:
|
|
286
|
+
task = progress.add_task("crawl", total=args.max_urls)
|
|
287
|
+
last_url = {"u": ""}
|
|
288
|
+
|
|
289
|
+
def _cb(url: str) -> None:
|
|
290
|
+
last_url["u"] = url
|
|
291
|
+
progress.advance(task)
|
|
292
|
+
|
|
293
|
+
findings.extend(crawl_and_scan(target, progress_cb=_cb, **crawl_kwargs))
|
|
294
|
+
_ok(console, f"DAST complete -- [bold]{len(findings) - before}[/] raw findings")
|
|
234
295
|
|
|
235
296
|
# dedupe across SAST + DAST
|
|
236
297
|
seen = set()
|
|
@@ -240,6 +301,8 @@ def main(argv=None) -> int:
|
|
|
240
301
|
continue
|
|
241
302
|
seen.add(f.dedup_key())
|
|
242
303
|
deduped.append(f)
|
|
304
|
+
if len(deduped) < len(findings) and not args.quiet:
|
|
305
|
+
_ok(console, f"Deduplication: {len(findings)} -> [bold]{len(deduped)}[/] unique")
|
|
243
306
|
findings = deduped
|
|
244
307
|
|
|
245
308
|
# noise reduction: suppress generic rules when a specific vendor rule fired on the same value
|
|
@@ -247,12 +310,16 @@ def main(argv=None) -> int:
|
|
|
247
310
|
before = len(findings)
|
|
248
311
|
findings = suppress_generic_when_specific(findings)
|
|
249
312
|
if before > len(findings) and not args.quiet:
|
|
250
|
-
console
|
|
313
|
+
_ok(console, f"Suppressed [bold]{before - len(findings)}[/] generic-rule duplicates")
|
|
251
314
|
|
|
252
315
|
if args.verify and findings:
|
|
316
|
+
verifiable = sum(1 for f in findings if any(r.id == f.rule_id and r.verify for r in rules))
|
|
253
317
|
if not args.quiet:
|
|
254
|
-
console
|
|
318
|
+
_info(console, f"Verifying [bold]{verifiable}[/] candidates against vendor APIs...")
|
|
255
319
|
verify_findings(findings, rules, timeout=args.verify_timeout)
|
|
320
|
+
if not args.quiet:
|
|
321
|
+
verified = sum(1 for f in findings if f.verified is True)
|
|
322
|
+
_ok(console, f"Verified [bold green]{verified}[/]/[bold]{verifiable}[/] live")
|
|
256
323
|
|
|
257
324
|
if not args.quiet:
|
|
258
325
|
_print_findings(findings, console, mask=args.mask)
|
|
@@ -261,12 +328,14 @@ def main(argv=None) -> int:
|
|
|
261
328
|
if findings:
|
|
262
329
|
out_base = Path(args.output)
|
|
263
330
|
out_base.parent.mkdir(parents=True, exist_ok=True)
|
|
331
|
+
if not args.quiet:
|
|
332
|
+
_info(console, f"Writing reports: [bold]{' '.join(args.report)}[/] -> [dim]{out_base}.*[/]")
|
|
264
333
|
written = write_reports(findings, out_base, args.report, unsafe_show=not args.mask)
|
|
265
334
|
if not args.quiet:
|
|
266
335
|
for fmt, p in written.items():
|
|
267
|
-
console
|
|
336
|
+
_ok(console, f"{fmt.upper():6s} -> [bold]{p}[/]")
|
|
268
337
|
elif not args.quiet:
|
|
269
|
-
console
|
|
338
|
+
_ok(console, "[bold green]No secrets found.[/]")
|
|
270
339
|
|
|
271
340
|
if args.fail_on and any(severity_at_least(f.severity, args.fail_on) for f in findings):
|
|
272
341
|
return 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/CloudProvider-Service.txt
RENAMED
|
File without changes
|
{scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/Docker-Compose-Kubernetes.txt
RENAMED
|
File without changes
|
{scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/Keys-SSH-Certificate.txt
RENAMED
|
File without changes
|
{scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/Node.js-Express-JS.txt
RENAMED
|
File without changes
|
{scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/OtherConfig-CI-DevOps.txt
RENAMED
|
File without changes
|
{scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/Python-Django-Flask.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|