souleyez 2.27.0__py3-none-any.whl → 2.32.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.

Potentially problematic release.


This version of souleyez might be problematic. Click here for more details.

@@ -5356,12 +5356,43 @@ def view_job_detail(job_id: int):
5356
5356
  # Check if exploitation was detected
5357
5357
  if job.get('exploitation_detected'):
5358
5358
  session_info = job.get('session_info', 'Session opened')
5359
- click.echo(f"Status: {click.style('🎯 EXPLOITED', fg='green', bold=True)} - {session_info}{elapsed_str}")
5359
+ click.echo(f"Status: {click.style('EXPLOITED', fg='green', bold=True)} - {session_info}{elapsed_str}")
5360
5360
  else:
5361
5361
  click.echo(f"Status: {click.style('▶ ' + status, fg=status_color, bold=True)}{elapsed_str}")
5362
5362
  else:
5363
5363
  click.echo(f"Status: {click.style(status, fg=status_color)}{elapsed_str}")
5364
5364
 
5365
+ # Check for connection issues in running jobs
5366
+ connection_warning = None
5367
+ log_path = job.get('log')
5368
+ if status == 'running' and log_path and os.path.exists(log_path):
5369
+ try:
5370
+ with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
5371
+ # Read last 5KB of log to check for recent connection issues
5372
+ f.seek(0, 2) # Seek to end
5373
+ file_size = f.tell()
5374
+ read_size = min(5000, file_size)
5375
+ f.seek(max(0, file_size - read_size))
5376
+ recent_log = f.read().lower()
5377
+
5378
+ # Count connection failure patterns
5379
+ connection_patterns = [
5380
+ 'unable to connect',
5381
+ 'connection refused',
5382
+ 'connection timed out',
5383
+ 'going to retry',
5384
+ 'target url content is not stable',
5385
+ 'considerable lagging has been detected',
5386
+ ]
5387
+ failure_count = sum(recent_log.count(pattern) for pattern in connection_patterns)
5388
+
5389
+ if failure_count >= 5:
5390
+ connection_warning = f"Connection issues detected ({failure_count} failures) - target may be overloaded"
5391
+ elif failure_count >= 2:
5392
+ connection_warning = f"Some connection issues detected ({failure_count} retries)"
5393
+ except Exception:
5394
+ pass
5395
+
5365
5396
  # Human-readable status summary
