souleyez 2.26.0__py3-none-any.whl → 2.28.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.
- souleyez/__init__.py +1 -1
- souleyez/core/tool_chaining.py +36 -12
- souleyez/docs/README.md +2 -2
- souleyez/docs/user-guide/configuration.md +1 -1
- souleyez/docs/user-guide/scope-management.md +683 -0
- souleyez/engine/background.py +655 -168
- souleyez/engine/result_handler.py +340 -11
- souleyez/engine/worker_manager.py +98 -2
- souleyez/main.py +222 -1
- souleyez/plugins/http_fingerprint.py +8 -2
- souleyez/plugins/nuclei.py +2 -1
- souleyez/plugins/searchsploit.py +21 -18
- souleyez/security/scope_validator.py +615 -0
- souleyez/storage/hosts.py +87 -2
- souleyez/storage/migrations/_026_add_engagement_scope.py +87 -0
- souleyez/ui/interactive.py +289 -5
- {souleyez-2.26.0.dist-info → souleyez-2.28.0.dist-info}/METADATA +9 -3
- {souleyez-2.26.0.dist-info → souleyez-2.28.0.dist-info}/RECORD +22 -19
- {souleyez-2.26.0.dist-info → souleyez-2.28.0.dist-info}/WHEEL +0 -0
- {souleyez-2.26.0.dist-info → souleyez-2.28.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.26.0.dist-info → souleyez-2.28.0.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.26.0.dist-info → souleyez-2.28.0.dist-info}/top_level.txt +0 -0
souleyez/main.py
CHANGED
|
@@ -173,7 +173,7 @@ def _check_privileged_tools():
|
|
|
173
173
|
|
|
174
174
|
|
|
175
175
|
@click.group()
|
|
176
|
-
@click.version_option(version='2.
|
|
176
|
+
@click.version_option(version='2.28.0')
|
|
177
177
|
def cli():
|
|
178
178
|
"""SoulEyez - AI-Powered Pentesting Platform by CyberSoul Security"""
|
|
179
179
|
from souleyez.log_config import init_logging
|
|
@@ -604,6 +604,227 @@ from souleyez.commands.audit import audit
|
|
|
604
604
|
cli.add_command(audit)
|
|
605
605
|
|
|
606
606
|
|
|
607
|
+
# ============================================================================
|
|
608
|
+
# SCOPE MANAGEMENT
|
|
609
|
+
# ============================================================================
|
|
610
|
+
|
|
611
|
+
@cli.group()
|
|
612
|
+
def scope():
|
|
613
|
+
"""Engagement scope management - define and enforce target boundaries."""
|
|
614
|
+
pass
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@scope.command("add")
|
|
618
|
+
@click.argument("engagement_name")
|
|
619
|
+
@click.option("--cidr", help="Add CIDR range (e.g., 192.168.1.0/24)")
|
|
620
|
+
@click.option("--domain", help="Add domain pattern (e.g., *.example.com)")
|
|
621
|
+
@click.option("--url", help="Add URL (e.g., https://app.example.com)")
|
|
622
|
+
@click.option("--hostname", help="Add specific hostname or IP")
|
|
623
|
+
@click.option("--exclude", is_flag=True, help="Add as exclusion (deny rule)")
|
|
624
|
+
@click.option("--description", "-d", default="", help="Description for this scope entry")
|
|
625
|
+
def scope_add(engagement_name, cidr, domain, url, hostname, exclude, description):
|
|
626
|
+
"""Add a scope entry to an engagement."""
|
|
627
|
+
from souleyez.security.scope_validator import ScopeManager
|
|
628
|
+
|
|
629
|
+
em = EngagementManager()
|
|
630
|
+
eng = em.get(engagement_name)
|
|
631
|
+
if not eng:
|
|
632
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
manager = ScopeManager()
|
|
636
|
+
|
|
637
|
+
# Determine scope type and value
|
|
638
|
+
if cidr:
|
|
639
|
+
scope_type, value = 'cidr', cidr
|
|
640
|
+
elif domain:
|
|
641
|
+
scope_type, value = 'domain', domain
|
|
642
|
+
elif url:
|
|
643
|
+
scope_type, value = 'url', url
|
|
644
|
+
elif hostname:
|
|
645
|
+
scope_type, value = 'hostname', hostname
|
|
646
|
+
else:
|
|
647
|
+
click.echo("Error: Must specify one of --cidr, --domain, --url, or --hostname", err=True)
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
try:
|
|
651
|
+
scope_id = manager.add_scope(
|
|
652
|
+
engagement_id=eng['id'],
|
|
653
|
+
scope_type=scope_type,
|
|
654
|
+
value=value,
|
|
655
|
+
is_excluded=exclude,
|
|
656
|
+
description=description
|
|
657
|
+
)
|
|
658
|
+
action = "exclusion" if exclude else "scope entry"
|
|
659
|
+
click.echo(f"Added {action}: {scope_type}={value} (id={scope_id})")
|
|
660
|
+
except ValueError as e:
|
|
661
|
+
click.echo(f"Error: {e}", err=True)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@scope.command("list")
|
|
665
|
+
@click.argument("engagement_name")
|
|
666
|
+
def scope_list(engagement_name):
|
|
667
|
+
"""List scope entries for an engagement."""
|
|
668
|
+
from souleyez.security.scope_validator import ScopeManager, ScopeValidator
|
|
669
|
+
|
|
670
|
+
em = EngagementManager()
|
|
671
|
+
eng = em.get(engagement_name)
|
|
672
|
+
if not eng:
|
|
673
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
674
|
+
return
|
|
675
|
+
|
|
676
|
+
manager = ScopeManager()
|
|
677
|
+
validator = ScopeValidator(eng['id'])
|
|
678
|
+
entries = manager.list_scope(eng['id'])
|
|
679
|
+
enforcement = validator.get_enforcement_mode()
|
|
680
|
+
|
|
681
|
+
click.echo(f"\nScope for '{engagement_name}' (enforcement: {enforcement})")
|
|
682
|
+
click.echo("=" * 70)
|
|
683
|
+
|
|
684
|
+
if not entries:
|
|
685
|
+
click.echo("No scope entries defined (all targets allowed)")
|
|
686
|
+
return
|
|
687
|
+
|
|
688
|
+
click.echo(f"{'ID':<5} {'Type':<10} {'Value':<35} {'Excluded':<10}")
|
|
689
|
+
click.echo("-" * 70)
|
|
690
|
+
|
|
691
|
+
for entry in entries:
|
|
692
|
+
excluded = "EXCLUDE" if entry.get('is_excluded') else ""
|
|
693
|
+
click.echo(f"{entry['id']:<5} {entry['scope_type']:<10} {entry['value']:<35} {excluded:<10}")
|
|
694
|
+
if entry.get('description'):
|
|
695
|
+
click.echo(f" {entry['description']}")
|
|
696
|
+
|
|
697
|
+
click.echo()
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@scope.command("remove")
|
|
701
|
+
@click.argument("engagement_name")
|
|
702
|
+
@click.argument("scope_id", type=int)
|
|
703
|
+
def scope_remove(engagement_name, scope_id):
|
|
704
|
+
"""Remove a scope entry by ID."""
|
|
705
|
+
from souleyez.security.scope_validator import ScopeManager
|
|
706
|
+
|
|
707
|
+
em = EngagementManager()
|
|
708
|
+
eng = em.get(engagement_name)
|
|
709
|
+
if not eng:
|
|
710
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
711
|
+
return
|
|
712
|
+
|
|
713
|
+
manager = ScopeManager()
|
|
714
|
+
if manager.remove_scope(scope_id):
|
|
715
|
+
click.echo(f"Removed scope entry {scope_id}")
|
|
716
|
+
else:
|
|
717
|
+
click.echo(f"Error: Failed to remove scope entry {scope_id}", err=True)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@scope.command("enforcement")
|
|
721
|
+
@click.argument("engagement_name")
|
|
722
|
+
@click.argument("mode", type=click.Choice(['off', 'warn', 'block']))
|
|
723
|
+
def scope_enforcement(engagement_name, mode):
|
|
724
|
+
"""Set enforcement mode for an engagement.
|
|
725
|
+
|
|
726
|
+
Modes:
|
|
727
|
+
off - No scope validation (default)
|
|
728
|
+
warn - Allow out-of-scope targets but log warning
|
|
729
|
+
block - Reject jobs targeting out-of-scope hosts
|
|
730
|
+
"""
|
|
731
|
+
from souleyez.security.scope_validator import ScopeManager
|
|
732
|
+
|
|
733
|
+
em = EngagementManager()
|
|
734
|
+
eng = em.get(engagement_name)
|
|
735
|
+
if not eng:
|
|
736
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
737
|
+
return
|
|
738
|
+
|
|
739
|
+
manager = ScopeManager()
|
|
740
|
+
if manager.set_enforcement(eng['id'], mode):
|
|
741
|
+
click.echo(f"Enforcement mode set to '{mode}' for '{engagement_name}'")
|
|
742
|
+
else:
|
|
743
|
+
click.echo("Error: Failed to set enforcement mode", err=True)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@scope.command("validate")
|
|
747
|
+
@click.argument("engagement_name")
|
|
748
|
+
@click.argument("target")
|
|
749
|
+
def scope_validate(engagement_name, target):
|
|
750
|
+
"""Test if a target is in scope."""
|
|
751
|
+
from souleyez.security.scope_validator import ScopeValidator
|
|
752
|
+
|
|
753
|
+
em = EngagementManager()
|
|
754
|
+
eng = em.get(engagement_name)
|
|
755
|
+
if not eng:
|
|
756
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
757
|
+
return
|
|
758
|
+
|
|
759
|
+
validator = ScopeValidator(eng['id'])
|
|
760
|
+
result = validator.validate_target(target)
|
|
761
|
+
|
|
762
|
+
if result.is_in_scope:
|
|
763
|
+
click.echo(f"IN SCOPE: {target}")
|
|
764
|
+
if result.matched_entry:
|
|
765
|
+
click.echo(f" Matched: {result.matched_entry.get('value')}")
|
|
766
|
+
else:
|
|
767
|
+
click.echo(f"OUT OF SCOPE: {target}")
|
|
768
|
+
click.echo(f" Reason: {result.reason}")
|
|
769
|
+
|
|
770
|
+
click.echo(f" Enforcement: {validator.get_enforcement_mode()}")
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
@scope.command("revalidate")
|
|
774
|
+
@click.argument("engagement_name")
|
|
775
|
+
def scope_revalidate(engagement_name):
|
|
776
|
+
"""Revalidate scope status for all hosts in an engagement."""
|
|
777
|
+
from souleyez.storage.hosts import HostManager
|
|
778
|
+
|
|
779
|
+
em = EngagementManager()
|
|
780
|
+
eng = em.get(engagement_name)
|
|
781
|
+
if not eng:
|
|
782
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
783
|
+
return
|
|
784
|
+
|
|
785
|
+
hm = HostManager()
|
|
786
|
+
result = hm.revalidate_scope_status(eng['id'])
|
|
787
|
+
|
|
788
|
+
click.echo(f"Revalidated hosts for '{engagement_name}':")
|
|
789
|
+
click.echo(f" Updated: {result['updated']}")
|
|
790
|
+
click.echo(f" In scope: {result['in_scope']}")
|
|
791
|
+
click.echo(f" Out of scope: {result['out_of_scope']}")
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
@scope.command("log")
|
|
795
|
+
@click.argument("engagement_name")
|
|
796
|
+
@click.option("--limit", "-n", default=50, help="Number of entries to show")
|
|
797
|
+
def scope_log(engagement_name, limit):
|
|
798
|
+
"""Show scope validation audit log."""
|
|
799
|
+
from souleyez.security.scope_validator import ScopeManager
|
|
800
|
+
|
|
801
|
+
em = EngagementManager()
|
|
802
|
+
eng = em.get(engagement_name)
|
|
803
|
+
if not eng:
|
|
804
|
+
click.echo(f"Error: Engagement '{engagement_name}' not found", err=True)
|
|
805
|
+
return
|
|
806
|
+
|
|
807
|
+
manager = ScopeManager()
|
|
808
|
+
log_entries = manager.get_validation_log(eng['id'], limit)
|
|
809
|
+
|
|
810
|
+
click.echo(f"\nScope validation log for '{engagement_name}' (last {limit})")
|
|
811
|
+
click.echo("=" * 80)
|
|
812
|
+
|
|
813
|
+
if not log_entries:
|
|
814
|
+
click.echo("No validation log entries")
|
|
815
|
+
return
|
|
816
|
+
|
|
817
|
+
click.echo(f"{'Time':<20} {'Target':<25} {'Result':<12} {'Action':<10}")
|
|
818
|
+
click.echo("-" * 80)
|
|
819
|
+
|
|
820
|
+
for entry in log_entries:
|
|
821
|
+
timestamp = entry.get('created_at', '')[:19] # Trim to datetime
|
|
822
|
+
target = entry.get('target', '')[:24]
|
|
823
|
+
result = entry.get('validation_result', '')
|
|
824
|
+
action = entry.get('action_taken', '')
|
|
825
|
+
click.echo(f"{timestamp:<20} {target:<25} {result:<12} {action:<10}")
|
|
826
|
+
|
|
827
|
+
|
|
607
828
|
@cli.group()
|
|
608
829
|
def jobs():
|
|
609
830
|
"""Background job management."""
|
|
@@ -326,6 +326,12 @@ class HttpFingerprintPlugin(PluginBase):
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
parsed = urlparse(url)
|
|
329
|
+
|
|
330
|
+
# Security: Only allow http/https schemes (B310 - prevent file:// or custom schemes)
|
|
331
|
+
if parsed.scheme not in ('http', 'https'):
|
|
332
|
+
result['error'] = f"Invalid URL scheme: {parsed.scheme}. Only http/https allowed."
|
|
333
|
+
return result
|
|
334
|
+
|
|
329
335
|
is_https = parsed.scheme == 'https'
|
|
330
336
|
|
|
331
337
|
# Create request with common browser headers
|
|
@@ -362,9 +368,9 @@ class HttpFingerprintPlugin(PluginBase):
|
|
|
362
368
|
except Exception:
|
|
363
369
|
pass # TLS info is optional
|
|
364
370
|
|
|
365
|
-
response = urllib.request.urlopen(req, timeout=timeout, context=ctx)
|
|
371
|
+
response = urllib.request.urlopen(req, timeout=timeout, context=ctx) # nosec B310 - scheme validated above
|
|
366
372
|
else:
|
|
367
|
-
response = urllib.request.urlopen(req, timeout=timeout)
|
|
373
|
+
response = urllib.request.urlopen(req, timeout=timeout) # nosec B310 - scheme validated above
|
|
368
374
|
|
|
369
375
|
result['status_code'] = response.getcode()
|
|
370
376
|
|
souleyez/plugins/nuclei.py
CHANGED
|
@@ -195,8 +195,9 @@ class NucleiPlugin(PluginBase):
|
|
|
195
195
|
import os
|
|
196
196
|
from pathlib import Path
|
|
197
197
|
|
|
198
|
-
# Check common template locations
|
|
198
|
+
# Check common template locations (nuclei v3 uses ~/.local/nuclei-templates)
|
|
199
199
|
template_paths = [
|
|
200
|
+
Path.home() / ".local" / "nuclei-templates", # nuclei v3 default
|
|
200
201
|
Path.home() / "nuclei-templates",
|
|
201
202
|
Path.home() / ".nuclei-templates",
|
|
202
203
|
Path("/usr/share/nuclei-templates"),
|
souleyez/plugins/searchsploit.py
CHANGED
|
@@ -126,27 +126,30 @@ class SearchSploitPlugin(PluginBase):
|
|
|
126
126
|
|
|
127
127
|
def build_command(self, target: str, args: List[str] = None, label: str = "", log_path: str = None):
|
|
128
128
|
"""Build command for background execution with PID tracking."""
|
|
129
|
-
# Validate target
|
|
130
|
-
if not target or target.lower() == 'none':
|
|
131
|
-
raise ValueError("SearchSploit requires a search term. Usage: souleyez run searchsploit 'Apache 2.4.49'")
|
|
132
|
-
|
|
133
129
|
args = args or []
|
|
134
|
-
|
|
130
|
+
|
|
135
131
|
# Replace <target> placeholder
|
|
136
132
|
args = [arg.replace("<target>", target) for arg in args]
|
|
137
|
-
|
|
138
|
-
# searchsploit syntax: searchsploit [--json] search_term
|
|
139
|
-
cmd = ["searchsploit"]
|
|
140
|
-
|
|
141
|
-
#
|
|
142
|
-
if
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
133
|
+
|
|
134
|
+
# searchsploit syntax: searchsploit [--json] search_term
|
|
135
|
+
cmd = ["searchsploit", "--json"]
|
|
136
|
+
|
|
137
|
+
# Determine search term: use args if they contain a non-flag search term,
|
|
138
|
+
# otherwise use target (but skip if target is a URL - not a valid search term)
|
|
139
|
+
non_flag_args = [a for a in args if not a.startswith('-')]
|
|
140
|
+
|
|
141
|
+
if non_flag_args:
|
|
142
|
+
# Args contain search term(s) - use those, ignore URL target
|
|
143
|
+
cmd.extend(non_flag_args)
|
|
144
|
+
elif target and not target.startswith(('http://', 'https://')):
|
|
145
|
+
# Target is not a URL - use it as search term
|
|
146
|
+
cmd.append(target)
|
|
147
|
+
else:
|
|
148
|
+
raise ValueError("SearchSploit requires a search term. Usage: souleyez run searchsploit 'Apache 2.4.49'")
|
|
149
|
+
|
|
150
|
+
# Add remaining flag args (excluding --json which is already added)
|
|
151
|
+
flag_args = [a for a in args if a.startswith('-') and a != '--json']
|
|
152
|
+
cmd.extend(flag_args)
|
|
150
153
|
|
|
151
154
|
return {
|
|
152
155
|
'cmd': cmd,
|