souleyez 2.32.0__py3-none-any.whl → 2.36.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 +2 -1
- souleyez/core/tool_chaining.py +53 -7
- 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 +2 -2
- souleyez/main.py +1 -1
- souleyez/ui/interactive.py +39 -9
- {souleyez-2.32.0.dist-info → souleyez-2.36.0.dist-info}/METADATA +3 -3
- {souleyez-2.32.0.dist-info → souleyez-2.36.0.dist-info}/RECORD +15 -14
- {souleyez-2.32.0.dist-info → souleyez-2.36.0.dist-info}/WHEEL +0 -0
- {souleyez-2.32.0.dist-info → souleyez-2.36.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.32.0.dist-info → souleyez-2.36.0.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.32.0.dist-info → souleyez-2.36.0.dist-info}/top_level.txt +0 -0
souleyez/__init__.py
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
__version__ = '2.
|
|
1
|
+
__version__ = '2.36.0'
|
|
2
|
+
|
souleyez/core/tool_chaining.py
CHANGED
|
@@ -3972,7 +3972,7 @@ class ToolChaining:
|
|
|
3972
3972
|
# This reduces noise and focuses on high-value targets
|
|
3973
3973
|
from souleyez.intelligence.sensitive_tables import is_sensitive_table, is_system_table
|
|
3974
3974
|
|
|
3975
|
-
MAX_TABLES_FOR_COLUMN_ENUM =
|
|
3975
|
+
MAX_TABLES_FOR_COLUMN_ENUM = 10 # Focused on sensitive tables only
|
|
3976
3976
|
tables_queued = 0
|
|
3977
3977
|
skipped_tables = 0
|
|
3978
3978
|
|
|
@@ -5770,6 +5770,17 @@ class ToolChaining:
|
|
|
5770
5770
|
|
|
5771
5771
|
status = existing_job.get('status')
|
|
5772
5772
|
|
|
5773
|
+
# === SQLMAP RULE-BASED DEDUP (check ALL completed jobs, not just recent) ===
|
|
5774
|
+
# For sqlmap: if same rule was already applied to this URL, skip it entirely
|
|
5775
|
+
# This prevents infinite loops where each sqlmap job re-triggers the same rules
|
|
5776
|
+
if cmd['tool'] == 'sqlmap' and status in ['done', 'queued', 'running']:
|
|
5777
|
+
cmd_rule_id = cmd.get('rule_id')
|
|
5778
|
+
existing_rule_id = existing_job.get('rule_id')
|
|
5779
|
+
if cmd_rule_id and existing_rule_id and cmd_rule_id == existing_rule_id:
|
|
5780
|
+
similar_exists = True
|
|
5781
|
+
print(f" ⏭️ Skipping sqlmap for {cmd_target}: rule #{cmd_rule_id} already applied (job #{existing_job['id']} {status})")
|
|
5782
|
+
break
|
|
5783
|
+
|
|
5773
5784
|
# Check if job is active (queued/running)
|
|
5774
5785
|
is_active = status in ['queued', 'running']
|
|
5775
5786
|
|
|
@@ -5784,11 +5795,28 @@ class ToolChaining:
|
|
|
5784
5795
|
current_time = datetime.now(finished_time.tzinfo) if finished_time.tzinfo else datetime.now()
|
|
5785
5796
|
time_delta = (current_time - finished_time).total_seconds()
|
|
5786
5797
|
|
|
5787
|
-
# Only block if
|
|
5798
|
+
# Only block if finished < 5 min ago AND (same args OR same rule_id for sqlmap)
|
|
5788
5799
|
if time_delta < DUPLICATE_WINDOW_SECONDS:
|
|
5789
5800
|
existing_args = existing_job.get('args', [])
|
|
5790
5801
|
cmd_args = cmd.get('args', [])
|
|
5791
|
-
|
|
5802
|
+
|
|
5803
|
+
# For sqlmap: also check rule_id (same rule = duplicate even with different parent)
|
|
5804
|
+
if cmd['tool'] == 'sqlmap':
|
|
5805
|
+
cmd_rule_id = cmd.get('rule_id')
|
|
5806
|
+
existing_rule_id = existing_job.get('rule_id')
|
|
5807
|
+
if cmd_rule_id and existing_rule_id and cmd_rule_id == existing_rule_id:
|
|
5808
|
+
is_recent_duplicate = True
|
|
5809
|
+
minutes_ago = int(time_delta // 60)
|
|
5810
|
+
seconds_ago = int(time_delta % 60)
|
|
5811
|
+
time_str = f"{minutes_ago}m {seconds_ago}s ago" if minutes_ago > 0 else f"{seconds_ago}s ago"
|
|
5812
|
+
print(f" ⏭️ Skipping sqlmap for {cmd_target}: rule #{cmd_rule_id} completed {time_str} (duplicate)")
|
|
5813
|
+
elif existing_args == cmd_args:
|
|
5814
|
+
is_recent_duplicate = True
|
|
5815
|
+
minutes_ago = int(time_delta // 60)
|
|
5816
|
+
seconds_ago = int(time_delta % 60)
|
|
5817
|
+
time_str = f"{minutes_ago}m {seconds_ago}s ago" if minutes_ago > 0 else f"{seconds_ago}s ago"
|
|
5818
|
+
print(f" ⏭️ Skipping sqlmap for {cmd_target}: job #{existing_job['id']} completed {time_str} (duplicate)")
|
|
5819
|
+
elif existing_args == cmd_args:
|
|
5792
5820
|
is_recent_duplicate = True
|
|
5793
5821
|
minutes_ago = int(time_delta // 60)
|
|
5794
5822
|
seconds_ago = int(time_delta % 60)
|
|
@@ -5810,16 +5838,34 @@ class ToolChaining:
|
|
|
5810
5838
|
print(f" ⏭️ Skipping {cmd['tool']} for {cmd_target}: similar job #{existing_job['id']} already exists ({existing_job['status']})")
|
|
5811
5839
|
break
|
|
5812
5840
|
|
|
5813
|
-
# For sqlmap
|
|
5814
|
-
elif cmd['tool']
|
|
5841
|
+
# For sqlmap: check by rule_id to prevent same rule firing twice on same URL
|
|
5842
|
+
elif cmd['tool'] == 'sqlmap':
|
|
5843
|
+
cmd_rule_id = cmd.get('rule_id')
|
|
5844
|
+
existing_rule_id = existing_job.get('rule_id')
|
|
5845
|
+
existing_args = existing_job.get('args', [])
|
|
5846
|
+
cmd_args = cmd.get('args', [])
|
|
5847
|
+
|
|
5848
|
+
# If both have rule_id and they match, it's a duplicate
|
|
5849
|
+
# (same rule already applied to this injection point)
|
|
5850
|
+
if cmd_rule_id and existing_rule_id and cmd_rule_id == existing_rule_id:
|
|
5851
|
+
similar_exists = True
|
|
5852
|
+
print(f" ⏭️ Skipping sqlmap for {cmd_target}: rule #{cmd_rule_id} already applied (job #{existing_job['id']} {existing_job['status']})")
|
|
5853
|
+
break
|
|
5854
|
+
# Also check if exact same args (covers manual/no-rule jobs)
|
|
5855
|
+
elif existing_args == cmd_args:
|
|
5856
|
+
similar_exists = True
|
|
5857
|
+
print(f" ⏭️ Skipping sqlmap for {cmd_target}: similar job #{existing_job['id']} already exists ({existing_job['status']})")
|
|
5858
|
+
break
|
|
5859
|
+
# If different rule_id and different args, allow it (different SQLMap phase)
|
|
5860
|
+
|
|
5861
|
+
# For gobuster: check if args match
|
|
5862
|
+
elif cmd['tool'] == 'gobuster':
|
|
5815
5863
|
existing_args = existing_job.get('args', [])
|
|
5816
5864
|
cmd_args = cmd.get('args', [])
|
|
5817
|
-
# Compare args - if they're the same, it's a duplicate
|
|
5818
5865
|
if existing_args == cmd_args:
|
|
5819
5866
|
similar_exists = True
|
|
5820
5867
|
print(f" ⏭️ Skipping {cmd['tool']} for {cmd_target}: similar job #{existing_job['id']} already exists ({existing_job['status']})")
|
|
5821
5868
|
break
|
|
5822
|
-
# If args are different, allow it (different SQLMap phase)
|
|
5823
5869
|
|
|
5824
5870
|
# For quick lookup tools (whois, dnsrecon), skip 5-min duplicate check
|
|
5825
5871
|
# They're fast and each theHarvester run should trigger them
|
souleyez/docs/README.md
CHANGED
|
@@ -30,6 +30,7 @@ from souleyez.integrations.siem.wazuh import WazuhSIEMClient
|
|
|
30
30
|
from souleyez.integrations.siem.splunk import SplunkSIEMClient
|
|
31
31
|
from souleyez.integrations.siem.elastic import ElasticSIEMClient
|
|
32
32
|
from souleyez.integrations.siem.sentinel import SentinelSIEMClient
|
|
33
|
+
from souleyez.integrations.siem.googlesecops import GoogleSecOpsSIEMClient
|
|
33
34
|
from souleyez.integrations.siem.factory import SIEMFactory
|
|
34
35
|
|
|
35
36
|
__all__ = [
|
|
@@ -45,4 +46,5 @@ __all__ = [
|
|
|
45
46
|
'SplunkSIEMClient',
|
|
46
47
|
'ElasticSIEMClient',
|
|
47
48
|
'SentinelSIEMClient',
|
|
49
|
+
'GoogleSecOpsSIEMClient',
|
|
48
50
|
]
|
|
@@ -11,7 +11,8 @@ from souleyez.integrations.siem.base import SIEMClient, SIEMConnectionStatus
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
# Registry of available SIEM types
|
|
14
|
-
|
|
14
|
+
# Ordered: Open Source first, then Commercial
|
|
15
|
+
SIEM_TYPES = ['wazuh', 'elastic', 'splunk', 'sentinel', 'google_secops']
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class SIEMFactory:
|
|
@@ -60,6 +61,10 @@ class SIEMFactory:
|
|
|
60
61
|
from souleyez.integrations.siem.sentinel import SentinelSIEMClient
|
|
61
62
|
return SentinelSIEMClient.from_config(config)
|
|
62
63
|
|
|
64
|
+
elif siem_type_lower == 'google_secops':
|
|
65
|
+
from souleyez.integrations.siem.googlesecops import GoogleSecOpsSIEMClient
|
|
66
|
+
return GoogleSecOpsSIEMClient.from_config(config)
|
|
67
|
+
|
|
63
68
|
else:
|
|
64
69
|
raise ValueError(
|
|
65
70
|
f"Unsupported SIEM type: {siem_type}. "
|
|
@@ -114,7 +119,7 @@ class SIEMFactory:
|
|
|
114
119
|
info_map = {
|
|
115
120
|
'wazuh': {
|
|
116
121
|
'name': 'Wazuh',
|
|
117
|
-
'description': 'Open
|
|
122
|
+
'description': '[Open Source] Security monitoring platform (OSSEC fork)',
|
|
118
123
|
'config_fields': [
|
|
119
124
|
{'name': 'api_url', 'label': 'Manager API URL', 'required': True,
|
|
120
125
|
'placeholder': 'https://wazuh.example.com:55000'},
|
|
@@ -130,7 +135,7 @@ class SIEMFactory:
|
|
|
130
135
|
},
|
|
131
136
|
'splunk': {
|
|
132
137
|
'name': 'Splunk',
|
|
133
|
-
'description': 'Enterprise SIEM and log management
|
|
138
|
+
'description': '[Commercial] Enterprise SIEM and log management',
|
|
134
139
|
'config_fields': [
|
|
135
140
|
{'name': 'api_url', 'label': 'REST API URL', 'required': True,
|
|
136
141
|
'placeholder': 'https://splunk.example.com:8089'},
|
|
@@ -144,7 +149,7 @@ class SIEMFactory:
|
|
|
144
149
|
},
|
|
145
150
|
'elastic': {
|
|
146
151
|
'name': 'Elastic Security',
|
|
147
|
-
'description': 'Elastic
|
|
152
|
+
'description': '[Open Source] Elastic Stack security solution (ELK SIEM)',
|
|
148
153
|
'config_fields': [
|
|
149
154
|
{'name': 'elasticsearch_url', 'label': 'Elasticsearch URL', 'required': True,
|
|
150
155
|
'placeholder': 'https://elastic.example.com:9200'},
|
|
@@ -159,7 +164,7 @@ class SIEMFactory:
|
|
|
159
164
|
},
|
|
160
165
|
'sentinel': {
|
|
161
166
|
'name': 'Microsoft Sentinel',
|
|
162
|
-
'description': 'Azure cloud-native SIEM',
|
|
167
|
+
'description': '[Commercial] Azure cloud-native SIEM',
|
|
163
168
|
'config_fields': [
|
|
164
169
|
{'name': 'tenant_id', 'label': 'Azure Tenant ID', 'required': True},
|
|
165
170
|
{'name': 'client_id', 'label': 'App Client ID', 'required': True},
|
|
@@ -170,6 +175,22 @@ class SIEMFactory:
|
|
|
170
175
|
{'name': 'workspace_id', 'label': 'Workspace ID (GUID)', 'required': True},
|
|
171
176
|
],
|
|
172
177
|
},
|
|
178
|
+
'google_secops': {
|
|
179
|
+
'name': 'Google SecOps',
|
|
180
|
+
'description': '[Commercial] Google Cloud security operations (Chronicle)',
|
|
181
|
+
'config_fields': [
|
|
182
|
+
{'name': 'customer_id', 'label': 'Chronicle Customer ID', 'required': True,
|
|
183
|
+
'placeholder': 'Your Chronicle customer ID'},
|
|
184
|
+
{'name': 'region', 'label': 'Chronicle Region', 'required': True,
|
|
185
|
+
'placeholder': 'us, europe, asia-southeast1'},
|
|
186
|
+
{'name': 'project_id', 'label': 'Google Cloud Project ID', 'required': False,
|
|
187
|
+
'placeholder': 'Optional if in service account JSON'},
|
|
188
|
+
{'name': 'credentials_json', 'label': 'Service Account JSON', 'required': True,
|
|
189
|
+
'secret': True, 'type': 'textarea',
|
|
190
|
+
'placeholder': 'Paste service account JSON key'},
|
|
191
|
+
{'name': 'verify_ssl', 'label': 'Verify SSL', 'required': False, 'type': 'boolean'},
|
|
192
|
+
],
|
|
193
|
+
},
|
|
173
194
|
}
|
|
174
195
|
|
|
175
196
|
return info_map.get(siem_type.lower(), {
|
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google SecOps (Chronicle) SIEM Client.
|
|
3
|
+
|
|
4
|
+
Implements the SIEMClient interface for Google SecOps (formerly Chronicle SIEM).
|
|
5
|
+
Uses Chronicle REST APIs for querying detections, events, and rules.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from typing import Dict, List, Optional, Any
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from souleyez.integrations.siem.base import (
|
|
17
|
+
SIEMClient,
|
|
18
|
+
SIEMAlert,
|
|
19
|
+
SIEMRule,
|
|
20
|
+
SIEMConnectionStatus,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GoogleSecOpsSIEMClient(SIEMClient):
|
|
25
|
+
"""Google SecOps (Chronicle) implementation of the SIEMClient interface.
|
|
26
|
+
|
|
27
|
+
Uses Chronicle APIs:
|
|
28
|
+
- Auth: OAuth 2.0 with service account JWT
|
|
29
|
+
- Search: POST /v1alpha/events:udmSearch
|
|
30
|
+
- Detections: GET /v1alpha/detections
|
|
31
|
+
- Rules: GET /v1alpha/rules
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Chronicle API regions
|
|
35
|
+
REGIONS = {
|
|
36
|
+
'us': 'https://backstory.googleapis.com',
|
|
37
|
+
'europe': 'https://europe-backstory.googleapis.com',
|
|
38
|
+
'asia-southeast1': 'https://asia-southeast1-backstory.googleapis.com',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
credentials_json: str,
|
|
44
|
+
customer_id: str,
|
|
45
|
+
region: str = 'us',
|
|
46
|
+
project_id: Optional[str] = None,
|
|
47
|
+
verify_ssl: bool = True,
|
|
48
|
+
):
|
|
49
|
+
"""Initialize Google SecOps client.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
credentials_json: Service account JSON key (as string)
|
|
53
|
+
customer_id: Chronicle customer ID
|
|
54
|
+
region: Chronicle region ('us', 'europe', 'asia-southeast1')
|
|
55
|
+
project_id: Google Cloud project ID (optional, extracted from creds if not provided)
|
|
56
|
+
verify_ssl: Verify SSL certificates
|
|
57
|
+
"""
|
|
58
|
+
self.customer_id = customer_id
|
|
59
|
+
self.region = region.lower()
|
|
60
|
+
self.verify_ssl = verify_ssl
|
|
61
|
+
self._access_token: Optional[str] = None
|
|
62
|
+
self._token_expiry: Optional[datetime] = None
|
|
63
|
+
|
|
64
|
+
# Parse service account credentials
|
|
65
|
+
try:
|
|
66
|
+
if isinstance(credentials_json, str):
|
|
67
|
+
self._credentials = json.loads(credentials_json)
|
|
68
|
+
else:
|
|
69
|
+
self._credentials = credentials_json
|
|
70
|
+
except json.JSONDecodeError as e:
|
|
71
|
+
raise ValueError(f"Invalid service account JSON: {e}")
|
|
72
|
+
|
|
73
|
+
self.project_id = project_id or self._credentials.get('project_id', '')
|
|
74
|
+
|
|
75
|
+
# Set API base URL
|
|
76
|
+
self.api_base = self.REGIONS.get(self.region, self.REGIONS['us'])
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_config(cls, config: Dict[str, Any]) -> 'GoogleSecOpsSIEMClient':
|
|
80
|
+
"""Create client from configuration dictionary.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
config: Dict with credentials_json, customer_id, region, etc.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
GoogleSecOpsSIEMClient instance
|
|
87
|
+
"""
|
|
88
|
+
return cls(
|
|
89
|
+
credentials_json=config.get('credentials_json', '{}'),
|
|
90
|
+
customer_id=config.get('customer_id', ''),
|
|
91
|
+
region=config.get('region', 'us'),
|
|
92
|
+
project_id=config.get('project_id'),
|
|
93
|
+
verify_ssl=config.get('verify_ssl', True),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def siem_type(self) -> str:
|
|
98
|
+
"""Return the SIEM type identifier."""
|
|
99
|
+
return 'google_secops'
|
|
100
|
+
|
|
101
|
+
def _create_jwt(self) -> str:
|
|
102
|
+
"""Create a signed JWT for service account authentication.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Signed JWT string
|
|
106
|
+
"""
|
|
107
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
108
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
109
|
+
from cryptography.hazmat.backends import default_backend
|
|
110
|
+
|
|
111
|
+
now = int(time.time())
|
|
112
|
+
expiry = now + 3600 # 1 hour
|
|
113
|
+
|
|
114
|
+
# JWT header
|
|
115
|
+
header = {
|
|
116
|
+
'alg': 'RS256',
|
|
117
|
+
'typ': 'JWT',
|
|
118
|
+
'kid': self._credentials.get('private_key_id', '')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# JWT claims
|
|
122
|
+
claims = {
|
|
123
|
+
'iss': self._credentials.get('client_email', ''),
|
|
124
|
+
'sub': self._credentials.get('client_email', ''),
|
|
125
|
+
'aud': 'https://oauth2.googleapis.com/token',
|
|
126
|
+
'iat': now,
|
|
127
|
+
'exp': expiry,
|
|
128
|
+
'scope': 'https://www.googleapis.com/auth/chronicle-backstory'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Encode header and claims
|
|
132
|
+
def b64_encode(data: dict) -> str:
|
|
133
|
+
return base64.urlsafe_b64encode(
|
|
134
|
+
json.dumps(data, separators=(',', ':')).encode()
|
|
135
|
+
).rstrip(b'=').decode()
|
|
136
|
+
|
|
137
|
+
header_b64 = b64_encode(header)
|
|
138
|
+
claims_b64 = b64_encode(claims)
|
|
139
|
+
message = f"{header_b64}.{claims_b64}".encode()
|
|
140
|
+
|
|
141
|
+
# Sign with private key
|
|
142
|
+
private_key_pem = self._credentials.get('private_key', '')
|
|
143
|
+
private_key = serialization.load_pem_private_key(
|
|
144
|
+
private_key_pem.encode(),
|
|
145
|
+
password=None,
|
|
146
|
+
backend=default_backend()
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
signature = private_key.sign(
|
|
150
|
+
message,
|
|
151
|
+
padding.PKCS1v15(),
|
|
152
|
+
hashes.SHA256()
|
|
153
|
+
)
|
|
154
|
+
signature_b64 = base64.urlsafe_b64encode(signature).rstrip(b'=').decode()
|
|
155
|
+
|
|
156
|
+
return f"{header_b64}.{claims_b64}.{signature_b64}"
|
|
157
|
+
|
|
158
|
+
def _get_access_token(self) -> str:
|
|
159
|
+
"""Get Google OAuth access token using service account.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Access token string
|
|
163
|
+
"""
|
|
164
|
+
# Check cached token
|
|
165
|
+
if self._access_token and self._token_expiry:
|
|
166
|
+
if datetime.now() < self._token_expiry:
|
|
167
|
+
return self._access_token
|
|
168
|
+
|
|
169
|
+
# Create signed JWT
|
|
170
|
+
jwt = self._create_jwt()
|
|
171
|
+
|
|
172
|
+
# Exchange JWT for access token
|
|
173
|
+
response = requests.post(
|
|
174
|
+
'https://oauth2.googleapis.com/token',
|
|
175
|
+
data={
|
|
176
|
+
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
177
|
+
'assertion': jwt,
|
|
178
|
+
},
|
|
179
|
+
timeout=30,
|
|
180
|
+
verify=self.verify_ssl
|
|
181
|
+
)
|
|
182
|
+
response.raise_for_status()
|
|
183
|
+
|
|
184
|
+
token_data = response.json()
|
|
185
|
+
self._access_token = token_data['access_token']
|
|
186
|
+
expires_in = token_data.get('expires_in', 3600)
|
|
187
|
+
self._token_expiry = datetime.now() + timedelta(seconds=expires_in - 60)
|
|
188
|
+
|
|
189
|
+
return self._access_token
|
|
190
|
+
|
|
191
|
+
def _request(
|
|
192
|
+
self,
|
|
193
|
+
method: str,
|
|
194
|
+
endpoint: str,
|
|
195
|
+
params: Optional[Dict] = None,
|
|
196
|
+
json_data: Optional[Dict] = None,
|
|
197
|
+
) -> requests.Response:
|
|
198
|
+
"""Make authenticated API request.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
method: HTTP method
|
|
202
|
+
endpoint: API endpoint (relative to api_base)
|
|
203
|
+
params: Query parameters
|
|
204
|
+
json_data: JSON request body
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Response object
|
|
208
|
+
"""
|
|
209
|
+
token = self._get_access_token()
|
|
210
|
+
url = f"{self.api_base}{endpoint}"
|
|
211
|
+
|
|
212
|
+
headers = {
|
|
213
|
+
'Authorization': f'Bearer {token}',
|
|
214
|
+
'Content-Type': 'application/json',
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
response = requests.request(
|
|
218
|
+
method=method,
|
|
219
|
+
url=url,
|
|
220
|
+
headers=headers,
|
|
221
|
+
params=params,
|
|
222
|
+
json=json_data,
|
|
223
|
+
verify=self.verify_ssl,
|
|
224
|
+
timeout=60
|
|
225
|
+
)
|
|
226
|
+
return response
|
|
227
|
+
|
|
228
|
+
def test_connection(self) -> SIEMConnectionStatus:
|
|
229
|
+
"""Test connection to Google SecOps.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
SIEMConnectionStatus with connection details
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
# Try to get access token first (validates credentials)
|
|
236
|
+
self._get_access_token()
|
|
237
|
+
|
|
238
|
+
# Query for a small time window to verify API access
|
|
239
|
+
response = self._request(
|
|
240
|
+
'GET',
|
|
241
|
+
'/v1alpha/detect/rules',
|
|
242
|
+
params={'page_size': 1}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if response.status_code == 200:
|
|
246
|
+
return SIEMConnectionStatus(
|
|
247
|
+
connected=True,
|
|
248
|
+
version='Chronicle API v1alpha',
|
|
249
|
+
siem_type='google_secops',
|
|
250
|
+
details={
|
|
251
|
+
'region': self.region,
|
|
252
|
+
'customer_id': self.customer_id,
|
|
253
|
+
'project_id': self.project_id,
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
elif response.status_code == 403:
|
|
257
|
+
return SIEMConnectionStatus(
|
|
258
|
+
connected=False,
|
|
259
|
+
error='Permission denied. Check service account permissions.',
|
|
260
|
+
siem_type='google_secops'
|
|
261
|
+
)
|
|
262
|
+
else:
|
|
263
|
+
return SIEMConnectionStatus(
|
|
264
|
+
connected=False,
|
|
265
|
+
error=f'API error: {response.status_code} - {response.text[:200]}',
|
|
266
|
+
siem_type='google_secops'
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
except requests.exceptions.ConnectionError as e:
|
|
270
|
+
return SIEMConnectionStatus(
|
|
271
|
+
connected=False,
|
|
272
|
+
error=f'Connection failed: {str(e)}',
|
|
273
|
+
siem_type='google_secops'
|
|
274
|
+
)
|
|
275
|
+
except ValueError as e:
|
|
276
|
+
return SIEMConnectionStatus(
|
|
277
|
+
connected=False,
|
|
278
|
+
error=f'Configuration error: {str(e)}',
|
|
279
|
+
siem_type='google_secops'
|
|
280
|
+
)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
return SIEMConnectionStatus(
|
|
283
|
+
connected=False,
|
|
284
|
+
error=str(e),
|
|
285
|
+
siem_type='google_secops'
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def get_alerts(
|
|
289
|
+
self,
|
|
290
|
+
start_time: datetime,
|
|
291
|
+
end_time: datetime,
|
|
292
|
+
source_ip: Optional[str] = None,
|
|
293
|
+
dest_ip: Optional[str] = None,
|
|
294
|
+
rule_ids: Optional[List[str]] = None,
|
|
295
|
+
search_text: Optional[str] = None,
|
|
296
|
+
limit: int = 100
|
|
297
|
+
) -> List[SIEMAlert]:
|
|
298
|
+
"""Query detections/alerts from Google SecOps.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
start_time: Start of time range
|
|
302
|
+
end_time: End of time range
|
|
303
|
+
source_ip: Filter by source IP
|
|
304
|
+
dest_ip: Filter by destination IP
|
|
305
|
+
rule_ids: Filter by rule IDs
|
|
306
|
+
search_text: Free text search
|
|
307
|
+
limit: Maximum number of results
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
List of normalized SIEMAlert objects
|
|
311
|
+
"""
|
|
312
|
+
# Format times for Chronicle API (RFC 3339)
|
|
313
|
+
start_str = start_time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
314
|
+
end_str = end_time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
315
|
+
|
|
316
|
+
# Query detections endpoint
|
|
317
|
+
params = {
|
|
318
|
+
'start_time': start_str,
|
|
319
|
+
'end_time': end_str,
|
|
320
|
+
'page_size': min(limit, 1000),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
response = self._request(
|
|
324
|
+
'GET',
|
|
325
|
+
'/v1alpha/detect/detections',
|
|
326
|
+
params=params
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if response.status_code != 200:
|
|
330
|
+
return []
|
|
331
|
+
|
|
332
|
+
data = response.json()
|
|
333
|
+
detections = data.get('detections', [])
|
|
334
|
+
|
|
335
|
+
# Filter and normalize results
|
|
336
|
+
alerts = []
|
|
337
|
+
for detection in detections:
|
|
338
|
+
alert = self._normalize_alert(detection)
|
|
339
|
+
|
|
340
|
+
# Apply filters
|
|
341
|
+
if source_ip and alert.source_ip != source_ip:
|
|
342
|
+
continue
|
|
343
|
+
if dest_ip and alert.dest_ip != dest_ip:
|
|
344
|
+
continue
|
|
345
|
+
if rule_ids and alert.rule_id not in rule_ids:
|
|
346
|
+
continue
|
|
347
|
+
if search_text:
|
|
348
|
+
search_lower = search_text.lower()
|
|
349
|
+
if (search_lower not in alert.rule_name.lower() and
|
|
350
|
+
search_lower not in alert.description.lower()):
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
alerts.append(alert)
|
|
354
|
+
|
|
355
|
+
if len(alerts) >= limit:
|
|
356
|
+
break
|
|
357
|
+
|
|
358
|
+
return alerts
|
|
359
|
+
|
|
360
|
+
def _normalize_alert(self, detection: Dict[str, Any]) -> SIEMAlert:
|
|
361
|
+
"""Convert Chronicle detection to normalized SIEMAlert.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
detection: Raw detection from Chronicle API
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Normalized SIEMAlert
|
|
368
|
+
"""
|
|
369
|
+
# Parse timestamp
|
|
370
|
+
timestamp_str = detection.get('detectionTime', '')
|
|
371
|
+
try:
|
|
372
|
+
timestamp = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
|
|
373
|
+
except (ValueError, AttributeError):
|
|
374
|
+
timestamp = datetime.now()
|
|
375
|
+
|
|
376
|
+
# Extract rule info
|
|
377
|
+
rule_info = detection.get('detection', [{}])[0] if detection.get('detection') else {}
|
|
378
|
+
rule_id = rule_info.get('ruleId', detection.get('ruleId', ''))
|
|
379
|
+
rule_name = rule_info.get('ruleName', detection.get('ruleName', rule_id))
|
|
380
|
+
|
|
381
|
+
# Map severity
|
|
382
|
+
severity_raw = detection.get('severity', rule_info.get('severity', 'INFORMATIONAL'))
|
|
383
|
+
severity = self._map_severity(severity_raw)
|
|
384
|
+
|
|
385
|
+
# Extract IPs from UDM events
|
|
386
|
+
source_ip = None
|
|
387
|
+
dest_ip = None
|
|
388
|
+
events = detection.get('collectionElements', [])
|
|
389
|
+
for element in events:
|
|
390
|
+
references = element.get('references', [])
|
|
391
|
+
for ref in references:
|
|
392
|
+
event = ref.get('event', {})
|
|
393
|
+
principal = event.get('principal', {})
|
|
394
|
+
target = event.get('target', {})
|
|
395
|
+
|
|
396
|
+
if not source_ip and principal.get('ip'):
|
|
397
|
+
ips = principal.get('ip', [])
|
|
398
|
+
source_ip = ips[0] if ips else None
|
|
399
|
+
|
|
400
|
+
if not dest_ip and target.get('ip'):
|
|
401
|
+
ips = target.get('ip', [])
|
|
402
|
+
dest_ip = ips[0] if ips else None
|
|
403
|
+
|
|
404
|
+
# Extract description
|
|
405
|
+
description = detection.get('description', rule_info.get('ruleText', ''))
|
|
406
|
+
if not description:
|
|
407
|
+
description = f"Chronicle detection: {rule_name}"
|
|
408
|
+
|
|
409
|
+
return SIEMAlert(
|
|
410
|
+
id=detection.get('id', str(hash(str(detection)))[:12]),
|
|
411
|
+
timestamp=timestamp,
|
|
412
|
+
rule_id=str(rule_id),
|
|
413
|
+
rule_name=str(rule_name),
|
|
414
|
+
severity=severity,
|
|
415
|
+
source_ip=source_ip,
|
|
416
|
+
dest_ip=dest_ip,
|
|
417
|
+
description=str(description)[:200],
|
|
418
|
+
raw_data=detection,
|
|
419
|
+
mitre_tactics=[],
|
|
420
|
+
mitre_techniques=[],
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def _map_severity(self, severity: str) -> str:
|
|
424
|
+
"""Map Chronicle severity to normalized severity."""
|
|
425
|
+
severity_upper = str(severity).upper()
|
|
426
|
+
severity_map = {
|
|
427
|
+
'CRITICAL': 'critical',
|
|
428
|
+
'HIGH': 'high',
|
|
429
|
+
'MEDIUM': 'medium',
|
|
430
|
+
'LOW': 'low',
|
|
431
|
+
'INFORMATIONAL': 'info',
|
|
432
|
+
'INFO': 'info',
|
|
433
|
+
}
|
|
434
|
+
return severity_map.get(severity_upper, 'info')
|
|
435
|
+
|
|
436
|
+
def get_rules(
|
|
437
|
+
self,
|
|
438
|
+
rule_ids: Optional[List[str]] = None,
|
|
439
|
+
enabled_only: bool = True
|
|
440
|
+
) -> List[SIEMRule]:
|
|
441
|
+
"""Get YARA-L detection rules from Google SecOps.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
rule_ids: Optional list of specific rule IDs
|
|
445
|
+
enabled_only: Only return enabled rules
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
List of normalized SIEMRule objects
|
|
449
|
+
"""
|
|
450
|
+
response = self._request(
|
|
451
|
+
'GET',
|
|
452
|
+
'/v1alpha/detect/rules',
|
|
453
|
+
params={'page_size': 1000}
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
if response.status_code != 200:
|
|
457
|
+
return []
|
|
458
|
+
|
|
459
|
+
data = response.json()
|
|
460
|
+
raw_rules = data.get('rules', [])
|
|
461
|
+
|
|
462
|
+
rules = []
|
|
463
|
+
for raw_rule in raw_rules:
|
|
464
|
+
rule_id = raw_rule.get('ruleId', '')
|
|
465
|
+
|
|
466
|
+
# Filter by rule_ids if provided
|
|
467
|
+
if rule_ids and rule_id not in rule_ids:
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
# Check if enabled
|
|
471
|
+
is_enabled = raw_rule.get('liveRuleEnabled', True)
|
|
472
|
+
if enabled_only and not is_enabled:
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
rule = SIEMRule(
|
|
476
|
+
id=rule_id,
|
|
477
|
+
name=raw_rule.get('ruleName', rule_id),
|
|
478
|
+
description=raw_rule.get('metadata', {}).get('description', ''),
|
|
479
|
+
severity=self._map_severity(raw_rule.get('metadata', {}).get('severity', '')),
|
|
480
|
+
enabled=is_enabled,
|
|
481
|
+
mitre_tactics=raw_rule.get('metadata', {}).get('mitreTactics', []),
|
|
482
|
+
mitre_techniques=raw_rule.get('metadata', {}).get('mitreTechniques', []),
|
|
483
|
+
raw_data=raw_rule,
|
|
484
|
+
)
|
|
485
|
+
rules.append(rule)
|
|
486
|
+
|
|
487
|
+
return rules
|
|
488
|
+
|
|
489
|
+
def get_recommended_rules(self, attack_type: str) -> List[Dict[str, Any]]:
|
|
490
|
+
"""Get recommended rules for detecting an attack type.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
attack_type: Tool/attack name (e.g., 'nmap', 'hydra')
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
List of rule recommendations
|
|
497
|
+
"""
|
|
498
|
+
# Chronicle/Google SecOps rule recommendations
|
|
499
|
+
recommendations_map = {
|
|
500
|
+
'nmap': [
|
|
501
|
+
{
|
|
502
|
+
'rule_id': 'network_port_scan',
|
|
503
|
+
'rule_name': 'Network Port Scan Detection',
|
|
504
|
+
'yaral': '''
|
|
505
|
+
rule network_port_scan {
|
|
506
|
+
meta:
|
|
507
|
+
description = "Detects potential port scanning activity"
|
|
508
|
+
severity = "MEDIUM"
|
|
509
|
+
events:
|
|
510
|
+
$e.metadata.event_type = "NETWORK_CONNECTION"
|
|
511
|
+
$e.principal.ip = $src_ip
|
|
512
|
+
match:
|
|
513
|
+
$src_ip over 5m
|
|
514
|
+
condition:
|
|
515
|
+
#e > 100
|
|
516
|
+
}''',
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
'hydra': [
|
|
520
|
+
{
|
|
521
|
+
'rule_id': 'brute_force_auth',
|
|
522
|
+
'rule_name': 'Brute Force Authentication',
|
|
523
|
+
'yaral': '''
|
|
524
|
+
rule brute_force_authentication {
|
|
525
|
+
meta:
|
|
526
|
+
description = "Detects brute force login attempts"
|
|
527
|
+
severity = "HIGH"
|
|
528
|
+
events:
|
|
529
|
+
$e.metadata.event_type = "USER_LOGIN"
|
|
530
|
+
$e.security_result.action = "BLOCK"
|
|
531
|
+
$e.principal.ip = $src_ip
|
|
532
|
+
match:
|
|
533
|
+
$src_ip over 5m
|
|
534
|
+
condition:
|
|
535
|
+
#e > 10
|
|
536
|
+
}''',
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
'sqlmap': [
|
|
540
|
+
{
|
|
541
|
+
'rule_id': 'sql_injection',
|
|
542
|
+
'rule_name': 'SQL Injection Attempt',
|
|
543
|
+
'yaral': '''
|
|
544
|
+
rule sql_injection_attempt {
|
|
545
|
+
meta:
|
|
546
|
+
description = "Detects SQL injection patterns in requests"
|
|
547
|
+
severity = "HIGH"
|
|
548
|
+
events:
|
|
549
|
+
$e.metadata.event_type = "NETWORK_HTTP"
|
|
550
|
+
re.regex($e.target.url, `(?i)(union|select|insert|update|delete|drop).*`)
|
|
551
|
+
condition:
|
|
552
|
+
$e
|
|
553
|
+
}''',
|
|
554
|
+
},
|
|
555
|
+
],
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
attack_lower = attack_type.lower()
|
|
559
|
+
recommendations = recommendations_map.get(attack_lower, [])
|
|
560
|
+
|
|
561
|
+
return [
|
|
562
|
+
{
|
|
563
|
+
'rule_id': r['rule_id'],
|
|
564
|
+
'rule_name': r['rule_name'],
|
|
565
|
+
'description': f"YARA-L rule for detecting {attack_type}",
|
|
566
|
+
'severity': 'high',
|
|
567
|
+
'enabled': False, # These are recommendations, not deployed
|
|
568
|
+
'siem_type': 'google_secops',
|
|
569
|
+
'yaral_rule': r.get('yaral', ''),
|
|
570
|
+
}
|
|
571
|
+
for r in recommendations
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
def search_udm_events(
|
|
575
|
+
self,
|
|
576
|
+
query: str,
|
|
577
|
+
start_time: datetime,
|
|
578
|
+
end_time: datetime,
|
|
579
|
+
limit: int = 100
|
|
580
|
+
) -> List[Dict[str, Any]]:
|
|
581
|
+
"""Search UDM events with a custom query.
|
|
582
|
+
|
|
583
|
+
This is a Chronicle-specific method for advanced queries.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
query: UDM search query
|
|
587
|
+
start_time: Start of time range
|
|
588
|
+
end_time: End of time range
|
|
589
|
+
limit: Maximum results
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
List of UDM events
|
|
593
|
+
"""
|
|
594
|
+
start_str = start_time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
595
|
+
end_str = end_time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
596
|
+
|
|
597
|
+
response = self._request(
|
|
598
|
+
'POST',
|
|
599
|
+
'/v1alpha/events:udmSearch',
|
|
600
|
+
json_data={
|
|
601
|
+
'query': query,
|
|
602
|
+
'time_range': {
|
|
603
|
+
'start_time': start_str,
|
|
604
|
+
'end_time': end_str,
|
|
605
|
+
},
|
|
606
|
+
'limit': limit,
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
if response.status_code != 200:
|
|
611
|
+
return []
|
|
612
|
+
|
|
613
|
+
data = response.json()
|
|
614
|
+
return data.get('events', {}).get('events', [])
|
|
@@ -10,8 +10,8 @@ from pathlib import Path
|
|
|
10
10
|
from souleyez.storage.database import get_db
|
|
11
11
|
from souleyez.storage.crypto import get_crypto_manager
|
|
12
12
|
|
|
13
|
-
# Supported SIEM types
|
|
14
|
-
SIEM_TYPES = ['wazuh', '
|
|
13
|
+
# Supported SIEM types (Open Source first, then Commercial)
|
|
14
|
+
SIEM_TYPES = ['wazuh', 'elastic', 'splunk', 'sentinel', 'google_secops']
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class WazuhConfig:
|
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.36.0')
|
|
177
177
|
def cli():
|
|
178
178
|
"""SoulEyez - AI-Powered Pentesting Platform by CyberSoul Security"""
|
|
179
179
|
from souleyez.log_config import init_logging
|
souleyez/ui/interactive.py
CHANGED
|
@@ -8930,18 +8930,48 @@ def _select_siem_type(engagement_id: int):
|
|
|
8930
8930
|
config = WazuhConfig.get_config(engagement_id)
|
|
8931
8931
|
current_type = config.get('siem_type', 'wazuh') if config else 'wazuh'
|
|
8932
8932
|
|
|
8933
|
-
#
|
|
8934
|
-
|
|
8935
|
-
|
|
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
|
|
8936
8960
|
click.echo()
|
|
8937
8961
|
|
|
8938
|
-
|
|
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:
|
|
8939
8966
|
info = SIEMFactory.get_type_info(siem_type)
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
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()
|
|
8945
8975
|
|
|
8946
8976
|
click.echo(" [q] Cancel")
|
|
8947
8977
|
click.echo()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: souleyez
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.36.0
|
|
4
4
|
Summary: AI-Powered Penetration Testing Platform with 40+ integrated tools
|
|
5
5
|
Author-email: CyberSoul Security <contact@cybersoulsecurity.com>
|
|
6
6
|
Maintainer-email: CyberSoul Security <contact@cybersoulsecurity.com>
|
|
@@ -78,7 +78,7 @@ Welcome to the SoulEyez beta! Thank you for helping us test and improve this pen
|
|
|
78
78
|
|
|
79
79
|
> ⚠️ **Important**: Only use SoulEyez on systems you have explicit authorization to test.
|
|
80
80
|
|
|
81
|
-
## Version: 2.
|
|
81
|
+
## Version: 2.36.0
|
|
82
82
|
|
|
83
83
|
### What's Included
|
|
84
84
|
|
|
@@ -316,4 +316,4 @@ Happy hacking! 🛡️
|
|
|
316
316
|
|
|
317
317
|
---
|
|
318
318
|
|
|
319
|
-
**Version**: 2.
|
|
319
|
+
**Version**: 2.36.0 | **Release Date**: January 2026 | **Maintainer**: CyberSoul Security
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
souleyez/__init__.py,sha256=
|
|
1
|
+
souleyez/__init__.py,sha256=HEBfUyspLrKCZucYurMXlYQAAw-Suz7LX59o47d1D6Q,24
|
|
2
2
|
souleyez/config.py,sha256=av357I3GYRWAklv8Dto-9-5Db699Wq5znez7zo7241Q,11595
|
|
3
3
|
souleyez/devtools.py,sha256=rptmUY4a5eVvYjdEc6273MSagL-D9xibPOFgohVqUno,3508
|
|
4
4
|
souleyez/feature_flags.py,sha256=mo6YAq07lc6sR3lEFKmIwTKxXZ2JPxwa5X97uR_mu50,4642
|
|
5
5
|
souleyez/history.py,sha256=gzs5I_j-3OigIP6yfmBChdqxaFmyUIxvTpzWUPe_Q6c,2853
|
|
6
6
|
souleyez/log_config.py,sha256=MMhPAJOqgXDfuE-xm5g0RxAfWndcmbhFHvIEMm1a_Wo,5830
|
|
7
|
-
souleyez/main.py,sha256=
|
|
7
|
+
souleyez/main.py,sha256=MhSJMRbHN0BMNFdAGq7-pgHEND96ua118mlbnv_AsJs,130228
|
|
8
8
|
souleyez/scanner.py,sha256=U3IWHRrJ5aQ32dSHiVAHB60w1R_z0E0QxfM99msYNlw,3124
|
|
9
9
|
souleyez/security.py,sha256=S84m1QmnKz_6NgH2I6IBIAorMHxRPNYVFSnks5xjihQ,2479
|
|
10
10
|
souleyez/ui.py,sha256=15pfsqoDPnojAqr5S0TZHJE2ZkSHzkHpNVfVvsRj66A,34301
|
|
@@ -59,7 +59,7 @@ souleyez/core/network_utils.py,sha256=-4WgUE91RBzyXDFgGTxMa0zsWowJ47cEOAKXNeVa-W
|
|
|
59
59
|
souleyez/core/parser_handler.py,sha256=cyZtEDctqMdWgubsU0Jg6o4XqBgyfaJ_AeBHQmmv4hM,5564
|
|
60
60
|
souleyez/core/pending_chains.py,sha256=Dnka7JK7A8gTWCGpTu6qrIgIDIXprkZmwJ0Rm2oWqRE,10972
|
|
61
61
|
souleyez/core/templates.py,sha256=DzlXlAz8_lwAFjjUWPp3r81KCCzbNeK-bkN1IlgQBSU,18112
|
|
62
|
-
souleyez/core/tool_chaining.py,sha256=
|
|
62
|
+
souleyez/core/tool_chaining.py,sha256=x1cJ6icuWu5YBZJ31q_Jm2iahGC3mBoUCnfYrkT0Kcc,281598
|
|
63
63
|
souleyez/core/version_utils.py,sha256=UOrOa3qfUdLKdzWT6GAGNV9TauwinXyLyelS8sOk0eE,11769
|
|
64
64
|
souleyez/core/vuln_correlation.py,sha256=U69MSI5I-AtiyOAbXohGDKMpEHRW9y4G_0M1ppRGX18,14765
|
|
65
65
|
souleyez/core/web_utils.py,sha256=f-Dqa6tH8ROnygn6-k7J1y8Qz2f1FmeJnPjPE0WRn34,4902
|
|
@@ -104,7 +104,7 @@ souleyez/detection/__init__.py,sha256=QIhvXjFdjrquQ6A0VQ7GZQkK_EXB59t8Dv9PKXhEUe
|
|
|
104
104
|
souleyez/detection/attack_signatures.py,sha256=akgWwiIkh6WYnghCuLhRV0y6FS0SQ0caGF8tZUc49oA,6965
|
|
105
105
|
souleyez/detection/mitre_mappings.py,sha256=xejE80YK-g8kKaeQoo-vBl8P3t8RTTItbfN0NaVZw6s,20558
|
|
106
106
|
souleyez/detection/validator.py,sha256=-AJ7QSJ3-6jFKLnPG_Rc34IXyF4JPyI82BFUgTA9zw0,15641
|
|
107
|
-
souleyez/docs/README.md,sha256=
|
|
107
|
+
souleyez/docs/README.md,sha256=m5TvWRRtfLwpPLWEF9xJ6pXGqO_l9FlQCSNgVCJOqJg,7183
|
|
108
108
|
souleyez/docs/api-reference/cli-commands.md,sha256=lTLFnILN3YRVdqCaag7WgsYXfDGglb1TuPexkxDsVdE,12917
|
|
109
109
|
souleyez/docs/api-reference/engagement-api.md,sha256=nd-EvQMtiJrobg2bzFEADp853HP1Uhb9dmgok0_-neE,11672
|
|
110
110
|
souleyez/docs/api-reference/integration-guide.md,sha256=c96uX79ukHyYotLa54wZ20Kx-EUZnrKegTeGkfLD-pw,16285
|
|
@@ -162,10 +162,11 @@ souleyez/importers/__init__.py,sha256=FNeqzNzKnaO7NUpoHt2IjXpA0g2eGl8YQwwfg2LhFE
|
|
|
162
162
|
souleyez/importers/msf_importer.py,sha256=pPeXKqIv5ML2mu2Ew_BFI3vJbqDuBc53OHE4GJmqd5A,11754
|
|
163
163
|
souleyez/importers/smart_importer.py,sha256=hem7Zhvurl8Lm_qY1qeOF-JigQWClXtdmKKlzavvHbo,14425
|
|
164
164
|
souleyez/integrations/__init__.py,sha256=AG15oe2CIfQIJCUCOnMifj-EeZdzCe0A764ZU2gKVCo,79
|
|
165
|
-
souleyez/integrations/siem/__init__.py,sha256=
|
|
165
|
+
souleyez/integrations/siem/__init__.py,sha256=6KWX7hgTvoOiD5upV7nmBph1e1WOyrMdtN_JfFRJ_pc,1368
|
|
166
166
|
souleyez/integrations/siem/base.py,sha256=JmbhRlHl7wsRyd3nOhCqDHFP4VwBv-VR7uIKWoohKDE,8144
|
|
167
167
|
souleyez/integrations/siem/elastic.py,sha256=LuxheashiOO-AJbtUEK_BZ1T8EUAr1KlZkOheWPPgrQ,14712
|
|
168
|
-
souleyez/integrations/siem/factory.py,sha256=
|
|
168
|
+
souleyez/integrations/siem/factory.py,sha256=8OBoVpVSjnQQ5oXjcq3iJUEK-ItnyCVkcZV8wHbWEug,9424
|
|
169
|
+
souleyez/integrations/siem/googlesecops.py,sha256=HY832skGOlpidH9KdvvSmvMXP-fxT5S90BD6gFwMKnU,19419
|
|
169
170
|
souleyez/integrations/siem/sentinel.py,sha256=8zxuMgqlfCeGu_gHPNjfmBfBR5PEmtMTbqliC9dnbcI,15789
|
|
170
171
|
souleyez/integrations/siem/splunk.py,sha256=Vi6RMSqjXCVhy6AmQ0kFs_OrP6jDez3Vis8ZYJtWIGA,28587
|
|
171
172
|
souleyez/integrations/siem/wazuh.py,sha256=vltBhkP5BkgJLmdmF6u_b2RgTvP7iE9sFDxOt9v71qM,10245
|
|
@@ -173,7 +174,7 @@ souleyez/integrations/siem/rule_mappings/__init__.py,sha256=PbCLigl6Zo8ClvxZ29yd
|
|
|
173
174
|
souleyez/integrations/siem/rule_mappings/wazuh_rules.py,sha256=tfduSFFVxUOgL19w4ZFmOC0JDlnewz3PwA94a_XFTmI,8307
|
|
174
175
|
souleyez/integrations/wazuh/__init__.py,sha256=d-zfG_enCStagLZNf3bZ57GujT64jsfI7zS3gJDui10,288
|
|
175
176
|
souleyez/integrations/wazuh/client.py,sha256=Gc42bYP-dh2sDJNLh2WYt6iK0OUMnE9WYkegaZ_qUwk,21824
|
|
176
|
-
souleyez/integrations/wazuh/config.py,sha256=
|
|
177
|
+
souleyez/integrations/wazuh/config.py,sha256=WGOkc53EKZFsriUgs8xK26g6UBGvoGnK1Gcc-cRxDYU,14920
|
|
177
178
|
souleyez/integrations/wazuh/host_mapper.py,sha256=e5wKLb3KbkWLOry8z-8WMD36hmRSWO6ZhB_-iyfgKjg,9047
|
|
178
179
|
souleyez/integrations/wazuh/sync.py,sha256=lrZ-9Fd1HKm2ZHPvj83MCFL9UhZqAowWEXTidXDizw0,12168
|
|
179
180
|
souleyez/intelligence/__init__.py,sha256=oLjyxmSk5VgLxYnkSM9PmrREpiGAVmnqrRBxRGf4cQo,423
|
|
@@ -346,7 +347,7 @@ souleyez/ui/export_view.py,sha256=0nQvVsKk7FU4uRzSfJ_qBZh_Lfn8hgGA2rbJ5bNg5-Y,65
|
|
|
346
347
|
souleyez/ui/gap_analysis_view.py,sha256=AytAOEBq010wwo9hne1TE-uJpY_xicjLrFANbvN3r3w,30727
|
|
347
348
|
souleyez/ui/help_system.py,sha256=nKGxLaMi-TKYs6xudTyw_tZqBb1cGFEuYYh6N-MAsJE,16648
|
|
348
349
|
souleyez/ui/intelligence_view.py,sha256=VeAQ-3mANRnLIVpRqocL3JV0HUmJtADdxDeC5lzQhE0,32168
|
|
349
|
-
souleyez/ui/interactive.py,sha256
|
|
350
|
+
souleyez/ui/interactive.py,sha256=dkyIys13Q6_OsDO84-sg1DNQvVTttVOBHjIZV2sSM7w,1406611
|
|
350
351
|
souleyez/ui/interactive_selector.py,sha256=6A51fgmFRnemBY0aCPHIhK2Rpba16NjSGKLzC0Q5vI8,16407
|
|
351
352
|
souleyez/ui/log_formatter.py,sha256=akhIkYoO_cCaKxS1V5N3iPmIrHzgsU7pmsedx70s9TI,3845
|
|
352
353
|
souleyez/ui/menu_components.py,sha256=N8zq2QXGmfaLJ08l53MMYt1y-5LRWgpZH6r8nXHonj8,3519
|
|
@@ -369,9 +370,9 @@ souleyez/ui/tutorial_state.py,sha256=Thf7_qCj4VKjG7UqgJqa9kjIqiFUU-7Q7kG4v-u2B4A
|
|
|
369
370
|
souleyez/ui/wazuh_vulns_view.py,sha256=3vJJEmrjgS2wD6EDB7ZV7WxgytBHTm-1WqNDjp7lVEI,21830
|
|
370
371
|
souleyez/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
371
372
|
souleyez/utils/tool_checker.py,sha256=kQcXJVY5NiO-orQAUnpHhpQvR5UOBNHJ0PaT0fBxYoQ,30782
|
|
372
|
-
souleyez-2.
|
|
373
|
-
souleyez-2.
|
|
374
|
-
souleyez-2.
|
|
375
|
-
souleyez-2.
|
|
376
|
-
souleyez-2.
|
|
377
|
-
souleyez-2.
|
|
373
|
+
souleyez-2.36.0.dist-info/licenses/LICENSE,sha256=J7vDD5QMF4w2oSDm35eBgosATE70ah1M40u9W4EpTZs,1090
|
|
374
|
+
souleyez-2.36.0.dist-info/METADATA,sha256=MUHVkcAp8S5N_wcVMGA_-fL5xYr2Uc5RN3EtRaxfSQw,11345
|
|
375
|
+
souleyez-2.36.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
376
|
+
souleyez-2.36.0.dist-info/entry_points.txt,sha256=bN5W1dhjDZJl3TKclMjRpfQvGPmyrJLwwDuCj_X39HE,48
|
|
377
|
+
souleyez-2.36.0.dist-info/top_level.txt,sha256=afAMzS9p4lcdBNxhGo6jl3ipQE9HUvvNIPOdjtPjr_Q,9
|
|
378
|
+
souleyez-2.36.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|