5366
5397
  status_summaries = {
5367
5398
  'done': 'Scan completed successfully with findings',
@@ -5377,6 +5408,11 @@ def view_job_detail(job_id: int):
5377
5408
  summary_color = 'yellow' if status in ['warning', 'error', 'killed'] else 'bright_black'
5378
5409
  click.echo(f" {click.style(summary, fg=summary_color)}")
5379
5410
 
5411
+ # Show connection warning if detected
5412
+ if connection_warning:
5413
+ click.echo(f" {click.style('WARNING: ' + connection_warning, fg='yellow', bold=True)}")
5414
+ click.echo(f" {click.style('Consider: Kill job [k] and retry with fewer threads or --time-sec=10', fg='yellow', dim=True)}")
5415
+
5380
5416
  click.echo(f"Created: {job.get('created_at', 'N/A')}")
5381
5417
 
5382
5418
  if job.get('args'):
@@ -5388,10 +5424,14 @@ def view_job_detail(job_id: int):
5388
5424
  # Show reason (how the job was created)
5389
5425
  metadata = job.get('metadata', {})
5390
5426
  reason = metadata.get('reason', '')
5391
- if reason:
5427
+ parent_id = job.get('parent_id')
5428
+ if reason and parent_id:
5429
+ # Show both reason and parent job reference
5430
+ click.echo(f"Reason: {reason} (from job #{parent_id})")
5431
+ elif reason:
5392
5432
  click.echo(f"Reason: {reason}")
5393
- elif job.get('parent_id'):
5394
- click.echo(f"Reason: Auto-chained from job #{job.get('parent_id')}")
5433
+ elif parent_id:
5434
+ click.echo(f"Reason: Auto-chained from job #{parent_id}")
5395
5435
  else:
5396
5436
  click.echo(f"Reason: Manual (created by user)")
5397
5437
 
@@ -5593,7 +5633,7 @@ def view_job_detail(job_id: int):
5593
5633
  pass
5594
5634
 
5595
5635
  # Parse and display SQLMap results if available (only when not showing raw logs)
5596
- if not show_raw_logs and job.get('tool') == 'sqlmap' and job.get('status') == 'done' and log_path and os.path.exists(log_path):
5636
+ if not show_raw_logs and job.get('tool') == 'sqlmap' and job.get('status') in ['done', 'completed', 'no_results'] and log_path and os.path.exists(log_path):
5597
5637
  try:
5598
5638
  with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
5599
5639
  log_content = f.read()
@@ -5602,11 +5642,17 @@ def view_job_detail(job_id: int):
5602
5642
  parsed = parse_sqlmap_output(log_content, job.get('target', ''))
5603
5643
  stats = get_sqli_stats(parsed)
5604
5644
 
5645
+ # Always show summary header
5646
+ click.echo(click.style("=" * 70, fg='cyan'))
5647
+ click.echo(click.style("SQL INJECTION SCAN", bold=True, fg='cyan'))
5648
+ click.echo(click.style("=" * 70, fg='cyan'))
5649
+ click.echo()
5650
+
5651
+ click.echo(click.style(f"Target: {job.get('target', 'unknown')}", bold=True))
5652
+
5605
5653
  # Show summary if anything was found
5606
5654
  if stats['total_vulns'] > 0 or stats['databases_found'] > 0:
5607
- click.echo(click.style("=" * 70, fg='cyan'))
5608
- click.echo(click.style("SQLMAP RESULTS SUMMARY", bold=True, fg='cyan'))
5609
- click.echo(click.style("=" * 70, fg='cyan'))
5655
+ click.echo(click.style(f"Result: {stats['total_vulns']} vulnerability(ies) found", fg='red', bold=True))
5610
5656
  click.echo()
5611
5657
 
5612
5658
  # Injection details with techniques and payloads
@@ -5737,15 +5783,15 @@ def view_job_detail(job_id: int):
5737
5783
  if engagement_id and stats['total_vulns'] > 0:
5738
5784
  fm = FindingsManager()
5739
5785
  all_findings = fm.list_findings(engagement_id)
5740
-
5786
+
5741
5787
  # Check if findings from this job already exist
5742
5788
  target = job.get('target', '')
5743
5789
  job_id = job.get('id')
5744
-
5790
+
5745
5791
  # Look for findings with matching tool and target URL
5746
- existing_findings = [f for f in all_findings
5792
+ existing_findings = [f for f in all_findings
5747
5793
  if f.get('tool') == 'sqlmap' and target in f.get('path', '')]
5748
-
5794
+
5749
5795
  if existing_findings:
5750
5796
  click.echo(click.style(f"✓ Findings already saved ({len(existing_findings)} findings from this scan)", fg='green'))
5751
5797
  else:
@@ -5755,14 +5801,30 @@ def view_job_detail(job_id: int):
5755
5801
  _save_sqlmap_findings(engagement_id, job, parsed)
5756
5802
  else:
5757
5803
  click.echo(click.style("⚠ No engagement set - findings not saved", fg='yellow'))
5758
-
5804
+ else:
5805
+ # No vulnerabilities found - show friendly message
5806
+ click.echo(click.style("Result: No SQL injection vulnerabilities found", fg='yellow'))
5807
+ click.echo()
5808
+ click.echo(f"URLs tested: {stats.get('urls_tested', 0)}")
5809
+ click.echo()
5810
+ click.echo("The target was tested but no injectable parameters were found.")
5811
+ click.echo()
5812
+ click.echo(click.style("Tips:", fg='bright_black'))
5813
+ click.echo(click.style(" - Try increasing --level and --risk for deeper testing", fg='bright_black'))
5814
+ click.echo(click.style(" - Test with authenticated session cookies", fg='bright_black'))
5815
+ click.echo(click.style(" - Try different injection techniques (--technique=BEUST)", fg='bright_black'))
5816
+ click.echo()
5817
+
5818
+ click.echo(click.style("=" * 70, fg='cyan'))
5819
+ click.echo()
5820
+
5759
5821
  except Exception as e:
5760
5822
  click.echo(click.style(f"Error parsing SQLMap results: {e}", fg='yellow'))
5761
5823
  import traceback
5762
5824
  traceback.print_exc()
5763
5825
 
5764
5826
  # Parse and display WPScan results if available (only when not showing raw logs)
5765
- if not show_raw_logs and job.get('tool') == 'wpscan' and job.get('status') in ['done', 'completed'] and log_path and os.path.exists(log_path):
5827
+ if not show_raw_logs and job.get('tool') == 'wpscan' and job.get('status') in ['done', 'completed', 'no_results'] and log_path and os.path.exists(log_path):
5766
5828
  try:
5767
5829
  with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
5768
5830
  log_content = f.read()
@@ -5927,12 +5989,35 @@ def view_job_detail(job_id: int):
5927
5989
  click.echo(f" ... and {len(users) - 10} more users")
5928
5990
  click.echo()
5929
5991
 
5992
+ click.echo(click.style("=" * 70, fg='cyan'))
5993
+ click.echo()
5994
+ else:
5995
+ # No results - show friendly message
5996
+ click.echo(click.style("=" * 70, fg='cyan'))
5997
+ click.echo(click.style("WPSCAN WORDPRESS SECURITY SCAN", bold=True, fg='cyan'))
5998
+ click.echo(click.style("=" * 70, fg='cyan'))
5999
+ click.echo()
6000
+
6001
+ click.echo(click.style(f"Target: {job.get('target', 'unknown')}", bold=True))
6002
+ click.echo()
6003
+ click.echo(click.style("Result: No WordPress detected or no issues found", fg='green', bold=True))
6004
+ click.echo()
6005
+ click.echo(" The scan did not find WordPress or any security issues.")
6006
+ click.echo()
6007
+ click.echo(click.style("Tips:", dim=True))
6008
+ click.echo(" • Verify the target is a WordPress site")
6009
+ click.echo(" • Try enumeration: --enumerate ap,at,u")
6010
+ click.echo(" • Check API token for vuln database access")
6011
+ click.echo()
6012
+ click.echo(click.style("=" * 70, fg='cyan'))
6013
+ click.echo()
6014
+
5930
6015
  except Exception as e:
5931
6016
  # Silently fail - not critical
5932
6017
  pass
5933
6018
 
5934
6019
  # Parse and display DNSRecon results if available (only when not showing raw logs)
5935
- if not show_raw_logs and job.get('tool') == 'dnsrecon' and job.get('status') in ['done', 'completed'] and log_path and os.path.exists(log_path):
6020
+ if not show_raw_logs and job.get('tool') == 'dnsrecon' and job.get('status') in ['done', 'completed', 'no_results'] and log_path and os.path.exists(log_path):
5936
6021
  try:
5937
6022
  from souleyez.parsers.dnsrecon_parser import parse_dnsrecon_output
5938
6023
  with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
@@ -5945,50 +6030,69 @@ def view_job_detail(job_id: int):
5945
6030
  click.echo(click.style("=" * 70, fg='cyan'))
5946
6031
  click.echo()
5947
6032
 
5948
- # Hosts (A records)
5949
- hosts = parsed.get('hosts', [])
5950
- if hosts:
5951
- click.echo(click.style(f"Hosts (A Records): {len(hosts)}", bold=True))
5952
- for host in hosts:
5953
- click.echo(f" • {host['hostname']} → {host['ip']}")
5954
- click.echo()
6033
+ click.echo(click.style(f"Target: {job.get('target', 'unknown')}", bold=True))
6034
+ click.echo()
5955
6035
 
5956
- # Nameservers
6036
+ # Collect all results
6037
+ hosts = parsed.get('hosts', [])
5957
6038
  ns = parsed.get('nameservers', [])
5958
- if ns:
5959
- click.echo(click.style(f"Nameservers (NS): {len(ns)}", bold=True))
5960
- for server in ns:
5961
- click.echo(f" • {server}")
5962
- click.echo()
5963
-
5964
- # Mail servers
5965
6039
  mx = parsed.get('mail_servers', [])
5966
- if mx:
5967
- click.echo(click.style(f"Mail Servers (MX): {len(mx)}", bold=True))
5968
- for server in mx[:5]: # Show first 5
5969
- click.echo(f" • {server}")
5970
- if len(mx) > 5:
5971
- click.echo(f" ... and {len(mx) - 5} more")
5972
- click.echo()
5973
-
5974
- # TXT records
5975
6040
  txt = parsed.get('txt_records', [])
5976
- if txt:
5977
- click.echo(click.style(f"TXT Records: {len(txt)}", bold=True))
5978
- for record in txt:
5979
- # Truncate long records
5980
- display = record[:80] + "..." if len(record) > 80 else record
5981
- click.echo(f" • {display}")
5982
- click.echo()
5983
-
5984
- # Subdomains
5985
6041
  subdomains = parsed.get('subdomains', [])
5986
- if subdomains:
5987
- click.echo(click.style(f"Subdomains: {len(subdomains)}", bold=True))
5988
- for sub in subdomains[:10]: # Show first 10
5989
- click.echo(f" • {sub}")
5990
- if len(subdomains) > 10:
5991
- click.echo(f" ... and {len(subdomains) - 10} more")
6042
+
6043
+ has_results = hosts or ns or mx or txt or subdomains
6044
+
6045
+ if has_results:
6046
+ # Hosts (A records)
6047
+ if hosts:
6048
+ click.echo(click.style(f"Hosts (A Records): {len(hosts)}", bold=True))
6049
+ for host in hosts:
6050
+ click.echo(f" • {host['hostname']} → {host['ip']}")
6051
+ click.echo()
6052
+
6053
+ # Nameservers
6054
+ if ns:
6055
+ click.echo(click.style(f"Nameservers (NS): {len(ns)}", bold=True))
6056
+ for server in ns:
6057
+ click.echo(f" • {server}")
6058
+ click.echo()
6059
+
6060
+ # Mail servers
6061
+ if mx:
6062
+ click.echo(click.style(f"Mail Servers (MX): {len(mx)}", bold=True))
6063
+ for server in mx[:5]: # Show first 5
6064
+ click.echo(f" • {server}")
6065
+ if len(mx) > 5:
6066
+ click.echo(f" ... and {len(mx) - 5} more")
6067
+ click.echo()
6068
+
6069
+ # TXT records
6070
+ if txt:
6071
+ click.echo(click.style(f"TXT Records: {len(txt)}", bold=True))
6072
+ for record in txt:
6073
+ # Truncate long records
6074
+ display = record[:80] + "..." if len(record) > 80 else record
6075
+ click.echo(f" • {display}")
6076
+ click.echo()
6077
+
6078
+ # Subdomains
6079
+ if subdomains:
6080
+ click.echo(click.style(f"Subdomains: {len(subdomains)}", bold=True))
6081
+ for sub in subdomains[:10]: # Show first 10
6082
+ click.echo(f" • {sub}")
6083
+ if len(subdomains) > 10:
6084
+ click.echo(f" ... and {len(subdomains) - 10} more")
6085
+ click.echo()
6086
+ else:
6087
+ # No results - show friendly message
6088
+ click.echo(click.style("Result: No DNS records discovered", fg='yellow', bold=True))
6089
+ click.echo()
6090
+ click.echo(" The scan did not find any DNS records.")
6091
+ click.echo()
6092
+ click.echo(click.style("Tips:", dim=True))
6093
+ click.echo(" • Verify the domain name is correct")
6094
+ click.echo(" • Try zone transfer: -a -t axfr")
6095
+ click.echo(" • Check if domain has public DNS records")
5992
6096
  click.echo()
5993
6097
 
5994
6098
  click.echo(click.style("=" * 70, fg='cyan'))
@@ -6167,7 +6271,7 @@ def view_job_detail(job_id: int):
6167
6271
  pass
6168
6272
 
6169
6273
  # Parse and display Nuclei results if available (only when not showing raw logs)
6170
- if not show_raw_logs and job.get('tool') == 'nuclei' and job.get('status') in ['done', 'completed'] and log_path and os.path.exists(log_path):
6274
+ if not show_raw_logs and job.get('tool') == 'nuclei' and job.get('status') in ['done', 'completed', 'no_results'] and log_path and os.path.exists(log_path):
6171
6275
  try:
6172
6276
  from souleyez.parsers.nuclei_parser import parse_nuclei_output
6173
6277
  with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
@@ -6175,10 +6279,17 @@ def view_job_detail(job_id: int):
6175
6279
  parsed = parse_nuclei_output(log_content, job.get('target', ''))
6176
6280
 
6177
6281
  findings = parsed.get('findings', [])
6282
+
6283
+ # Always show summary header
6284
+ click.echo(click.style("=" * 70, fg='cyan'))
6285
+ click.echo(click.style("VULNERABILITY SCAN", bold=True, fg='cyan'))
6286
+ click.echo(click.style("=" * 70, fg='cyan'))
6287
+ click.echo()
6288
+
6289
+ click.echo(click.style(f"Target: {job.get('target', 'unknown')}", bold=True))
6290
+
6178
6291
  if findings:
6179
- click.echo(click.style("=" * 70, fg='cyan'))
6180
- click.echo(click.style("NUCLEI FINDINGS", bold=True, fg='cyan'))
6181
- click.echo(click.style("=" * 70, fg='cyan'))
6292
+ click.echo(click.style(f"Result: {len(findings)} vulnerability(ies) found", fg='red', bold=True))
6182
6293
  click.echo()
6183
6294
 
6184
6295
  # Group by severity
@@ -6215,13 +6326,27 @@ def view_job_detail(job_id: int):
6215
6326
  click.echo(f" ... and {len(items) - 5} more")
6216
6327
  click.echo()
6217
6328
 
6329
+ click.echo(click.style("=" * 70, fg='cyan'))
6330
+ click.echo()
6331
+ else:
6332
+ # No findings - show friendly message
6333
+ click.echo(click.style("Result: No vulnerabilities detected", fg='green', bold=True))
6334
+ click.echo()
6335
+ click.echo(" The scan completed without finding any vulnerabilities.")
6336
+ click.echo(" This could mean the target is secure or templates didn't match.")
6337
+ click.echo()
6338
+ click.echo(click.style("Tips:", dim=True))
6339
+ click.echo(" • Try different severity levels: -severity critical,high,medium,low")
6340
+ click.echo(" • Try specific tags: -tags cve,exposure,misconfiguration")
6341
+ click.echo(" • Update templates: nuclei -update-templates")
6342
+ click.echo()
6218
6343
  click.echo(click.style("=" * 70, fg='cyan'))
6219
6344
  click.echo()
6220
6345
  except Exception as e:
6221
6346
  pass
6222
6347
 
6223
6348
  # Parse and display theHarvester results if available (only when not showing raw logs)
6224
- if not show_raw_logs and job.get('tool') == 'theharvester' and job.get('status') in ['done', 'completed'] and log_path and os.path.exists(log_path):
6349
+ if not show_raw_logs and job.get('tool') == 'theharvester' and job.get('status') in ['done', 'completed', 'no_results'] and log_path and os.path.exists(log_path):
6225
6350
  try:
6226
6351
  from souleyez.parsers.theharvester_parser import parse_theharvester_output
6227
6352
  with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
@@ -6233,54 +6358,73 @@ def view_job_detail(job_id: int):
6233
6358
  click.echo(click.style("=" * 70, fg='cyan'))
6234
6359
  click.echo()
6235
6360
 
6236
- # Emails
6237
- emails = parsed.get('emails', [])
6238
- if emails:
6239
- click.echo(click.style(f"Emails: {len(emails)}", bold=True))
6240
- for email in emails[:10]:
6241
- click.echo(f" • {email}")
6242
- if len(emails) > 10:
6243
- click.echo(f" ... and {len(emails) - 10} more")
6244
- click.echo()
6361
+ click.echo(click.style(f"Target: {job.get('target', 'unknown')}", bold=True))
6362
+ click.echo()
6245
6363
 
6246
- # IPs
6364
+ # Collect all results
6365
+ emails = parsed.get('emails', [])
6247
6366
  ips = parsed.get('ips', [])
6248
- if ips:
6249
- click.echo(click.style(f"IP Addresses: {len(ips)}", bold=True))
6250
- for ip in ips[:10]:
6251
- click.echo(f" • {ip}")
6252
- if len(ips) > 10:
6253
- click.echo(f" ... and {len(ips) - 10} more")
6254
- click.echo()
6255
-
6256
- # ASNs
6257
6367
  asns = parsed.get('asns', [])
6258
- if asns:
6259
- click.echo(click.style(f"ASNs: {len(asns)}", bold=True))
6260
- for asn in asns[:10]:
6261
- click.echo(f" • {asn}")
6262
- if len(asns) > 10:
6263
- click.echo(f" ... and {len(asns) - 10} more")
6264
- click.echo()
6265
-
6266
- # Interesting URLs
6267
6368
  urls = parsed.get('urls', parsed.get('base_urls', []))
6268
- if urls:
6269
- click.echo(click.style(f"Interesting URLs: {len(urls)}", bold=True))
6270
- for url in urls[:15]:
6271
- click.echo(f" • {url}")
6272
- if len(urls) > 15:
6273
- click.echo(f" ... and {len(urls) - 15} more")
6274
- click.echo()
6275
-
6276
- # Subdomains
6277
6369
  subdomains = parsed.get('subdomains', [])
6278
- if subdomains:
6279
- click.echo(click.style(f"Hosts Found: {len(subdomains)}", bold=True))
6280
- for sub in subdomains[:15]:
6281
- click.echo(f" • {sub}")
6282
- if len(subdomains) > 15:
6283
- click.echo(f" ... and {len(subdomains) - 15} more")
6370
+
6371
+ has_results = emails or ips or asns or urls or subdomains
6372
+
6373
+ if has_results:
6374
+ # Emails
6375
+ if emails:
6376
+ click.echo(click.style(f"Emails: {len(emails)}", bold=True))
6377
+ for email in emails[:10]:
6378
+ click.echo(f" • {email}")
6379
+ if len(emails) > 10:
6380
+ click.echo(f" ... and {len(emails) - 10} more")
6381
+ click.echo()
6382
+
6383
+ # IPs
6384
+ if ips:
6385
+ click.echo(click.style(f"IP Addresses: {len(ips)}", bold=True))
6386
+ for ip in ips[:10]:
6387
+ click.echo(f" • {ip}")
6388
+ if len(ips) > 10:
6389
+ click.echo(f" ... and {len(ips) - 10} more")
6390
+ click.echo()
6391
+
6392
+ # ASNs
6393
+ if asns:
6394
+ click.echo(click.style(f"ASNs: {len(asns)}", bold=True))
6395
+ for asn in asns[:10]:
6396
+ click.echo(f" • {asn}")
6397
+ if len(asns) > 10:
6398
+ click.echo(f" ... and {len(asns) - 10} more")
6399
+ click.echo()
6400
+
6401
+ # Interesting URLs
6402
+ if urls:
6403
+ click.echo(click.style(f"Interesting URLs: {len(urls)}", bold=True))
6404
+ for url in urls[:15]:
6405
+ click.echo(f" • {url}")
6406
+ if len(urls) > 15:
6407
+ click.echo(f" ... and {len(urls) - 15} more")
6408
+ click.echo()
6409
+
6410
+ # Subdomains
6411
+ if subdomains:
6412
+ click.echo(click.style(f"Hosts Found: {len(subdomains)}", bold=True))
6413
+ for sub in subdomains[:15]:
6414
+ click.echo(f" • {sub}")
6415
+ if len(subdomains) > 15:
6416
+ click.echo(f" ... and {len(subdomains) - 15} more")
6417
+ click.echo()
6418
+ else:
6419
+ # No results - show friendly message
6420
+ click.echo(click.style("Result: No assets discovered", fg='yellow', bold=True))
6421
+ click.echo()
6422
+ click.echo(" The scan completed without finding any publicly exposed assets.")
6423
+ click.echo()
6424
+ click.echo(click.style("Tips:", dim=True))
6425
+ click.echo(" • Try different data sources (-b google,bing,linkedin)")
6426
+ click.echo(" • Check if the domain is correct")
6427
+ click.echo(" • Some organizations have minimal public exposure")
6284
6428
  click.echo()
6285
6429
 
6286
6430
  click.echo(click.style("=" * 70, fg='cyan'))
@@ -6290,7 +6434,7 @@ def view_job_detail(job_id: int):
6290
6434
  pass
6291
6435
 
6292
6436
  # Parse and display Nikto results if available (only when not showing raw logs)
6293
- if not show_raw_logs and job.get('tool') == 'nikto' and job.get('status') in ['done', 'completed'] and log_path and os.path.exists(log_path):
6437
+ if not show_raw_logs and job.get('tool') == 'nikto' and job.get('status') in ['done', 'completed', 'no_results'] and log_path and os.path.exists(log_path):
6294
6438
  try:
6295
6439
  from souleyez.parsers.nikto_parser import parse_nikto_output
6296
6440
  with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
@@ -6298,21 +6442,25 @@ def view_job_detail(job_id: int):
6298
6442
  parsed = parse_nikto_output(log_content, job.get('target', ''))
6299
6443
 
6300
6444
  findings = parsed.get('findings', [])
6301
- if findings:
6302
- click.echo(click.style("=" * 70, fg='cyan'))
6303
- click.echo(click.style("NIKTO SCAN RESULTS", bold=True, fg='cyan'))
6304
- click.echo(click.style("=" * 70, fg='cyan'))
6305
- click.echo()
6306
6445
 
6307
- # Target info
6308
- if parsed.get('target_ip'):
6309
- click.echo(click.style(f"Target: {parsed['target_ip']}", bold=True))
6310
- if parsed.get('server'):
6311
- click.echo(f"Server: {parsed['server']}")
6312
- if parsed.get('target_port'):
6313
- click.echo(f"Port: {parsed['target_port']}")
6314
- click.echo()
6446
+ # Always show header
6447
+ click.echo(click.style("=" * 70, fg='cyan'))
6448
+ click.echo(click.style("NIKTO SCAN RESULTS", bold=True, fg='cyan'))
6449
+ click.echo(click.style("=" * 70, fg='cyan'))
6450
+ click.echo()
6315
6451
 
6452
+ # Target info
6453
+ if parsed.get('target_ip'):
6454
+ click.echo(click.style(f"Target: {parsed['target_ip']}", bold=True))
6455
+ elif job.get('target'):
6456
+ click.echo(click.style(f"Target: {job.get('target')}", bold=True))
6457
+ if parsed.get('server'):
6458
+ click.echo(f"Server: {parsed['server']}")
6459
+ if parsed.get('target_port'):
6460
+ click.echo(f"Port: {parsed['target_port']}")
6461
+ click.echo()
6462
+
6463
+ if findings:
6316
6464
  # Stats
6317
6465
  stats = parsed.get('stats', {})
6318
6466
  by_severity = stats.get('by_severity', {})
@@ -6356,15 +6504,26 @@ def view_job_detail(job_id: int):
6356
6504
  if len(findings) > 15:
6357
6505
  click.echo(f" ... and {len(findings) - 15} more findings")
6358
6506
  click.echo()
6359
-
6360
- click.echo(click.style("=" * 70, fg='cyan'))
6507
+ else:
6508
+ # No findings - show friendly message
6509
+ click.echo(click.style("Result: No issues detected", fg='green', bold=True))
6361
6510
  click.echo()
6511
+ click.echo(" The scan completed without finding any web server issues.")
6512
+ click.echo()
6513
+ click.echo(click.style("Tips:", dim=True))
6514
+ click.echo(" • Try with different tuning: -T 1-9 for specific test types")
6515
+ click.echo(" • Check if the target web server is running")
6516
+ click.echo(" • Use -Cgidirs to specify CGI directories")
6517
+ click.echo()
6518
+
6519
+ click.echo(click.style("=" * 70, fg='cyan'))
6520
+ click.echo()
6362
6521
  except Exception as e:
6363
6522
  # Fall back to raw log if parsing fails
6364
6523
  pass
6365
6524
 
6366
6525
  # Parse and display WHOIS results if available (only when not showing raw logs)
6367
- if not show_raw_logs and job.get('tool') == 'whois' and job.get('status') in ['done', 'completed'] and log_path and os.path.exists(log_path):
6526
+ if not show_raw_logs and job.get('tool') == 'whois' and job.get('status') in ['done', 'completed', 'no_results'] and log_path and os.path.exists(log_path):
6368
6527
  try:
6369
6528
  from souleyez.parsers.whois_parser import parse_whois_output
6370
6529
  with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
@@ -6376,44 +6535,65 @@ def view_job_detail(job_id: int):
6376
6535
  click.echo(click.style("=" * 70, fg='cyan'))
6377
6536
  click.echo()
6378
6537
 
6379
- # Domain and registrar
6380
- if parsed.get('domain'):
6381
- click.echo(click.style(f"Domain: {parsed['domain']}", bold=True))
6382
- if parsed.get('registrar'):
6383
- click.echo(f"Registrar: {parsed['registrar']}")
6384
- click.echo()
6538
+ # Check if we have any data
6539
+ has_data = (parsed.get('domain') or parsed.get('registrar') or
6540
+ parsed.get('dates') or parsed.get('nameservers') or
6541
+ parsed.get('status') or parsed.get('dnssec'))
6385
6542
 
6386
- # Registration dates
6387
- dates = parsed.get('dates', {})
6388
- if dates:
6389
- click.echo(click.style("Registration Information:", bold=True))
6390
- if dates.get('created'):
6391
- click.echo(f" Created: {dates['created']}")
6392
- if dates.get('updated'):
6393
- click.echo(f" Updated: {dates['updated']}")
6394
- if dates.get('expires'):
6395
- click.echo(f" Expires: {dates['expires']}")
6543
+ if has_data:
6544
+ # Domain and registrar
6545
+ if parsed.get('domain'):
6546
+ click.echo(click.style(f"Domain: {parsed['domain']}", bold=True))
6547
+ elif job.get('target'):
6548
+ click.echo(click.style(f"Target: {job.get('target')}", bold=True))
6549
+ if parsed.get('registrar'):
6550
+ click.echo(f"Registrar: {parsed['registrar']}")
6396
6551
  click.echo()
6397
6552
 
6398
- # Nameservers
6399
- ns = parsed.get('nameservers', [])
6400
- if ns:
6401
- click.echo(click.style(f"Nameservers: {len(ns)}", bold=True))
6402
- for server in ns:
6403
- click.echo(f" {server}")
6404
- click.echo()
6553
+ # Registration dates
6554
+ dates = parsed.get('dates', {})
6555
+ if dates:
6556
+ click.echo(click.style("Registration Information:", bold=True))
6557
+ if dates.get('created'):
6558
+ click.echo(f" Created: {dates['created']}")
6559
+ if dates.get('updated'):
6560
+ click.echo(f" Updated: {dates['updated']}")
6561
+ if dates.get('expires'):
6562
+ click.echo(f" Expires: {dates['expires']}")
6563
+ click.echo()
6405
6564
 
6406
- # Status
6407
- status_list = parsed.get('status', [])
6408
- if status_list:
6409
- click.echo(click.style("Domain Status:", bold=True))
6410
- for status in status_list:
6411
- click.echo(f" • {status}")
6412
- click.echo()
6565
+ # Nameservers
6566
+ ns = parsed.get('nameservers', [])
6567
+ if ns:
6568
+ click.echo(click.style(f"Nameservers: {len(ns)}", bold=True))
6569
+ for server in ns:
6570
+ click.echo(f" • {server}")
6571
+ click.echo()
6572
+
6573
+ # Status
6574
+ status_list = parsed.get('status', [])
6575
+ if status_list:
6576
+ click.echo(click.style("Domain Status:", bold=True))
6577
+ for status in status_list:
6578
+ click.echo(f" • {status}")
6579
+ click.echo()
6413
6580
 
6414
- # DNSSEC
6415
- if parsed.get('dnssec'):
6416
- click.echo(f"DNSSEC: {parsed['dnssec']}")
6581
+ # DNSSEC
6582
+ if parsed.get('dnssec'):
6583
+ click.echo(f"DNSSEC: {parsed['dnssec']}")
6584
+ click.echo()
6585
+ else:
6586
+ # No results - show friendly message
6587
+ click.echo(click.style(f"Target: {job.get('target', 'unknown')}", bold=True))
6588
+ click.echo()
6589
+ click.echo(click.style("Result: No WHOIS information found", fg='yellow', bold=True))
6590
+ click.echo()
6591
+ click.echo(" The WHOIS lookup did not return any information.")
6592
+ click.echo()
6593
+ click.echo(click.style("Tips:", dim=True))
6594
+ click.echo(" • Verify the domain name is correct")
6595
+ click.echo(" • Some domains have private WHOIS")
6596
+ click.echo(" • Try a different WHOIS server")
6417
6597
  click.echo()
6418
6598
 
6419
6599
  click.echo(click.style("=" * 70, fg='cyan'))
@@ -6471,7 +6651,7 @@ def view_job_detail(job_id: int):
6471
6651
  pass
6472
6652
 
6473
6653
  # Parse and display SMBMap results if available (only when not showing raw logs)
6474
- if not show_raw_logs and job.get('tool') == 'smbmap' and job.get('status') in ['done', 'completed'] and log_path and os.path.exists(log_path):
6654
+ if not show_raw_logs and job.get('tool') == 'smbmap' and job.get('status') in ['done', 'completed', 'no_results'] and log_path and os.path.exists(log_path):
6475
6655
  try:
6476
6656
  from souleyez.parsers.smbmap_parser import parse_smbmap_output
6477
6657
  with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
@@ -6479,19 +6659,23 @@ def view_job_detail(job_id: int):
6479
6659
  parsed = parse_smbmap_output(log_content, job.get('target', ''))
6480
6660
 
6481
6661
  shares = parsed.get('shares', [])
6482
- if shares:
6483
- click.echo(click.style("=" * 70, fg='cyan'))
6484
- click.echo(click.style("SMB SHARE ENUMERATION", bold=True, fg='cyan'))
6485
- click.echo(click.style("=" * 70, fg='cyan'))
6486
- click.echo()
6487
6662
 
6488
- if parsed.get('target'):
6489
- click.echo(click.style(f"Target: {parsed['target']}", bold=True))
6490
- if parsed.get('status'):
6491
- auth_color = 'green' if parsed['status'] == 'Authenticated' else 'yellow'
6492
- click.echo(f"Authentication: {click.style(parsed['status'], fg=auth_color)}")
6493
- click.echo()
6663
+ # Always show header
6664
+ click.echo(click.style("=" * 70, fg='cyan'))
6665
+ click.echo(click.style("SMB SHARE ENUMERATION", bold=True, fg='cyan'))
6666
+ click.echo(click.style("=" * 70, fg='cyan'))
6667
+ click.echo()
6668
+
6669
+ if parsed.get('target'):
6670
+ click.echo(click.style(f"Target: {parsed['target']}", bold=True))
6671
+ elif job.get('target'):
6672
+ click.echo(click.style(f"Target: {job.get('target')}", bold=True))
6673
+ if parsed.get('status'):
6674
+ auth_color = 'green' if parsed['status'] == 'Authenticated' else 'yellow'
6675
+ click.echo(f"Authentication: {click.style(parsed['status'], fg=auth_color)}")
6676
+ click.echo()
6494
6677
 
6678
+ if shares:
6495
6679
  # Group shares by permissions
6496
6680
  writable = [s for s in shares if s.get('writable')]
6497
6681
  readable = [s for s in shares if s.get('readable') and not s.get('writable')]
@@ -6528,14 +6712,25 @@ def view_job_detail(job_id: int):
6528
6712
  if files:
6529
6713
  click.echo(click.style(f"Files Enumerated: {len(files)}", bold=True))
6530
6714
  click.echo()
6531
-
6532
- click.echo(click.style("=" * 70, fg='cyan'))
6715
+ else:
6716
+ # No results - show friendly message
6717
+ click.echo(click.style("Result: No SMB shares found", fg='yellow', bold=True))
6718
+ click.echo()
6719
+ click.echo(" The scan did not find any accessible SMB shares.")
6720
+ click.echo()
6721
+ click.echo(click.style("Tips:", dim=True))
6722
+ click.echo(" • Try with credentials: -u user -p password")
6723
+ click.echo(" • Check if SMB is enabled on the target")
6724
+ click.echo(" • Verify the target IP is correct")
6533
6725
  click.echo()
6726
+
6727
+ click.echo(click.style("=" * 70, fg='cyan'))
6728
+ click.echo()
6534
6729
  except Exception as e:
6535
6730
  pass
6536
6731
 
6537
6732
  # Parse and display enum4linux results if available (only when not showing raw logs)
6538
- if not show_raw_logs and job.get('tool') == 'enum4linux' and job.get('status') in ['done', 'completed'] and log_path and os.path.exists(log_path):
6733
+ if not show_raw_logs and job.get('tool') == 'enum4linux' and job.get('status') in ['done', 'completed', 'no_results'] and log_path and os.path.exists(log_path):
6539
6734
  try:
6540
6735
  from souleyez.parsers.enum4linux_parser import parse_enum4linux_output
6541
6736
  with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
@@ -6549,52 +6744,70 @@ def view_job_detail(job_id: int):
6549
6744
 
6550
6745
  if parsed.get('target'):
6551
6746
  click.echo(click.style(f"Target: {parsed['target']}", bold=True))
6747
+ elif job.get('target'):
6748
+ click.echo(click.style(f"Target: {job.get('target')}", bold=True))
6552
6749
  if parsed.get('workgroup'):
6553
6750
  click.echo(f"Workgroup/Domain: {parsed['workgroup']}")
6554
6751
  if parsed.get('domain_sid'):
6555
6752
  click.echo(f"Domain SID: {parsed['domain_sid']}")
6556
6753
  click.echo()
6557
6754
 
6558
- # Users
6755
+ # Collect all results
6559
6756
  users = parsed.get('users', [])
6560
- if users:
6561
- click.echo(click.style(f"Users Discovered ({len(users)}):", bold=True, fg='green'))
6562
- for user in users[:15]:
6563
- click.echo(f" • {user}")
6564
- if len(users) > 15:
6565
- click.echo(f" ... and {len(users) - 15} more")
6566
- click.echo()
6567
-
6568
- # Groups
6569
6757
  groups = parsed.get('groups', [])
6570
- if groups:
6571
- click.echo(click.style(f"Groups Discovered ({len(groups)}):", bold=True, fg='cyan'))
6572
- for group in groups[:10]:
6573
- click.echo(f" • {group}")
6574
- if len(groups) > 10:
6575
- click.echo(f" ... and {len(groups) - 10} more")
6576
- click.echo()
6577
-
6578
- # Shares
6579
6758
  shares = parsed.get('shares', [])
6580
- if shares:
6581
- click.echo(click.style(f"Shares Found ({len(shares)}):", bold=True, fg='yellow'))
6582
- for share in shares:
6583
- name = share.get('name', '')
6584
- share_type = share.get('type', '')
6585
- comment = share.get('comment', '')
6586
- mapping = share.get('mapping', 'N/A')
6587
-
6588
- # Color code by access
6589
- if mapping == 'OK':
6590
- access_display = click.style('Accessible', fg='green')
6591
- elif mapping == 'DENIED':
6592
- access_display = click.style('Denied', fg='red')
6593
- else:
6594
- access_display = click.style('Unknown', dim=True)
6595
6759
 
6596
- comment_str = f" - {comment}" if comment else ""
6597
- click.echo(f" • {name} ({share_type}) [{access_display}]{comment_str}")
6760
+ has_results = users or groups or shares or parsed.get('workgroup') or parsed.get('domain_sid')
6761
+
6762
+ if has_results:
6763
+ # Users
6764
+ if users:
6765
+ click.echo(click.style(f"Users Discovered ({len(users)}):", bold=True, fg='green'))
6766
+ for user in users[:15]:
6767
+ click.echo(f" • {user}")
6768
+ if len(users) > 15:
6769
+ click.echo(f" ... and {len(users) - 15} more")
6770
+ click.echo()
6771
+
6772
+ # Groups
6773
+ if groups:
6774
+ click.echo(click.style(f"Groups Discovered ({len(groups)}):", bold=True, fg='cyan'))
6775
+ for group in groups[:10]:
6776
+ click.echo(f" • {group}")
6777
+ if len(groups) > 10:
6778
+ click.echo(f" ... and {len(groups) - 10} more")
6779
+ click.echo()
6780
+
6781
+ # Shares
6782
+ if shares:
6783
+ click.echo(click.style(f"Shares Found ({len(shares)}):", bold=True, fg='yellow'))
6784
+ for share in shares:
6785
+ name = share.get('name', '')
6786
+ share_type = share.get('type', '')
6787
+ comment = share.get('comment', '')
6788
+ mapping = share.get('mapping', 'N/A')
6789
+
6790
+ # Color code by access
6791
+ if mapping == 'OK':
6792
+ access_display = click.style('Accessible', fg='green')
6793
+ elif mapping == 'DENIED':
6794
+ access_display = click.style('Denied', fg='red')
6795
+ else:
6796
+ access_display = click.style('Unknown', dim=True)
6797
+
6798
+ comment_str = f" - {comment}" if comment else ""
6799
+ click.echo(f" • {name} ({share_type}) [{access_display}]{comment_str}")
6800
+ click.echo()
6801
+ else:
6802
+ # No results - show friendly message
6803
+ click.echo(click.style("Result: No SMB/Samba information discovered", fg='yellow', bold=True))
6804
+ click.echo()
6805
+ click.echo(" The scan did not find any users, groups, or shares.")
6806
+ click.echo()
6807
+ click.echo(click.style("Tips:", dim=True))
6808
+ click.echo(" • Check if SMB is enabled on the target")
6809
+ click.echo(" • Try with credentials for authenticated enumeration")
6810
+ click.echo(" • Verify the target IP is correct")
6598
6811
  click.echo()
6599
6812
 
6600
6813
  click.echo(click.style("=" * 70, fg='cyan'))
@@ -6670,7 +6883,7 @@ def view_job_detail(job_id: int):
6670
6883
  pass
6671
6884
 
6672
6885
  # Parse and display Hydra results if available (only when not showing raw logs)
6673
- if not show_raw_logs and job.get('tool') == 'hydra' and log_path and os.path.exists(log_path):
6886
+ if not show_raw_logs and job.get('tool') == 'hydra' and job.get('status') in ['done', 'completed', 'no_results'] and log_path and os.path.exists(log_path):
6674
6887
  try:
6675
6888
  from souleyez.parsers.hydra_parser import parse_hydra_output
6676
6889
  with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
@@ -6690,7 +6903,7 @@ def view_job_detail(job_id: int):
6690
6903
  # Summary info
6691
6904
  click.echo(click.style(f"Target: {parsed.get('target_host', 'unknown')}", bold=True))
6692
6905
  click.echo(f"Service: {parsed.get('service', 'unknown')} (port {parsed.get('port', 'unknown')})")
6693
- click.echo(click.style(f"\n{len(credentials)} Valid Credential(s) Found", fg='green', bold=True))
6906
+ click.echo(click.style(f"\n{len(credentials)} Valid Credential(s) Found", fg='green', bold=True))
6694
6907
  click.echo()
6695
6908
 
6696
6909
  # Display credentials in table format
@@ -6710,7 +6923,7 @@ def view_job_detail(job_id: int):
6710
6923
 
6711
6924
  # Show password visibility status
6712
6925
  if not show_passwords:
6713
- click.echo(click.style("🔒 Passwords are hidden. Use [p] to reveal.", fg='yellow', dim=True))
6926
+ click.echo(click.style("Passwords are hidden. Use [p] to reveal.", fg='yellow', dim=True))
6714
6927
  click.echo()
6715
6928
 
6716
6929
  # Show if credentials were saved
@@ -6722,7 +6935,7 @@ def view_job_detail(job_id: int):
6722
6935
  all_creds = cm.list_credentials(engagement_id)
6723
6936
  hydra_creds = [c for c in all_creds if c.get('tool') == 'hydra']
6724
6937
  if hydra_creds:
6725
- click.echo(click.style(f"Credentials saved to database ({len(hydra_creds)} total from Hydra)", fg='green'))
6938
+ click.echo(click.style(f"Credentials saved to database ({len(hydra_creds)} total from Hydra)", fg='green'))
6726
6939
  click.echo()
6727
6940
 
6728
6941
  click.echo(click.style("=" * 70, fg='cyan'))
@@ -6738,7 +6951,7 @@ def view_job_detail(job_id: int):
6738
6951
  # Summary info
6739
6952
  click.echo(click.style(f"Target: {parsed.get('target_host', 'unknown')}", bold=True))
6740
6953
  click.echo(f"Service: {parsed.get('service', 'unknown')} (port {parsed.get('port', 'unknown')})")
6741
- click.echo(click.style(f"\n{len(usernames)} Valid Username(s) Found (password unknown)", fg='yellow', bold=True))
6954
+ click.echo(click.style(f"\n{len(usernames)} Valid Username(s) Found (password unknown)", fg='yellow', bold=True))
6742
6955
  click.echo()
6743
6956
 
6744
6957
  # Display usernames
@@ -6752,24 +6965,63 @@ def view_job_detail(job_id: int):
6752
6965
 
6753
6966
  click.echo(click.style("=" * 70, fg='yellow'))
6754
6967
  click.echo()
6968
+ else:
6969
+ # No credentials or usernames found
6970
+ click.echo(click.style("=" * 70, fg='cyan'))
6971
+ click.echo(click.style("HYDRA PASSWORD ATTACK", bold=True, fg='cyan'))
6972
+ click.echo(click.style("=" * 70, fg='cyan'))
6973
+ click.echo()
6974
+
6975
+ # Summary info
6976
+ target = parsed.get('target_host') or job.get('target', 'unknown')
6977
+ click.echo(click.style(f"Target: {target}", bold=True))
6978
+ service = parsed.get('service', 'unknown')
6979
+ port = parsed.get('port', 'unknown')
6980
+ if service != 'unknown' or port != 'unknown':
6981
+ click.echo(f"Service: {service} (port {port})")
6982
+ click.echo()
6983
+
6984
+ click.echo(click.style("Result: No valid credentials found", fg='yellow', bold=True))
6985
+ click.echo()
6986
+ click.echo(" The password attack completed without finding valid credentials.")
6987
+ click.echo()
6988
+ click.echo(click.style("Tips:", dim=True))
6989
+ click.echo(" • Try a larger wordlist")
6990
+ click.echo(" • Verify the service is accessible")
6991
+ click.echo(" • Check if account lockout is enabled")
6992
+ click.echo()
6993
+ click.echo(click.style("=" * 70, fg='cyan'))
6994
+ click.echo()
6755
6995
  except Exception as e:
6756
6996
  # Silently fail - not critical
6757
6997
  pass
6758
6998
 
6759
6999
  # Parse and display SearchSploit results if available (only when not showing raw logs)
6760
- if not show_raw_logs and job.get('tool') == 'searchsploit' and job.get('status') in ['done', 'completed'] and log_path and os.path.exists(log_path):
7000
+ if not show_raw_logs and job.get('tool') == 'searchsploit' and job.get('status') in ['done', 'completed', 'no_results'] and log_path and os.path.exists(log_path):
6761
7001
  try:
6762
7002
  from souleyez.parsers.searchsploit_parser import parse_searchsploit
6763
7003
  parsed = parse_searchsploit(log_path, job.get('target', ''))
6764
7004
 
6765
7005
  exploits = parsed.get('exploits', [])
6766
- if exploits:
6767
- click.echo(click.style("=" * 70, fg='cyan'))
6768
- click.echo(click.style("SEARCHSPLOIT RESULTS", bold=True, fg='cyan'))
6769
- click.echo(click.style("=" * 70, fg='cyan'))
6770
- click.echo()
6771
7006
 
6772
- click.echo(click.style(f"Search Term: {parsed.get('target', 'unknown')}", bold=True))
7007
+ # Always show summary header
7008
+ click.echo(click.style("=" * 70, fg='cyan'))
7009
+ click.echo(click.style("EXPLOIT SEARCH", bold=True, fg='cyan'))
7010
+ click.echo(click.style("=" * 70, fg='cyan'))
7011
+ click.echo()
7012
+
7013
+ # Extract search term from args or target
7014
+ search_term = parsed.get('target', '')
7015
+ if not search_term:
7016
+ # Try to get from job args
7017
+ args = job.get('args', [])
7018
+ non_flag_args = [a for a in args if not a.startswith('-') and a != '--json']
7019
+ if non_flag_args:
7020
+ search_term = ' '.join(non_flag_args)
7021
+
7022
+ click.echo(click.style(f"Search Term: {search_term or 'unknown'}", bold=True))
7023
+
7024
+ if exploits:
6773
7025
  click.echo(click.style(f"Found: {len(exploits)} exploit(s)", fg='green', bold=True))
6774
7026
  click.echo()
6775
7027
 
@@ -6818,6 +7070,19 @@ def view_job_detail(job_id: int):
6818
7070
 
6819
7071
  click.echo(click.style("=" * 70, fg='cyan'))
6820
7072
  click.echo()
7073
+ else:
7074
+ # No exploits found - show friendly message
7075
+ click.echo(click.style("Results: 0 exploits found", fg='yellow'))
7076
+ click.echo()
7077
+ click.echo("No matching exploits found in Exploit-DB database.")
7078
+ click.echo()
7079
+ click.echo(click.style("Tips:", fg='bright_black'))
7080
+ click.echo(click.style(" - Try broader search terms (e.g., 'Apache' instead of 'Apache 2.4.49')", fg='bright_black'))
7081
+ click.echo(click.style(" - Check for typos in the search term", fg='bright_black'))
7082
+ click.echo(click.style(" - Update Exploit-DB: searchsploit -u", fg='bright_black'))
7083
+ click.echo()
7084
+ click.echo(click.style("=" * 70, fg='cyan'))
7085
+ click.echo()
6821
7086
 
6822
7087
  except Exception as e:
6823
7088
  # Silently fail - not critical
@@ -7172,6 +7437,275 @@ def view_results_menu():
7172
7437
  return
7173
7438
 
7174
7439
 
7440
+ def _scope_management_menu(engagement_id: int, engagement_name: str):
7441
+ """Interactive scope management menu for an engagement."""
7442
+ from souleyez.security.scope_validator import ScopeManager, ScopeValidator
7443
+ from souleyez.storage.hosts import HostManager
7444
+ from rich.console import Console
7445
+ from rich.table import Table
7446
+
7447
+ manager = ScopeManager()
7448
+ validator = ScopeValidator(engagement_id)
7449
+
7450
+ while True:
7451
+ DesignSystem.clear_screen()
7452
+ render_standard_header(f"SCOPE MANAGEMENT - {engagement_name}")
7453
+
7454
+ # Get current scope info
7455
+ entries = manager.list_scope(engagement_id)
7456
+ enforcement = validator.get_enforcement_mode()
7457
+
7458
+ # Show enforcement status
7459
+ if enforcement == 'block':
7460
+ enf_color = 'red'
7461
+ enf_desc = 'Block out-of-scope targets'
7462
+ elif enforcement == 'warn':
7463
+ enf_color = 'yellow'
7464
+ enf_desc = 'Warn but allow'
7465
+ else:
7466
+ enf_color = 'bright_black'
7467
+ enf_desc = 'No validation'
7468
+
7469
+ click.echo()
7470
+ click.echo(f" Enforcement: {click.style(enforcement.upper(), fg=enf_color, bold=True)} - {enf_desc}")
7471
+ click.echo()
7472
+
7473
+ # Display scope entries
7474
+ if not entries:
7475
+ click.echo(click.style(" No scope entries defined - all targets allowed", fg='yellow'))
7476
+ click.echo()
7477
+ else:
7478
+ console = Console()
7479
+ table = Table(show_header=True, header_style="bold", box=DesignSystem.TABLE_BOX, padding=(0, 1))
7480
+
7481
+ table.add_column("ID", width=4, justify="right")
7482
+ table.add_column("Type", width=10)
7483
+ table.add_column("Value", min_width=30)
7484
+ table.add_column("Status", width=10)
7485
+ table.add_column("Description", max_width=25)
7486
+
7487
+ for entry in entries:
7488
+ status = "[red]EXCLUDE[/red]" if entry.get('is_excluded') else "[green]INCLUDE[/green]"
7489
+ table.add_row(
7490
+ str(entry['id']),
7491
+ entry['scope_type'],
7492
+ entry['value'],
7493
+ status,
7494
+ entry.get('description', '') or ''
7495
+ )
7496
+
7497
+ console.print(table)
7498
+ click.echo()
7499
+
7500
+ click.echo("─" * get_terminal_width())
7501
+ click.echo()
7502
+ click.echo(" [+] Add scope entry")
7503
+ click.echo(" [-] Remove scope entry")
7504
+ click.echo(" [e] Change enforcement mode")
7505
+ click.echo(" [t] Test target validation")
7506
+ click.echo(" [r] Revalidate all hosts")
7507
+ click.echo(" [l] View validation log")
7508
+ click.echo(" [q] Back")
7509
+ click.echo()
7510
+
7511
+ try:
7512
+ choice = click.prompt(" Select option", type=str, default='q', show_default=False).strip().lower()
7513
+
7514
+ if choice == 'q':
7515
+ return
7516
+
7517
+ elif choice == '+':
7518
+ # Add scope entry
7519
+ click.echo()
7520
+ click.echo(click.style(" Add Scope Entry", fg='cyan', bold=True))
7521
+ click.echo()
7522
+ click.echo(" Type:")
7523
+ click.echo(" [1] CIDR range (e.g., 192.168.1.0/24)")
7524
+ click.echo(" [2] Domain pattern (e.g., *.example.com)")
7525
+ click.echo(" [3] URL (e.g., https://app.example.com)")
7526
+ click.echo(" [4] Hostname/IP (exact match)")
7527
+ click.echo(" [c] Cancel")
7528
+ click.echo()
7529
+
7530
+ type_choice = click.prompt(" Select type", type=str, default='c').strip().lower()
7531
+
7532
+ if type_choice == 'c':
7533
+ continue
7534
+
7535
+ type_map = {'1': 'cidr', '2': 'domain', '3': 'url', '4': 'hostname'}
7536
+ scope_type = type_map.get(type_choice)
7537
+
7538
+ if not scope_type:
7539
+ click.echo(click.style("\n Invalid choice!", fg='red'))
7540
+ click.pause()
7541
+ continue
7542
+
7543
+ # Get value
7544
+ if scope_type == 'cidr':
7545
+ value_hint = "192.168.1.0/24"
7546
+ elif scope_type == 'domain':
7547
+ value_hint = "*.example.com or example.com"
7548
+ elif scope_type == 'url':
7549
+ value_hint = "https://app.example.com"
7550
+ else:
7551
+ value_hint = "10.0.0.1 or server.local"
7552
+
7553
+ value = click.prompt(f"\n Enter {scope_type} ({value_hint})", type=str).strip()
7554
+ if not value:
7555
+ continue
7556
+
7557
+ # Ask if exclusion
7558
+ is_excluded = click.confirm(" Is this an EXCLUSION (deny rule)?", default=False)
7559
+
7560
+ # Optional description
7561
+ description = click.prompt(" Description (optional)", type=str, default="").strip()
7562
+
7563
+ try:
7564
+ scope_id = manager.add_scope(
7565
+ engagement_id=engagement_id,
7566
+ scope_type=scope_type,
7567
+ value=value,
7568
+ is_excluded=is_excluded,
7569
+ description=description or None
7570
+ )
7571
+ entry_type = "exclusion" if is_excluded else "scope entry"
7572
+ click.echo(click.style(f"\n ✓ Added {entry_type}: {scope_type}={value} (ID: {scope_id})", fg='green'))
7573
+ except ValueError as e:
7574
+ click.echo(click.style(f"\n ✗ Invalid value: {e}", fg='red'))
7575
+ except Exception as e:
7576
+ if "UNIQUE constraint" in str(e):
7577
+ click.echo(click.style(f"\n ✗ This scope entry already exists!", fg='red'))
7578
+ else:
7579
+ click.echo(click.style(f"\n ✗ Error: {e}", fg='red'))
7580
+ click.pause()
7581
+
7582
+ elif choice == '-':
7583
+ # Remove scope entry
7584
+ if not entries:
7585
+ click.echo(click.style("\n No scope entries to remove!", fg='yellow'))
7586
+ click.pause()
7587
+ continue
7588
+
7589
+ try:
7590
+ scope_id = click.prompt("\n Enter scope ID to remove", type=int)
7591
+ entry = next((e for e in entries if e['id'] == scope_id), None)
7592
+
7593
+ if entry:
7594
+ if click.confirm(f" Remove '{entry['scope_type']}={entry['value']}'?", default=False):
7595
+ if manager.remove_scope(scope_id):
7596
+ click.echo(click.style(f"\n ✓ Removed scope entry {scope_id}", fg='green'))
7597
+ else:
7598
+ click.echo(click.style(f"\n ✗ Failed to remove scope entry!", fg='red'))
7599
+ else:
7600
+ click.echo(click.style(f"\n ✗ Scope ID {scope_id} not found!", fg='red'))
7601
+ except ValueError:
7602
+ click.echo(click.style("\n ✗ Invalid ID!", fg='red'))
7603
+ click.pause()
7604
+
7605
+ elif choice == 'e':
7606
+ # Change enforcement mode
7607
+ click.echo()
7608
+ click.echo(click.style(" Enforcement Mode", fg='cyan', bold=True))
7609
+ click.echo()
7610
+ click.echo(f" Current: {click.style(enforcement.upper(), fg=enf_color, bold=True)}")
7611
+ click.echo()
7612
+ click.echo(" [1] OFF - No scope validation")
7613
+ click.echo(" [2] WARN - Allow but log warning")
7614
+ click.echo(" [3] BLOCK - Reject out-of-scope targets")
7615
+ click.echo(" [c] Cancel")
7616
+ click.echo()
7617
+
7618
+ mode_choice = click.prompt(" Select mode", type=str, default='c').strip().lower()
7619
+
7620
+ mode_map = {'1': 'off', '2': 'warn', '3': 'block'}
7621
+ new_mode = mode_map.get(mode_choice)
7622
+
7623
+ if new_mode:
7624
+ if manager.set_enforcement(engagement_id, new_mode):
7625
+ click.echo(click.style(f"\n ✓ Enforcement mode set to {new_mode.upper()}", fg='green'))
7626
+ # Refresh validator cache
7627
+ validator = ScopeValidator(engagement_id)
7628
+ else:
7629
+ click.echo(click.style("\n ✗ Failed to set enforcement mode!", fg='red'))
7630
+ click.pause()
7631
+
7632
+ elif choice == 't':
7633
+ # Test target validation
7634
+ click.echo()
7635
+ target = click.prompt(" Enter target to test", type=str).strip()
7636
+ if target:
7637
+ result = validator.validate_target(target)
7638
+ click.echo()
7639
+ if result.is_in_scope:
7640
+ click.echo(click.style(f" ✓ IN SCOPE: {target}", fg='green', bold=True))
7641
+ if result.matched_entry:
7642
+ click.echo(f" Matched: {result.matched_entry.get('value')}")
7643
+ else:
7644
+ click.echo(click.style(f" ✗ OUT OF SCOPE: {target}", fg='red', bold=True))
7645
+ click.echo(f" Reason: {result.reason}")
7646
+ click.echo(f" Enforcement: {enforcement}")
7647
+ click.pause()
7648
+
7649
+ elif choice == 'r':
7650
+ # Revalidate all hosts
7651
+ click.echo()
7652
+ if click.confirm(" Revalidate scope status for all hosts in this engagement?", default=True):
7653
+ hm = HostManager()
7654
+ result = hm.revalidate_scope_status(engagement_id)
7655
+ click.echo()
7656
+ click.echo(click.style(" Revalidation complete:", fg='green'))
7657
+ click.echo(f" Updated: {result['updated']}")
7658
+ click.echo(f" In scope: {result['in_scope']}")
7659
+ click.echo(f" Out of scope: {result['out_of_scope']}")
7660
+ click.pause()
7661
+
7662
+ elif choice == 'l':
7663
+ # View validation log
7664
+ log_entries = manager.get_validation_log(engagement_id, limit=30)
7665
+ click.echo()
7666
+ click.echo(click.style(" Recent Scope Validation Log (last 30)", fg='cyan', bold=True))
7667
+ click.echo()
7668
+
7669
+ if not log_entries:
7670
+ click.echo(" No validation log entries yet.")
7671
+ else:
7672
+ console = Console()
7673
+ table = Table(show_header=True, header_style="bold", box=DesignSystem.TABLE_BOX, padding=(0, 1))
7674
+
7675
+ table.add_column("Time", width=19)
7676
+ table.add_column("Target", min_width=20)
7677
+ table.add_column("Result", width=12)
7678
+ table.add_column("Action", width=10)
7679
+ table.add_column("Job", width=6)
7680
+
7681
+ for entry in log_entries:
7682
+ timestamp = entry.get('created_at', '')[:19]
7683
+ target = entry.get('target', '')
7684
+ result_val = entry.get('validation_result', '')
7685
+ action = entry.get('action_taken', '')
7686
+ job_id = str(entry.get('job_id', '-') or '-')
7687
+
7688
+ # Color code results
7689
+ if result_val == 'in_scope':
7690
+ result_display = "[green]in_scope[/green]"
7691
+ elif result_val == 'out_of_scope':
7692
+ result_display = "[red]out_of_scope[/red]"
7693
+ else:
7694
+ result_display = result_val
7695
+
7696
+ table.add_row(timestamp, target, result_display, action, job_id)
7697
+
7698
+ console.print(table)
7699
+ click.pause()
7700
+
7701
+ else:
7702
+ click.echo(click.style("\n Invalid choice!", fg='red'))
7703
+ click.pause()
7704
+
7705
+ except (KeyboardInterrupt, EOFError):
7706
+ return
7707
+
7708
+
7175
7709
  def _engagements_bulk_action_menu(selected_ids: set, em, current_id: int):
7176
7710
  """Show bulk action menu for selected engagements."""
7177
7711
  click.echo()
@@ -7426,6 +7960,7 @@ def manage_engagements_menu():
7426
7960
  click.echo(" [a] All - Toggle pagination")
7427
7961
  click.echo(" [+] Create - Create new engagement")
7428
7962
  click.echo(" [-] Delete - Delete engagement(s)")
7963
+ click.echo(" [s] Scope - Manage target scope")
7429
7964
  click.echo(" [d] Deliverables - Track progress")
7430
7965
  click.echo(" [q] Back")
7431
7966
 
@@ -7460,6 +7995,15 @@ def manage_engagements_menu():
7460
7995
  current_page = 0
7461
7996
  continue
7462
7997
 
7998
+ elif choice_input == 's':
7999
+ # Scope Management
8000
+ if not current_ws:
8001
+ click.echo(click.style("\n ⚠️ Please select an engagement first!", fg='yellow'))
8002
+ click.pause()
8003
+ continue
8004
+ _scope_management_menu(current_ws['id'], current_ws['name'])
8005
+ continue
8006
+
7463
8007
  elif choice_input == 'd':
7464
8008
  # Deliverables Tracker
7465
8009
  if not current_ws:
@@ -7608,6 +8152,40 @@ def manage_engagements_menu():
7608
8152
  click.echo(click.style(f" ✓ Created engagement '{ws_name}' (ID: {ws_id})", fg='green'))
7609
8153
  if selected_template:
7610
8154
  click.echo(click.style(f" ✓ Applied preset: {selected_template.name}", fg='green'))
8155
+
8156
+ # Save scope to engagement_scope table if provided
8157
+ if ws_scope.strip():
8158
+ from souleyez.security.scope_validator import ScopeManager
8159
+ import ipaddress
8160
+ scope_mgr = ScopeManager()
8161
+ scope_value = ws_scope.strip()
8162
+
8163
+ # Determine scope type
8164
+ scope_type = None
8165
+ try:
8166
+ # Check if CIDR
8167
+ ipaddress.ip_network(scope_value, strict=False)
8168
+ scope_type = 'cidr'
8169
+ except ValueError:
8170
+ try:
8171
+ # Check if single IP
8172
+ ipaddress.ip_address(scope_value)
8173
+ scope_type = 'cidr'
8174
+ scope_value = f"{scope_value}/32" # Convert single IP to CIDR
8175
+ except ValueError:
8176
+ # Check if URL
8177
+ if scope_value.startswith(('http://', 'https://')):
8178
+ scope_type = 'url'
8179
+ else:
8180
+ # Assume domain
8181
+ scope_type = 'domain'
8182
+
8183
+ try:
8184
+ scope_mgr.add_scope(ws_id, scope_type, scope_value, description="Added during engagement creation")
8185
+ click.echo(click.style(f" ✓ Added scope: {scope_type}={scope_value}", fg='green'))
8186
+ except Exception as scope_err:
8187
+ click.echo(click.style(f" ⚠ Could not save scope: {scope_err}", fg='yellow'))
8188
+
7611
8189
  click.echo()
7612
8190
 
7613
8191
  # Show next steps based on preset
@@ -8385,19 +8963,28 @@ def _select_siem_type(engagement_id: int):
8385
8963
  if new_type == current_type:
8386
8964
  click.echo(f"\n {info['name']} is already selected.")
8387
8965
  else:
8388
- # If switching SIEM types, we need to reconfigure
8389
- click.echo(f"\n Switching to {click.style(info['name'], bold=True)}...")
8390
- click.echo(" You'll need to configure the connection settings.")
8391
-
8392
- # Save minimal config to set the SIEM type
8393
- WazuhConfig.save_siem_config(
8394
- engagement_id=engagement_id,
8395
- siem_type=new_type,
8396
- config={'siem_type': new_type},
8397
- enabled=False # Not enabled until configured
8398
- )
8399
- click.echo(click.style(f"\n SIEM type set to {info['name']}", fg='green'))
8400
- click.echo(" Use 'Configure Connection' to set up credentials.")
8966
+ # Check if this SIEM type already has a config
8967
+ existing_config = WazuhConfig.get_config(engagement_id, new_type)
8968
+
8969
+ if existing_config and existing_config.get('enabled'):
8970
+ # Already configured - just make it current by updating timestamp
8971
+ click.echo(f"\n Switching to {click.style(info['name'], bold=True)}...")
8972
+ WazuhConfig.set_current_siem(engagement_id, new_type)
8973
+ click.echo(click.style(f"\n ✓ Switched to {info['name']} (existing config restored)", fg='green'))
8974
+ else:
8975
+ # Not configured - create placeholder
8976
+ click.echo(f"\n Switching to {click.style(info['name'], bold=True)}...")
8977
+ click.echo(" You'll need to configure the connection settings.")
8978
+
8979
+ # Save minimal config to set the SIEM type
8980
+ WazuhConfig.save_siem_config(
8981
+ engagement_id=engagement_id,
8982
+ siem_type=new_type,
8983
+ config={'siem_type': new_type},
8984
+ enabled=False # Not enabled until configured
8985
+ )
8986
+ click.echo(click.style(f"\n ✓ SIEM type set to {info['name']}", fg='green'))
8987
+ click.echo(" Use 'Configure Connection' to set up credentials.")
8401
8988
  else:
8402
8989
  click.echo(click.style("\n ✗ Invalid choice", fg='red'))
8403
8990
  except ValueError:
@@ -30962,31 +31549,36 @@ def view_documentation_menu():
30962
31549
  'name': 'Keyboard Shortcuts',
30963
31550
  'file': None, # Special handler
30964
31551
  'description': 'Command Center and Dashboard shortcuts'
31552
+ },
31553
+ '8': {
31554
+ 'name': 'Scope Management Guide',
31555
+ 'file': 'docs/user-guide/scope-management.md',
31556
+ 'description': 'Target validation and boundary enforcement'
30965
31557
  }
30966
31558
  }
