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.
- souleyez/__init__.py +2 -1
- souleyez/core/msf_auto_mapper.py +3 -2
- souleyez/core/tool_chaining.py +77 -11
- souleyez/docs/README.md +1 -1
- souleyez/integrations/siem/__init__.py +2 -0
- souleyez/integrations/siem/factory.py +26 -5
- souleyez/integrations/siem/googlesecops.py +614 -0
- souleyez/integrations/wazuh/config.py +143 -20
- souleyez/main.py +7 -40
- souleyez/storage/database.py +59 -20
- souleyez/storage/migrations/_027_multi_siem_persistence.py +119 -0
- souleyez/storage/migrations/__init__.py +6 -0
- souleyez/storage/schema.sql +44 -4
- souleyez/ui/interactive.py +575 -237
- souleyez-2.39.0.dist-info/METADATA +265 -0
- {souleyez-2.28.0.dist-info → souleyez-2.39.0.dist-info}/RECORD +20 -18
- souleyez-2.28.0.dist-info/METADATA +0 -319
- {souleyez-2.28.0.dist-info → souleyez-2.39.0.dist-info}/WHEEL +0 -0
- {souleyez-2.28.0.dist-info → souleyez-2.39.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.28.0.dist-info → souleyez-2.39.0.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.28.0.dist-info → souleyez-2.39.0.dist-info}/top_level.txt +0 -0
souleyez/ui/interactive.py
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
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
|
|
5394
|
-
click.echo(f"Reason: Auto-chained from job #{
|
|
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')
|
|
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("
|
|
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
|
-
|
|
5949
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
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("
|
|
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
|
-
|
|
6237
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
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
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
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
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
click.echo(f"
|
|
6392
|
-
if
|
|
6393
|
-
click.echo(f"
|
|
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
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
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
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
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
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
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
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
6597
|
-
|
|
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
|
|
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("
|
|
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"
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
8635
|
-
|
|
8636
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
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
|
-
#
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
engagement_id
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
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:
|