souleyez 2.28.0__py3-none-any.whl → 2.39.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))
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")
6361
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
@@ -7887,6 +8152,40 @@ def manage_engagements_menu():
7887
8152
  click.echo(click.style(f" ✓ Created engagement '{ws_name}' (ID: {ws_id})", fg='green'))
7888
8153
  if selected_template:
7889
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
+
7890
8189
  click.echo()
7891
8190
 
7892
8191
  # Show next steps based on preset
@@ -8631,18 +8930,48 @@ def _select_siem_type(engagement_id: int):
8631
8930
  config = WazuhConfig.get_config(engagement_id)
8632
8931
  current_type = config.get('siem_type', 'wazuh') if config else 'wazuh'
8633
8932
 
8634
- # Show available SIEM types
8635
- siem_types = SIEMFactory.get_available_types()
8636
- click.echo(" Available SIEM platforms:")
8933
+ # Define SIEM categories with emojis
8934
+ siem_emojis = {
8935
+ 'wazuh': '🦎',
8936
+ 'elastic': '🦌',
8937
+ 'splunk': '⚡',
8938
+ 'sentinel': '🛡️',
8939
+ 'google_secops': '🔍',
8940
+ }
8941
+ open_source_siems = ['wazuh', 'elastic']
8942
+ commercial_siems = ['splunk', 'sentinel', 'google_secops']
8943
+
8944
+ # Build ordered list for selection (open source first)
8945
+ siem_types = open_source_siems + commercial_siems
8946
+
8947
+ # Show Open Source section
8948
+ click.echo(" 🌐 " + click.style("OPEN SOURCE", fg='green', bold=True))
8949
+ click.echo(" " + "─" * 60)
8950
+ idx = 1
8951
+ for siem_type in open_source_siems:
8952
+ info = SIEMFactory.get_type_info(siem_type)
8953
+ emoji = siem_emojis.get(siem_type, '📊')
8954
+ current_marker = click.style(" (current)", fg='green') if siem_type == current_type else ""
8955
+ # Remove [Open Source] prefix from description since we have section header
8956
+ desc = info['description'].replace('[Open Source] ', '')
8957
+ click.echo(f" [{idx}] {emoji} {click.style(info['name'], bold=True)}{current_marker}")
8958
+ click.echo(f" {click.style(desc, dim=True)}")
8959
+ idx += 1
8637
8960
  click.echo()
8638
8961
 
8639
- for i, siem_type in enumerate(siem_types, 1):
8962
+ # Show Commercial section
8963
+ click.echo(" 💼 " + click.style("COMMERCIAL", fg='cyan', bold=True))
8964
+ click.echo(" " + "─" * 60)
8965
+ for siem_type in commercial_siems:
8640
8966
  info = SIEMFactory.get_type_info(siem_type)
8641
- is_current = " (current)" if siem_type == current_type else ""
8642
- marker = click.style("*", fg='green') if siem_type == current_type else " "
8643
- click.echo(f" {marker} [{i}] {click.style(info['name'], bold=True)}{is_current}")
8644
- click.echo(f" {info['description']}")
8645
- click.echo()
8967
+ emoji = siem_emojis.get(siem_type, '📊')
8968
+ current_marker = click.style(" (current)", fg='green') if siem_type == current_type else ""
8969
+ # Remove [Commercial] prefix from description since we have section header
8970
+ desc = info['description'].replace('[Commercial] ', '')
8971
+ click.echo(f" [{idx}] {emoji} {click.style(info['name'], bold=True)}{current_marker}")
8972
+ click.echo(f" {click.style(desc, dim=True)}")
8973
+ idx += 1
8974
+ click.echo()
8646
8975
 
8647
8976
  click.echo(" [q] Cancel")
8648
8977
  click.echo()
@@ -8664,19 +8993,28 @@ def _select_siem_type(engagement_id: int):
8664
8993
  if new_type == current_type:
8665
8994
  click.echo(f"\n {info['name']} is already selected.")
8666
8995
  else:
8667
- # If switching SIEM types, we need to reconfigure
8668
- click.echo(f"\n Switching to {click.style(info['name'], bold=True)}...")
8669
- click.echo(" You'll need to configure the connection settings.")
8670
-
8671
- # Save minimal config to set the SIEM type
8672
- WazuhConfig.save_siem_config(
8673
- engagement_id=engagement_id,
8674
- siem_type=new_type,
8675
- config={'siem_type': new_type},
8676
- enabled=False # Not enabled until configured
8677
- )
8678
- click.echo(click.style(f"\n SIEM type set to {info['name']}", fg='green'))
8679
- click.echo(" Use 'Configure Connection' to set up credentials.")
8996
+ # Check if this SIEM type already has a config
8997
+ existing_config = WazuhConfig.get_config(engagement_id, new_type)
8998
+
8999
+ if existing_config and existing_config.get('enabled'):
9000
+ # Already configured - just make it current by updating timestamp
9001
+ click.echo(f"\n Switching to {click.style(info['name'], bold=True)}...")
9002
+ WazuhConfig.set_current_siem(engagement_id, new_type)
9003
+ click.echo(click.style(f"\n ✓ Switched to {info['name']} (existing config restored)", fg='green'))
9004
+ else:
9005
+ # Not configured - create placeholder
9006
+ click.echo(f"\n Switching to {click.style(info['name'], bold=True)}...")
9007
+ click.echo(" You'll need to configure the connection settings.")
9008
+
9009
+ # Save minimal config to set the SIEM type
9010
+ WazuhConfig.save_siem_config(
9011
+ engagement_id=engagement_id,
9012
+ siem_type=new_type,
9013
+ config={'siem_type': new_type},
9014
+ enabled=False # Not enabled until configured
9015
+ )
9016
+ click.echo(click.style(f"\n ✓ SIEM type set to {info['name']}", fg='green'))
9017
+ click.echo(" Use 'Configure Connection' to set up credentials.")
8680
9018
  else:
8681
9019
  click.echo(click.style("\n ✗ Invalid choice", fg='red'))
8682
9020
  except ValueError: