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.
Files changed (48) hide show
  1. {scan4secrets-2.1.2/scan4secrets.egg-info → scan4secrets-2.1.3}/PKG-INFO +1 -1
  2. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/pyproject.toml +1 -1
  3. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/__init__.py +1 -1
  4. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/cli.py +83 -14
  5. {scan4secrets-2.1.2 → scan4secrets-2.1.3/scan4secrets.egg-info}/PKG-INFO +1 -1
  6. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/LICENSE +0 -0
  7. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/README.md +0 -0
  8. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/__main__.py +0 -0
  9. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/rules.yaml +0 -0
  10. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/CloudProvider-Service.txt +0 -0
  11. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/Docker-Compose-Kubernetes.txt +0 -0
  12. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/Keys-SSH-Certificate.txt +0 -0
  13. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/Node.js-Express-JS.txt +0 -0
  14. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/OtherConfig-CI-DevOps.txt +0 -0
  15. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/Python-Django-Flask.txt +0 -0
  16. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/React-Next.js-Vite-Frontend.txt +0 -0
  17. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/admin-panels.txt +0 -0
  18. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/api-paths.txt +0 -0
  19. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/backup-files.txt +0 -0
  20. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/common.txt +0 -0
  21. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/database-dumps.txt +0 -0
  22. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/env.txt +0 -0
  23. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/php-laravel-symfony-drupal.txt +0 -0
  24. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/config/wordlist/wordpress.txt +0 -0
  25. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/__init__.py +0 -0
  26. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/crawler.py +0 -0
  27. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/entropy.py +0 -0
  28. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/findings.py +0 -0
  29. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/rules.py +0 -0
  30. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/scanner.py +0 -0
  31. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/sourcemap.py +0 -0
  32. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/verifier.py +0 -0
  33. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/engine/wordlists.py +0 -0
  34. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/__init__.py +0 -0
  35. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/csv_.py +0 -0
  36. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/excel.py +0 -0
  37. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/html.py +0 -0
  38. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/json_.py +0 -0
  39. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/jsonl.py +0 -0
  40. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/pdf.py +0 -0
  41. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets/reporters/sarif.py +0 -0
  42. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets.egg-info/SOURCES.txt +0 -0
  43. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets.egg-info/dependency_links.txt +0 -0
  44. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets.egg-info/entry_points.txt +0 -0
  45. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets.egg-info/requires.txt +0 -0
  46. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/scan4secrets.egg-info/top_level.txt +0 -0
  47. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/setup.cfg +0 -0
  48. {scan4secrets-2.1.2 → scan4secrets-2.1.3}/tests/test_rules.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scan4secrets
3
- Version: 2.1.2
3
+ Version: 2.1.3
4
4
  Summary: DAST + SAST secret scanner with live verification, source-map parsing, and CI-native reporting
5
5
  Author: M14R41
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scan4secrets"
7
- version = "2.1.2"
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"
@@ -1,3 +1,3 @@
1
1
  """scan4secrets — DAST + SAST secret scanner."""
2
2
 
3
- __version__ = "2.1.2"
3
+ __version__ = "2.1.3"
@@ -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.print(f"[bold]scan4secrets v{__version__}[/] {len(rules)} rules loaded")
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.print(f"[bold cyan]SAST[/] scanning {args.path}")
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.print(f"[bold cyan]DAST[/] crawling {args.url}")
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(normalize_url(args.url), args.wordlist)
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(normalize_url(args.url), only=args.wordlist_only)
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(normalize_url(args.url))
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.print(f"[dim]wordlist ({scope_label}) seeded {len(extra_seeds)} candidate URLs[/]")
222
- findings.extend(crawl_and_scan(
223
- normalize_url(args.url), rules,
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.print(f"[dim]suppressed {before - len(findings)} generic-rule duplicates[/]")
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.print(f"[bold]Verifying[/] {sum(1 for f in findings if any(r.id == f.rule_id and r.verify for r in rules))} candidates...")
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.print(f"[green]+[/] {fmt.upper():6s} -> {p}")
336
+ _ok(console, f"{fmt.upper():6s} -> [bold]{p}[/]")
268
337
  elif not args.quiet:
269
- console.print("[green]No secrets found.[/]")
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scan4secrets
3
- Version: 2.1.2
3
+ Version: 2.1.3
4
4
  Summary: DAST + SAST secret scanner with live verification, source-map parsing, and CI-native reporting
5
5
  Author: M14R41
6
6
  License: MIT
File without changes
File without changes
File without changes