logler 1.0.2__cp311-cp311-manylinux_2_38_x86_64.whl → 1.0.7__cp311-cp311-manylinux_2_38_x86_64.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.
- logler/__init__.py +1 -1
- logler/investigate.py +9 -7
- logler/llm_cli.py +6 -4
- logler/safe_regex.py +124 -0
- logler/web/app.py +29 -5
- {logler-1.0.2.dist-info → logler-1.0.7.dist-info}/METADATA +1 -1
- {logler-1.0.2.dist-info → logler-1.0.7.dist-info}/RECORD +11 -10
- logler_rs/logler_rs.cpython-311-x86_64-linux-gnu.so +0 -0
- {logler-1.0.2.dist-info → logler-1.0.7.dist-info}/WHEEL +0 -0
- {logler-1.0.2.dist-info → logler-1.0.7.dist-info}/entry_points.txt +0 -0
- {logler-1.0.2.dist-info → logler-1.0.7.dist-info}/licenses/LICENSE +0 -0
logler/__init__.py
CHANGED
logler/investigate.py
CHANGED
|
@@ -30,10 +30,13 @@ Example Usage:
|
|
|
30
30
|
|
|
31
31
|
import json
|
|
32
32
|
import re
|
|
33
|
+
import warnings
|
|
33
34
|
from typing import List, Optional, Dict, Any, Tuple
|
|
34
35
|
from datetime import datetime
|
|
35
36
|
from collections import defaultdict
|
|
36
37
|
|
|
38
|
+
from .safe_regex import try_compile
|
|
39
|
+
|
|
37
40
|
try:
|
|
38
41
|
import logler_rs
|
|
39
42
|
|
|
@@ -48,10 +51,10 @@ except ImportError:
|
|
|
48
51
|
RUST_AVAILABLE = True
|
|
49
52
|
else:
|
|
50
53
|
RUST_AVAILABLE = False
|
|
51
|
-
|
|
52
|
-
except
|
|
54
|
+
warnings.warn("Rust backend not available. Using Python fallback.", stacklevel=2)
|
|
55
|
+
except (ImportError, AttributeError, OSError):
|
|
53
56
|
RUST_AVAILABLE = False
|
|
54
|
-
|
|
57
|
+
warnings.warn("Rust backend not available. Using Python fallback.", stacklevel=2)
|
|
55
58
|
|
|
56
59
|
|
|
57
60
|
def _normalize_entry(entry: Dict[str, Any]) -> None:
|
|
@@ -97,9 +100,8 @@ def _apply_custom_regex_to_results(result: Dict[str, Any], pattern: Optional[str
|
|
|
97
100
|
"""Apply a user-provided regex to fill missing fields like timestamp/level."""
|
|
98
101
|
if not pattern:
|
|
99
102
|
return
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
except re.error:
|
|
103
|
+
regex = try_compile(pattern)
|
|
104
|
+
if regex is None:
|
|
103
105
|
return
|
|
104
106
|
|
|
105
107
|
for item in result.get("results", []) or []:
|
|
@@ -2198,7 +2200,7 @@ def cross_service_timeline(
|
|
|
2198
2200
|
e for e in all_entries if e["timestamp"] and start_dt <= e["timestamp"] <= end_dt
|
|
2199
2201
|
]
|
|
2200
2202
|
except Exception as e:
|
|
2201
|
-
|
|
2203
|
+
warnings.warn(f"Could not parse time window: {e}", stacklevel=2)
|
|
2202
2204
|
|
|
2203
2205
|
# Sort by timestamp
|
|
2204
2206
|
all_entries.sort(key=lambda e: e["timestamp"] if e["timestamp"] else datetime.min)
|
logler/llm_cli.py
CHANGED
|
@@ -18,6 +18,8 @@ from typing import Optional, List, Dict, Any
|
|
|
18
18
|
from datetime import datetime, timedelta
|
|
19
19
|
from collections import defaultdict
|
|
20
20
|
|
|
21
|
+
from .safe_regex import safe_compile, RegexTimeoutError, RegexPatternTooLongError
|
|
22
|
+
|
|
21
23
|
# Exit codes
|
|
22
24
|
EXIT_SUCCESS = 0 # Success with results
|
|
23
25
|
EXIT_NO_RESULTS = 1 # Success but no results found
|
|
@@ -808,8 +810,8 @@ def verify_pattern(
|
|
|
808
810
|
_error_json(f"No files found matching: {files}")
|
|
809
811
|
|
|
810
812
|
try:
|
|
811
|
-
regex =
|
|
812
|
-
except re.error as e:
|
|
813
|
+
regex = safe_compile(pattern)
|
|
814
|
+
except (re.error, RegexTimeoutError, RegexPatternTooLongError) as e:
|
|
813
815
|
_error_json(f"Invalid regex pattern: {e}")
|
|
814
816
|
|
|
815
817
|
parser = LogParser()
|
|
@@ -950,8 +952,8 @@ def emit(
|
|
|
950
952
|
query_regex = None
|
|
951
953
|
if query:
|
|
952
954
|
try:
|
|
953
|
-
query_regex =
|
|
954
|
-
except re.error as e:
|
|
955
|
+
query_regex = safe_compile(query, re.IGNORECASE)
|
|
956
|
+
except (re.error, RegexTimeoutError, RegexPatternTooLongError) as e:
|
|
955
957
|
_error_json(f"Invalid regex pattern: {e}")
|
|
956
958
|
|
|
957
959
|
for file_path in file_list:
|
logler/safe_regex.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Safe regex compilation with timeout protection against ReDoS attacks.
|
|
3
|
+
|
|
4
|
+
This module provides a safe_compile function that wraps re.compile with:
|
|
5
|
+
- Pattern length validation
|
|
6
|
+
- Compilation timeout (Unix only, graceful fallback on Windows)
|
|
7
|
+
- Clear error messages
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import threading
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RegexTimeoutError(Exception):
|
|
16
|
+
"""Raised when regex compilation times out."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RegexPatternTooLongError(Exception):
|
|
22
|
+
"""Raised when regex pattern exceeds maximum allowed length."""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Maximum pattern length to prevent ReDoS via complexity
|
|
28
|
+
MAX_PATTERN_LENGTH = 1000
|
|
29
|
+
|
|
30
|
+
# Timeout for regex compilation in seconds
|
|
31
|
+
COMPILE_TIMEOUT = 2.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _compile_with_timeout(
|
|
35
|
+
pattern: str,
|
|
36
|
+
flags: int,
|
|
37
|
+
timeout: float,
|
|
38
|
+
result_container: dict,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Worker function for threaded compilation."""
|
|
41
|
+
try:
|
|
42
|
+
result_container["result"] = re.compile(pattern, flags)
|
|
43
|
+
except re.error as e:
|
|
44
|
+
result_container["error"] = e
|
|
45
|
+
except Exception as e:
|
|
46
|
+
result_container["error"] = e
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def safe_compile(
|
|
50
|
+
pattern: str,
|
|
51
|
+
flags: int = 0,
|
|
52
|
+
timeout: float = COMPILE_TIMEOUT,
|
|
53
|
+
max_length: int = MAX_PATTERN_LENGTH,
|
|
54
|
+
) -> re.Pattern:
|
|
55
|
+
"""
|
|
56
|
+
Safely compile a regex pattern with timeout protection.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
pattern: The regex pattern to compile
|
|
60
|
+
flags: Optional regex flags (e.g., re.IGNORECASE)
|
|
61
|
+
timeout: Maximum time in seconds to allow for compilation
|
|
62
|
+
max_length: Maximum allowed pattern length
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Compiled regex pattern
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
RegexPatternTooLongError: If pattern exceeds max_length
|
|
69
|
+
RegexTimeoutError: If compilation takes longer than timeout
|
|
70
|
+
re.error: If the pattern is invalid
|
|
71
|
+
"""
|
|
72
|
+
# Validate pattern length
|
|
73
|
+
if len(pattern) > max_length:
|
|
74
|
+
raise RegexPatternTooLongError(
|
|
75
|
+
f"Regex pattern length {len(pattern)} exceeds maximum {max_length}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Use threading for cross-platform timeout support
|
|
79
|
+
result_container: dict = {}
|
|
80
|
+
thread = threading.Thread(
|
|
81
|
+
target=_compile_with_timeout,
|
|
82
|
+
args=(pattern, flags, timeout, result_container),
|
|
83
|
+
)
|
|
84
|
+
thread.start()
|
|
85
|
+
thread.join(timeout=timeout)
|
|
86
|
+
|
|
87
|
+
if thread.is_alive():
|
|
88
|
+
# Thread is still running - compilation timed out
|
|
89
|
+
# Note: We can't actually kill the thread, but we return an error
|
|
90
|
+
raise RegexTimeoutError(
|
|
91
|
+
f"Regex compilation timed out after {timeout}s (pattern may cause catastrophic backtracking)"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if "error" in result_container:
|
|
95
|
+
raise result_container["error"]
|
|
96
|
+
|
|
97
|
+
return result_container["result"]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def try_compile(
|
|
101
|
+
pattern: str,
|
|
102
|
+
flags: int = 0,
|
|
103
|
+
timeout: float = COMPILE_TIMEOUT,
|
|
104
|
+
max_length: int = MAX_PATTERN_LENGTH,
|
|
105
|
+
) -> Optional[re.Pattern]:
|
|
106
|
+
"""
|
|
107
|
+
Try to compile a regex pattern safely, returning None on failure.
|
|
108
|
+
|
|
109
|
+
This is a convenience wrapper around safe_compile that catches all
|
|
110
|
+
exceptions and returns None instead.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
pattern: The regex pattern to compile
|
|
114
|
+
flags: Optional regex flags
|
|
115
|
+
timeout: Maximum compilation time
|
|
116
|
+
max_length: Maximum pattern length
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Compiled pattern or None if compilation fails
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
return safe_compile(pattern, flags, timeout, max_length)
|
|
123
|
+
except (RegexTimeoutError, RegexPatternTooLongError, re.error):
|
|
124
|
+
return None
|
logler/web/app.py
CHANGED
|
@@ -36,6 +36,22 @@ def _ensure_within_root(path: Path) -> Path:
|
|
|
36
36
|
raise HTTPException(status_code=403, detail="Requested path is outside the configured log root")
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def _sanitize_glob_pattern(pattern: str) -> str:
|
|
40
|
+
"""Remove path traversal sequences from glob patterns."""
|
|
41
|
+
import re as _re
|
|
42
|
+
|
|
43
|
+
# Remove any ../ or ..\ sequences that could escape the root
|
|
44
|
+
# Use a loop to handle multiple consecutive traversal attempts like ../../
|
|
45
|
+
while ".." in pattern:
|
|
46
|
+
old_pattern = pattern
|
|
47
|
+
pattern = _re.sub(r"\.\.[\\/]", "", pattern)
|
|
48
|
+
pattern = _re.sub(r"[\\/]\.\.", "", pattern)
|
|
49
|
+
pattern = _re.sub(r"^\.\.", "", pattern) # Leading ..
|
|
50
|
+
if pattern == old_pattern:
|
|
51
|
+
break # No more changes possible
|
|
52
|
+
return pattern
|
|
53
|
+
|
|
54
|
+
|
|
39
55
|
def _glob_within_root(pattern: str) -> List[Path]:
|
|
40
56
|
"""
|
|
41
57
|
Run a glob pattern scoped to LOG_ROOT, returning file paths only.
|
|
@@ -43,6 +59,9 @@ def _glob_within_root(pattern: str) -> List[Path]:
|
|
|
43
59
|
if not pattern:
|
|
44
60
|
return []
|
|
45
61
|
|
|
62
|
+
# Sanitize the pattern to prevent path traversal
|
|
63
|
+
pattern = _sanitize_glob_pattern(pattern)
|
|
64
|
+
|
|
46
65
|
# Normalize relative patterns to LOG_ROOT
|
|
47
66
|
raw_pattern = pattern
|
|
48
67
|
if not Path(pattern).is_absolute():
|
|
@@ -168,14 +187,14 @@ def _rust_filter(
|
|
|
168
187
|
) -> Optional[List[Dict[str, Any]]]:
|
|
169
188
|
try:
|
|
170
189
|
import logler_rs # type: ignore
|
|
171
|
-
except
|
|
190
|
+
except ImportError:
|
|
172
191
|
return None
|
|
173
192
|
|
|
174
193
|
try:
|
|
175
194
|
from ..cache import get_cached_investigator
|
|
176
195
|
|
|
177
196
|
inv = get_cached_investigator(files)
|
|
178
|
-
except
|
|
197
|
+
except (ImportError, AttributeError, TypeError):
|
|
179
198
|
inv = logler_rs.PyInvestigator()
|
|
180
199
|
inv.load_files(files)
|
|
181
200
|
|
|
@@ -226,7 +245,8 @@ def _rust_filter(
|
|
|
226
245
|
if track:
|
|
227
246
|
_track_entries(entries)
|
|
228
247
|
return entries
|
|
229
|
-
except
|
|
248
|
+
except (json.JSONDecodeError, KeyError, ValueError, AttributeError, RuntimeError):
|
|
249
|
+
# Rust filter failed - fall back to Python implementation
|
|
230
250
|
return None
|
|
231
251
|
|
|
232
252
|
|
|
@@ -646,7 +666,10 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
|
646
666
|
await follow_file(websocket, file_path, current_filters, drop_count)
|
|
647
667
|
|
|
648
668
|
except WebSocketDisconnect:
|
|
649
|
-
|
|
669
|
+
pass # Normal disconnect
|
|
670
|
+
finally:
|
|
671
|
+
if websocket in websocket_clients:
|
|
672
|
+
websocket_clients.remove(websocket)
|
|
650
673
|
|
|
651
674
|
|
|
652
675
|
async def follow_file(
|
|
@@ -667,7 +690,8 @@ async def follow_file(
|
|
|
667
690
|
with open(path, "r") as f:
|
|
668
691
|
f.seek(0, 2)
|
|
669
692
|
position = f.tell()
|
|
670
|
-
|
|
693
|
+
with open(path, "r") as f:
|
|
694
|
+
line_number = sum(1 for _ in f)
|
|
671
695
|
|
|
672
696
|
# Follow file
|
|
673
697
|
try:
|
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
logler/__init__.py,sha256=
|
|
1
|
+
logler/__init__.py,sha256=0WHiHld8WxfO6VAlSVrrguLyPJy3dYsVWs4RIAB5rK8,513
|
|
2
2
|
logler/bootstrap.py,sha256=mpc3VUHDPXbMYj1xlZuGmx4I6vSRjL880TuJpBkZj4k,1289
|
|
3
3
|
logler/cache.py,sha256=HxJZ_s2yzhMgz77QpSKgdC8UZextS0PKgtmi0yHnfu8,2324
|
|
4
4
|
logler/cli.py,sha256=87E4V9Ow8A26y4ymyZANLuHkL3bXXteQ3nRAl0y2wcA,22816
|
|
5
5
|
logler/helpers.py,sha256=lhv_a6RQ97JcOp8SVhwVEdFhT-rfMSxf4fqe9a_Xsfk,9182
|
|
6
|
-
logler/investigate.py,sha256=
|
|
7
|
-
logler/llm_cli.py,sha256=
|
|
6
|
+
logler/investigate.py,sha256=0VCqkTB_U6oc2BfCdLZox94QZo1UliMHrg-2-4TBSQw,136461
|
|
7
|
+
logler/llm_cli.py,sha256=JryWimoTHkvOv1F0IyJmRhoDR0rei48toXrUlLgvo8s,49268
|
|
8
8
|
logler/log_reader.py,sha256=VCnvlj90peG-jFPJujyV7WzSk-fFdefLs_xAhON6lhA,8545
|
|
9
9
|
logler/parser.py,sha256=GyOP0WnpkvaLP-ZZ3ufmyUb3XRM02SDkt6R5msR7vXc,6484
|
|
10
|
+
logler/safe_regex.py,sha256=K7ryQSvUrTZG8bynJ2K1s7TdBup9hW3DJvQlG9yJ3vA,3431
|
|
10
11
|
logler/terminal.py,sha256=46iQAxU7mX5OVC8GhWi_aSs1hRGCNS3Ub_XmguMPWS8,8103
|
|
11
12
|
logler/tracker.py,sha256=zlVFr3H7ev1A7klziVwNwpZ9_PH9eShyJYU4AI7wXaw,4656
|
|
12
13
|
logler/tree_formatter.py,sha256=ZIG12bt7MfbTk4vLwtvvE9MUGIKBjBl1wn57jpjbmhg,27576
|
|
13
14
|
logler/watcher.py,sha256=p5HyCjVy7O14CA-RI8_HoZbZXdXHaNy04_gs60vqQt4,1526
|
|
14
15
|
logler/web/__init__.py,sha256=5UhDXVGSD9f7hGVVgsU1cJha1lHeZvP5tAYr3oUf09Q,34
|
|
15
|
-
logler/web/app.py,sha256=
|
|
16
|
+
logler/web/app.py,sha256=vY8HZPaul5PLuHR-cIZ7l6pYWv7K0ORDyN5-2FH3Ui4,25927
|
|
16
17
|
logler/web/static/css/tailwind.css,sha256=mUKXSL_ROgO2A1hhOcPxQhvUmdRl72RNRZUGTQSKZkU,12743
|
|
17
18
|
logler/web/static/css/tailwind.input.css,sha256=zBp60NAZ3bHTLQ7LWIugrCbOQdhiXdbDZjSLJfg6KOw,59
|
|
18
19
|
logler/web/static/logler-logo.png,sha256=5S0tVP1HqU2VniFmfTWISJfUHkJSKlww_cilBwWm-uU,1430687
|
|
19
20
|
logler/web/tailwind.config.cjs,sha256=aDOQf-5Fq4l73hN4BgkkjRMbR9EF2toT2rhW2Z5a_Mo,130
|
|
20
21
|
logler/web/templates/index.html,sha256=ymQIQDQGctv3tuW6ig7mIoQRjtbed5Den0xxeF72RVI,77047
|
|
21
|
-
logler-1.0.
|
|
22
|
-
logler-1.0.
|
|
23
|
-
logler-1.0.
|
|
24
|
-
logler-1.0.
|
|
22
|
+
logler-1.0.7.dist-info/METADATA,sha256=07N_AgpYnPpC5eGFSjD9E4rKOinix6Xmj56pRkTaN0U,20835
|
|
23
|
+
logler-1.0.7.dist-info/WHEEL,sha256=yOXEotj6jig1MGTeOjL9PYNG7IpztVvHxNhmm3wo1LQ,109
|
|
24
|
+
logler-1.0.7.dist-info/entry_points.txt,sha256=KAtycgnrhm85NgD_TCx2QslE9oClYHWKPM9NG598RNs,41
|
|
25
|
+
logler-1.0.7.dist-info/licenses/LICENSE,sha256=SaPvdtwQLl1BA-6s4Exl0M3X7L_6gcnOGzBD86t7dIo,1061
|
|
25
26
|
logler_rs/__init__.py,sha256=y2ULUqMIhS92vwz6utTdkCn5l_T1Kso3YLGqCIZUxKY,119
|
|
26
|
-
logler_rs/logler_rs.cpython-311-x86_64-linux-gnu.so,sha256=
|
|
27
|
-
logler-1.0.
|
|
27
|
+
logler_rs/logler_rs.cpython-311-x86_64-linux-gnu.so,sha256=PpoObvCVQNVWpNeWZxWkbCrOea0zkLbZGoB2doBwZHE,48948480
|
|
28
|
+
logler-1.0.7.dist-info/RECORD,,
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|