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/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.26.0')
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
 
@@ -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"),
@@ -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 [args]
139
- cmd = ["searchsploit"]
140
-
141
- # Force JSON output for parsing
142
- if "--json" not in args:
143
- cmd.append("--json")
144
-
145
- # Add search term
146
- cmd.append(target)
147
-
148
- # Add user args (after target)
149
- cmd.extend(args)
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,