30967
31559
 
30968
31560
  # Add PRO-only documentation
30969
31561
  if is_pro:
30970
31562
  docs.update({
30971
- '8': {
31563
+ '9': {
30972
31564
  'name': 'MSF Integration Guide',
30973
31565
  'file': None, # Special handler
30974
31566
  'description': 'Metasploit Framework workflows',
30975
31567
  'pro': True
30976
31568
  },
30977
- '9': {
31569
+ '10': {
30978
31570
  'name': 'RBAC & User Management',
30979
31571
  'file': 'docs/user-guide/rbac.md',
30980
31572
  'description': 'Roles, permissions, and audit logging',
30981
31573
  'pro': True
30982
31574
  },
30983
- '10': {
31575
+ '11': {
30984
31576
  'name': 'SIEM Integration Guide',
30985
31577
  'file': 'docs/user-guide/siem-integration.md',
30986
31578
  'description': 'Splunk, Wazuh, Elastic, Sentinel setup',
30987
31579
  'pro': True
30988
31580
  },
30989
- '11': {
31581
+ '12': {
30990
31582
  'name': 'AI Integration Guide',
30991
31583
  'file': 'docs/user-guide/ai-integration.md',
30992
31584
  'description': 'AI providers and autonomous execution',
@@ -31055,7 +31647,7 @@ def view_documentation_menu():
31055
31647
  continue
31056
31648
 
31057
31649
  # Special handler for MSF Integration Guide
31058
- if choice == '8':
31650
+ if choice == '9':
31059
31651
  _show_msf_help()
31060
31652
  continue
31061
31653