secploy 0.2.6__tar.gz → 0.2.7__tar.gz
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.
- {secploy-0.2.6/secploy.egg-info → secploy-0.2.7}/PKG-INFO +1 -1
- {secploy-0.2.6 → secploy-0.2.7}/secploy/__init__.py +1 -1
- {secploy-0.2.6 → secploy-0.2.7}/secploy/client.py +2 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy/log_capture.py +94 -56
- {secploy-0.2.6 → secploy-0.2.7}/secploy/schemas.py +3 -3
- {secploy-0.2.6 → secploy-0.2.7/secploy.egg-info}/PKG-INFO +1 -1
- {secploy-0.2.6 → secploy-0.2.7}/setup.py +1 -1
- {secploy-0.2.6 → secploy-0.2.7}/LICENSE +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/MANIFEST.in +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/README.md +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy/enums.py +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy/events.py +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy/lib/__init__.py +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy/lib/config.py +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy/lib/secploy_logger.py +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy/processor.py +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy/utils.py +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy.egg-info/SOURCES.txt +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy.egg-info/dependency_links.txt +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy.egg-info/entry_points.txt +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy.egg-info/requires.txt +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/secploy.egg-info/top_level.txt +0 -0
- {secploy-0.2.6 → secploy-0.2.7}/setup.cfg +0 -0
|
@@ -23,7 +23,7 @@ def cli():
|
|
|
23
23
|
|
|
24
24
|
parser = argparse.ArgumentParser(description=__description__)
|
|
25
25
|
parser.add_argument('--version', action='version', version=f'Secploy SDK v{__version__}')
|
|
26
|
-
parser.add_argument('--test-config', help='Test a configuration file', metavar='CONFIG_FILE')
|
|
26
|
+
parser.add_argument('--test-config', help='Test a configuration file: To verify if it is well configured', metavar='CONFIG_FILE')
|
|
27
27
|
|
|
28
28
|
args = parser.parse_args()
|
|
29
29
|
|
|
@@ -4,7 +4,10 @@ Secploy Log Capture Module
|
|
|
4
4
|
This module provides functionality for capturing and forwarding logs to Secploy's ingest endpoint.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
7
9
|
import logging
|
|
10
|
+
import sys
|
|
8
11
|
import threading
|
|
9
12
|
import traceback
|
|
10
13
|
from typing import Any, Dict, Optional, List, Union
|
|
@@ -17,7 +20,7 @@ from .schemas import LogEntry, Context, Tags
|
|
|
17
20
|
|
|
18
21
|
class SecployLogCapturer:
|
|
19
22
|
"""
|
|
20
|
-
Manages log capture and forwarding to Secploy ingest endpoint.
|
|
23
|
+
Manages log capture and exception forwarding to Secploy ingest endpoint.
|
|
21
24
|
"""
|
|
22
25
|
|
|
23
26
|
def __init__(self, client, levels: Optional[List[Union[str, int]]] = None):
|
|
@@ -30,58 +33,105 @@ class SecployLogCapturer:
|
|
|
30
33
|
self.client = client
|
|
31
34
|
self.levels = levels
|
|
32
35
|
self._handler = self._create_handler()
|
|
36
|
+
self._installed = False
|
|
33
37
|
|
|
34
38
|
def _create_handler(self) -> logging.Handler:
|
|
35
39
|
"""Create the custom logging handler."""
|
|
36
40
|
return SecployLogHandler(self.client, levels=self.levels)
|
|
37
41
|
|
|
38
42
|
def start_capture(self, loggers: Union[str, List[str], None] = None):
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
loggers: Logger name(s) to capture. Can be:
|
|
44
|
-
- None to capture the root logger
|
|
45
|
-
- A string for a single logger
|
|
46
|
-
- A list of logger names
|
|
47
|
-
"""
|
|
43
|
+
"""Start capturing logs and exceptions."""
|
|
44
|
+
if self._installed:
|
|
45
|
+
return
|
|
46
|
+
self._installed = True
|
|
48
47
|
|
|
49
48
|
if isinstance(loggers, str):
|
|
50
49
|
loggers = [loggers]
|
|
51
50
|
elif loggers is None:
|
|
52
|
-
loggers = ['']
|
|
53
|
-
|
|
51
|
+
loggers = ['']
|
|
52
|
+
|
|
54
53
|
for logger_name in loggers:
|
|
55
54
|
logger = logging.getLogger(logger_name)
|
|
56
55
|
logger.addHandler(self._handler)
|
|
56
|
+
|
|
57
|
+
sys.excepthook = self._capture_uncaught_exceptions
|
|
58
|
+
|
|
59
|
+
if hasattr(threading, "excepthook"):
|
|
60
|
+
threading.excepthook = self._thread_exception_handler
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
loop = asyncio.get_event_loop()
|
|
64
|
+
loop.set_exception_handler(self._async_exception_handler)
|
|
65
|
+
except RuntimeError:
|
|
66
|
+
pass
|
|
57
67
|
|
|
58
68
|
def stop_capture(self, loggers: Union[str, List[str], None] = None):
|
|
59
|
-
"""
|
|
60
|
-
Stop capturing logs from specified loggers.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
loggers: Logger name(s) to stop capturing. Same format as start_capture()
|
|
64
|
-
"""
|
|
65
69
|
if isinstance(loggers, str):
|
|
66
70
|
loggers = [loggers]
|
|
67
71
|
elif loggers is None:
|
|
68
|
-
loggers = ['']
|
|
69
|
-
|
|
72
|
+
loggers = ['']
|
|
73
|
+
|
|
70
74
|
for logger_name in loggers:
|
|
71
75
|
logger = logging.getLogger(logger_name)
|
|
72
76
|
if self._handler in logger.handlers:
|
|
73
77
|
logger.removeHandler(self._handler)
|
|
74
78
|
|
|
75
79
|
def stop_all(self):
|
|
76
|
-
"""Stop all log capturing."""
|
|
77
|
-
# Find all loggers that have our handler
|
|
80
|
+
"""Stop all log capturing and exception hooks."""
|
|
78
81
|
for logger in [logging.getLogger(name) for name in logging.root.manager.loggerDict]:
|
|
79
82
|
if self._handler in logger.handlers:
|
|
80
83
|
logger.removeHandler(self._handler)
|
|
81
|
-
# Check root logger too
|
|
82
84
|
if self._handler in logging.root.handlers:
|
|
83
85
|
logging.root.removeHandler(self._handler)
|
|
84
86
|
|
|
87
|
+
sys.excepthook = sys.__excepthook__
|
|
88
|
+
if hasattr(threading, "excepthook"):
|
|
89
|
+
threading.excepthook = threading.__excepthook__
|
|
90
|
+
try:
|
|
91
|
+
loop = asyncio.get_event_loop()
|
|
92
|
+
loop.set_exception_handler(None)
|
|
93
|
+
except RuntimeError:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
self._installed = False
|
|
97
|
+
|
|
98
|
+
def _capture_uncaught_exceptions(self, exc_type, exc_value, exc_tb):
|
|
99
|
+
self._send_exception(exc_type, exc_value, exc_tb, source="sys")
|
|
100
|
+
|
|
101
|
+
def _thread_exception_handler(self, args):
|
|
102
|
+
self._send_exception(args.exc_type, args.exc_value, args.exc_traceback, source="thread")
|
|
103
|
+
|
|
104
|
+
def _async_exception_handler(self, loop, context):
|
|
105
|
+
exc = context.get("exception")
|
|
106
|
+
if exc:
|
|
107
|
+
self._send_exception(type(exc), exc, exc.__traceback__, source="asyncio")
|
|
108
|
+
else:
|
|
109
|
+
self._send_exception(RuntimeError, RuntimeError(context["message"]), None, source="asyncio")
|
|
110
|
+
|
|
111
|
+
def _send_exception(self, exc_type, exc_value, exc_tb, source="system"):
|
|
112
|
+
stacktrace = traceback.format_exception(exc_type, exc_value, exc_tb)
|
|
113
|
+
|
|
114
|
+
log_entry = LogEntry(
|
|
115
|
+
timestamp=datetime.now().timestamp(),
|
|
116
|
+
type="error",
|
|
117
|
+
message=str(exc_value),
|
|
118
|
+
context=Context(
|
|
119
|
+
user_id="unknown",
|
|
120
|
+
session_id="global",
|
|
121
|
+
http_method="NONE",
|
|
122
|
+
http_url="",
|
|
123
|
+
http_status=500,
|
|
124
|
+
stacktrace=stacktrace,
|
|
125
|
+
tags=Tags(
|
|
126
|
+
environment=self.client.environment,
|
|
127
|
+
service=f"exception.{source}",
|
|
128
|
+
region="unknown"
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
payload = log_entry.model_dump(mode="json")
|
|
133
|
+
self.client._event_queue.put(("log", payload))
|
|
134
|
+
|
|
85
135
|
|
|
86
136
|
class SecployLogHandler(logging.Handler):
|
|
87
137
|
"""
|
|
@@ -94,45 +144,34 @@ class SecployLogHandler(logging.Handler):
|
|
|
94
144
|
self._local = threading.local()
|
|
95
145
|
self._event_handler = EventHandler(self.client._event_queue)
|
|
96
146
|
|
|
97
|
-
if levels:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
else:
|
|
103
|
-
|
|
147
|
+
# if levels:
|
|
148
|
+
# self.levels = {
|
|
149
|
+
# lvl if isinstance(lvl, int) else logging._nameToLevel[lvl.upper()]
|
|
150
|
+
# for lvl in levels
|
|
151
|
+
# }
|
|
152
|
+
# else:
|
|
153
|
+
# self.levels = None # None = capture all levels
|
|
104
154
|
|
|
105
|
-
def filter(self, record: logging.LogRecord) -> bool:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
155
|
+
# def filter(self, record: logging.LogRecord) -> bool:
|
|
156
|
+
# """Return True if this record should be handled."""
|
|
157
|
+
# if self.levels is None:
|
|
158
|
+
# return True
|
|
159
|
+
# return record.levelno in self.levels
|
|
110
160
|
|
|
111
161
|
def get_thread_id(self) -> str:
|
|
112
162
|
"""Get the current thread identifier."""
|
|
113
163
|
return str(threading.get_ident())
|
|
114
164
|
|
|
115
|
-
def format_exc_info(self, exc_info) -> Optional[Dict[str, Any]]:
|
|
116
|
-
"""Format exception information if available."""
|
|
117
|
-
if not exc_info:
|
|
118
|
-
return None
|
|
119
|
-
|
|
120
|
-
exc_type, exc_value, exc_tb = exc_info
|
|
121
|
-
return {
|
|
122
|
-
'type': exc_type.__name__ if exc_type else None,
|
|
123
|
-
'message': str(exc_value) if exc_value else None,
|
|
124
|
-
'stacktrace': traceback.format_tb(exc_tb) if exc_tb else None
|
|
125
|
-
}
|
|
126
|
-
|
|
127
165
|
def emit(self, record: logging.LogRecord):
|
|
128
166
|
"""Send the log record to Secploy ingest."""
|
|
167
|
+
print(f'\n\nLEVEL NAME: {record.levelname} - LEVEL NO: {record.levelno}\n\n')
|
|
129
168
|
try:
|
|
130
169
|
# Get stacktrace if exception exists
|
|
131
170
|
stacktrace = []
|
|
132
171
|
if record.exc_info:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
172
|
+
stacktrace = traceback.format_exception(*record.exc_info)
|
|
173
|
+
elif record.stack_info:
|
|
174
|
+
stacktrace = [record.stack_info]
|
|
136
175
|
|
|
137
176
|
# Build tags with log metadata
|
|
138
177
|
tags = Tags(
|
|
@@ -144,23 +183,22 @@ class SecployLogHandler(logging.Handler):
|
|
|
144
183
|
# Build context
|
|
145
184
|
context = Context(
|
|
146
185
|
user_id=getattr(record, 'user_id', 'unknown'),
|
|
147
|
-
session_id=self.get_thread_id(),
|
|
186
|
+
session_id=self.get_thread_id(),
|
|
148
187
|
http_method=getattr(record, 'http_method', 'NONE'),
|
|
149
|
-
http_url=getattr(record, 'path', ''),
|
|
150
188
|
http_status=getattr(record, 'status_code', 0),
|
|
151
|
-
|
|
152
|
-
|
|
189
|
+
tags=tags,
|
|
190
|
+
http_url=getattr(record, "http_url", getattr(record, "path", "")),
|
|
191
|
+
stacktrace=stacktrace
|
|
153
192
|
)
|
|
154
193
|
|
|
155
194
|
# Create LogEntry using our schema
|
|
156
195
|
log_entry = LogEntry(
|
|
157
196
|
timestamp=datetime.fromtimestamp(record.created).timestamp(),
|
|
158
|
-
type=record.levelname.lower(),
|
|
197
|
+
type=record.levelname.lower(),
|
|
159
198
|
message=self.format(record),
|
|
160
199
|
context=context
|
|
161
200
|
)
|
|
162
201
|
payload = log_entry.model_dump(mode="json")
|
|
163
|
-
# Validate and send the log entry
|
|
164
202
|
self._event_handler.send_event('log', payload)
|
|
165
203
|
|
|
166
204
|
except Exception as e:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import TypedDict, Optional, Union, List
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from pydantic import BaseModel,
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
4
|
from .enums import LogLevel
|
|
5
5
|
|
|
6
6
|
|
|
@@ -28,8 +28,7 @@ class Tags(BaseModel):
|
|
|
28
28
|
service: str
|
|
29
29
|
region: str
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
extra = Extra.allow # Allow additional fields in tags
|
|
31
|
+
model_config = ConfigDict(extra="allow")
|
|
33
32
|
|
|
34
33
|
|
|
35
34
|
class Context(BaseModel):
|
|
@@ -40,6 +39,7 @@ class Context(BaseModel):
|
|
|
40
39
|
http_status: int
|
|
41
40
|
stacktrace: List[str]
|
|
42
41
|
tags: Tags
|
|
42
|
+
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class LogEntry(BaseModel):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|