qalita 2.5.3__py3-none-any.whl → 2.5.4__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.
Files changed (50) hide show
  1. qalita/_frontend/node_modules/react/cjs/react-compiler-runtime.production.js +16 -0
  2. qalita/_frontend/node_modules/react/cjs/react-jsx-dev-runtime.production.js +14 -0
  3. qalita/_frontend/node_modules/react/cjs/react-jsx-runtime.production.js +34 -0
  4. qalita/_frontend/node_modules/react/cjs/react.production.js +542 -0
  5. qalita/_frontend/node_modules/react/compiler-runtime.js +14 -0
  6. qalita/_frontend/node_modules/react/index.js +1 -1
  7. qalita/_frontend/node_modules/react/jsx-dev-runtime.js +1 -1
  8. qalita/_frontend/node_modules/react/jsx-runtime.js +1 -1
  9. qalita/_frontend/node_modules/react/package.json +19 -15
  10. qalita/_frontend/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +6603 -0
  11. qalita/_frontend/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +6692 -0
  12. qalita/_frontend/node_modules/react-dom/cjs/react-dom-server.browser.production.js +7410 -0
  13. qalita/_frontend/node_modules/react-dom/cjs/react-dom-server.edge.production.js +7512 -0
  14. qalita/_frontend/node_modules/react-dom/cjs/react-dom-server.node.production.js +7707 -0
  15. qalita/_frontend/node_modules/react-dom/cjs/react-dom.production.js +210 -0
  16. qalita/_frontend/node_modules/react-dom/index.js +1 -1
  17. qalita/_frontend/node_modules/react-dom/package.json +75 -20
  18. qalita/_frontend/node_modules/react-dom/server.browser.js +3 -4
  19. qalita/_frontend/node_modules/react-dom/server.edge.js +17 -0
  20. qalita/_frontend/node_modules/react-dom/server.node.js +5 -4
  21. qalita/_frontend/node_modules/react-dom/static.node.js +14 -0
  22. qalita/_frontend/package.json +6 -6
  23. qalita/commands/pack.py +41 -23
  24. qalita/commands/source.py +5 -4
  25. qalita/commands/worker.py +72 -24
  26. qalita/internal/request.py +4 -1
  27. qalita/internal/utils.py +178 -20
  28. qalita/web/blueprints/context.py +2 -1
  29. qalita/web/blueprints/dashboard.py +14 -5
  30. qalita/web/blueprints/sources.py +24 -6
  31. qalita/web/blueprints/workers.py +18 -4
  32. {qalita-2.5.3.dist-info → qalita-2.5.4.dist-info}/METADATA +2 -2
  33. {qalita-2.5.3.dist-info → qalita-2.5.4.dist-info}/RECORD +38 -37
  34. qalita/_frontend/node_modules/react/cjs/react-jsx-dev-runtime.production.min.js +0 -10
  35. qalita/_frontend/node_modules/react/cjs/react-jsx-runtime.production.min.js +0 -11
  36. qalita/_frontend/node_modules/react/cjs/react.production.min.js +0 -26
  37. qalita/_frontend/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.min.js +0 -93
  38. qalita/_frontend/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.min.js +0 -101
  39. qalita/_frontend/node_modules/react-dom/cjs/react-dom-server.browser.production.min.js +0 -96
  40. qalita/_frontend/node_modules/react-dom/cjs/react-dom-server.node.production.min.js +0 -102
  41. qalita/_frontend/node_modules/react-dom/cjs/react-dom.production.min.js +0 -322
  42. qalita/_frontend/node_modules/scheduler/cjs/scheduler.development.js +0 -634
  43. qalita/_frontend/node_modules/scheduler/cjs/scheduler.production.min.js +0 -19
  44. qalita/_frontend/node_modules/scheduler/index.js +0 -7
  45. qalita/_frontend/node_modules/scheduler/package.json +0 -36
  46. /qalita/_frontend/.next/static/{fm8OzItmoekYnFcSZ5R8j → X4_AlYMbCyee-ZVLjCYMg}/_buildManifest.js +0 -0
  47. /qalita/_frontend/.next/static/{fm8OzItmoekYnFcSZ5R8j → X4_AlYMbCyee-ZVLjCYMg}/_ssgManifest.js +0 -0
  48. {qalita-2.5.3.dist-info → qalita-2.5.4.dist-info}/WHEEL +0 -0
  49. {qalita-2.5.3.dist-info → qalita-2.5.4.dist-info}/entry_points.txt +0 -0
  50. {qalita-2.5.3.dist-info → qalita-2.5.4.dist-info}/licenses/LICENSE +0 -0
qalita/commands/worker.py CHANGED
@@ -570,16 +570,40 @@ def pull_pack(config, pack_id, pack_version=None):
570
570
 
571
571
  jobs_path = config.get_worker_run_path()
572
572
  # Système de caching, on regarde si le pack est déjà présent dans le cache sinon on le télécharge
573
- file_name = pack_url.split("/")[-1]
574
- bucket_name = pack_url.split("/")[3]
575
- s3_folder = "/".join(pack_url.split("/")[4:-1])
576
- local_path = f"{jobs_path}/{bucket_name}/{s3_folder}/{file_name}"
573
+ # Validate URL components to prevent path traversal attacks
574
+ import re
575
+ url_parts = pack_url.split("/")
576
+ file_name = url_parts[-1] if url_parts else ""
577
+ bucket_name = url_parts[3] if len(url_parts) > 3 else ""
578
+ s3_folder = "/".join(url_parts[4:-1]) if len(url_parts) > 4 else ""
579
+
580
+ # Sanitize path components - only allow alphanumeric, dots, hyphens, underscores
581
+ safe_pattern = re.compile(r'^[\w\-\.]+$')
582
+ if not file_name or not safe_pattern.match(file_name):
583
+ logger.error(f"Invalid file name in pack URL: {file_name}")
584
+ sys.exit(1)
585
+ if bucket_name and not safe_pattern.match(bucket_name):
586
+ logger.error(f"Invalid bucket name in pack URL: {bucket_name}")
587
+ sys.exit(1)
588
+
589
+ # Build path and validate it stays within jobs_path
590
+ cache_folder = os.path.join(jobs_path, bucket_name, s3_folder) if s3_folder else os.path.join(jobs_path, bucket_name)
591
+ local_path = os.path.join(cache_folder, file_name)
592
+
593
+ # Resolve to real paths and verify containment
594
+ jobs_path_real = os.path.realpath(os.path.abspath(jobs_path))
595
+ local_path_normalized = os.path.normpath(os.path.abspath(local_path))
596
+
597
+ # Check that the path doesn't escape jobs_path
598
+ if not local_path_normalized.startswith(jobs_path_real + os.sep) and local_path_normalized != jobs_path_real:
599
+ logger.error(f"Invalid pack cache path detected: {local_path_normalized}")
600
+ sys.exit(1)
577
601
 
578
602
  if os.path.exists(local_path):
579
603
  logger.info(f"Using CACHED Pack at : {local_path}")
580
604
  return local_path, pack_version
581
- if not os.path.exists(f"{jobs_path}/{bucket_name}/{s3_folder}"):
582
- os.makedirs(f"{jobs_path}/{bucket_name}/{s3_folder}")
605
+ if not os.path.exists(cache_folder):
606
+ os.makedirs(cache_folder)
583
607
 
584
608
  # Fetch the pack from api
585
609
  response = send_api_request(f"/api/v1/assets/{pack_asset_id}/fetch", "get")
@@ -678,27 +702,51 @@ def job_run(
678
702
  # Uncompress the pack
679
703
  archive_name = pack_file_path.split("/")[-1]
680
704
  archive_path = os.path.join(temp_folder_name, archive_name)
681
- with tarfile.open(archive_path, "r:gz") as tar:
682
- # Validate members
683
- safe_members = []
705
+
706
+ def is_within_directory(directory: str, target: str) -> bool:
707
+ """Check if target path is within the directory (prevents path traversal)."""
708
+ abs_directory = os.path.abspath(directory)
709
+ abs_target = os.path.abspath(target)
710
+ prefix = os.path.commonprefix([abs_directory, abs_target])
711
+ return prefix == abs_directory
712
+
713
+ def safe_extract(tar: tarfile.TarFile, path: str) -> None:
714
+ """Safely extract tar members, preventing path traversal attacks."""
684
715
  for member in tar.getmembers():
685
- # Skip if not a file
686
- if not member.isfile():
716
+ member_path = os.path.join(path, member.name)
717
+
718
+ # Check for absolute paths
719
+ if os.path.isabs(member.name):
720
+ logger.warning(f"Skipping absolute path in tar: {member.name}")
687
721
  continue
688
- # Check for path traversal attack
689
- if member.name.startswith(("/", "..")):
690
- logger.warning(
691
- f"Skipping potentially dangerous tar file member {member.name}"
692
- )
722
+
723
+ # Check for path traversal using '..'
724
+ if ".." in member.name.split("/") or ".." in member.name.split(os.sep):
725
+ logger.warning(f"Skipping path traversal attempt in tar: {member.name}")
693
726
  continue
694
- safe_members.append(member)
695
- # Assert that all members are safe
696
- if not all(
697
- not member.name.startswith(("/", "..")) for member in safe_members
698
- ):
699
- raise AssertionError("Unsafe tar file member detected")
700
- # Extract safe members
701
- tar.extractall(path=temp_folder_name, members=safe_members)
727
+
728
+ # Verify the resolved path is within the extraction directory
729
+ if not is_within_directory(path, member_path):
730
+ logger.warning(f"Skipping path outside extraction directory: {member.name}")
731
+ continue
732
+
733
+ # Check for unsafe symlinks
734
+ if member.issym() or member.islnk():
735
+ # Resolve the link target
736
+ if member.issym():
737
+ link_target = os.path.join(os.path.dirname(member_path), member.linkname)
738
+ else:
739
+ link_target = os.path.join(path, member.linkname)
740
+
741
+ if not is_within_directory(path, link_target):
742
+ logger.warning(f"Skipping symlink pointing outside directory: {member.name} -> {member.linkname}")
743
+ continue
744
+
745
+ # Extract the safe member
746
+ tar.extract(member, path)
747
+
748
+ with tarfile.open(archive_path, "r:gz") as tar:
749
+ safe_extract(tar, temp_folder_name)
702
750
 
703
751
  # Delete the compressed pack (Windows may keep a short lock; retry briefly)
704
752
  for _ in range(5):
@@ -121,7 +121,10 @@ def send_request(
121
121
  token = None
122
122
  headers = {"Authorization": f"Bearer {token}"} if token else {}
123
123
 
124
- verify_ssl = not os.getenv("SKIP_SSL_VERIFY", False)
124
+ # SSL verification is enabled by default for security
125
+ # Only disable if SKIP_SSL_VERIFY is explicitly set to a truthy value
126
+ skip_ssl_env = os.getenv("SKIP_SSL_VERIFY", "").lower()
127
+ verify_ssl = skip_ssl_env not in ("true", "1", "yes", "on")
125
128
 
126
129
  try:
127
130
  if mode == "post":
qalita/internal/utils.py CHANGED
@@ -26,16 +26,157 @@ def safe_path_join(base_dir: str, *paths: str) -> str:
26
26
  Raises:
27
27
  ValueError: If the resulting path would escape the base directory.
28
28
  """
29
- base = os.path.abspath(base_dir)
30
- full_path = os.path.abspath(os.path.join(base, *paths))
29
+ # Validate base_dir
30
+ if not isinstance(base_dir, str):
31
+ raise ValueError("Base directory must be a string")
32
+ if "\x00" in base_dir:
33
+ raise ValueError("Base directory contains null bytes")
34
+
35
+ # Validate path components
36
+ for p in paths:
37
+ if not isinstance(p, str):
38
+ raise ValueError("Path component must be a string")
39
+ if "\x00" in p:
40
+ raise ValueError("Path component contains null bytes")
41
+ # Reject absolute paths in components
42
+ if os.path.isabs(p):
43
+ raise ValueError(f"Path component must be relative: {p}")
44
+
45
+ base = os.path.realpath(os.path.abspath(base_dir))
46
+ full_path = os.path.realpath(os.path.abspath(os.path.join(base, *paths)))
47
+
31
48
  # Ensure the resulting path is within the base directory
32
49
  if not full_path.startswith(base + os.sep) and full_path != base:
33
50
  raise ValueError(f"Path traversal detected: attempted to access {full_path} outside of {base}")
34
51
  return full_path
35
52
 
36
53
 
37
- def safe_path_check(path: str) -> str:
38
- """Validate and normalize a path, preventing common path traversal patterns.
54
+ def validate_directory_path(path: str) -> str: # lgtm[py/path-injection]
55
+ """Validate and normalize a directory path for safe operations.
56
+
57
+ This function performs security checks to ensure the path is safe to use
58
+ for file system operations. It validates the path exists and is a directory.
59
+
60
+ Security: This function acts as a sanitizer for path injection attacks by:
61
+ - Rejecting null bytes and control characters
62
+ - Rejecting paths with traversal sequences after normalization
63
+ - Resolving to absolute canonical path (resolves symlinks)
64
+ - Verifying the path exists and is a directory
65
+
66
+ Args:
67
+ path: The directory path to validate.
68
+
69
+ Returns:
70
+ The normalized absolute path.
71
+
72
+ Raises:
73
+ ValueError: If the path is invalid or contains dangerous patterns.
74
+ FileNotFoundError: If the directory does not exist.
75
+ """
76
+ if not isinstance(path, str):
77
+ raise ValueError("Path must be a string")
78
+ if not path or not path.strip():
79
+ raise ValueError("Path cannot be empty")
80
+ if "\x00" in path:
81
+ raise ValueError("Path contains null bytes")
82
+ if any(ord(c) < 32 for c in path if c not in ('\t', '\n', '\r')):
83
+ raise ValueError("Path contains invalid control characters")
84
+
85
+ # Strip whitespace from input
86
+ clean_path = path.strip()
87
+
88
+ # Normalize path components (resolve .., ., etc.)
89
+ normalized = os.path.normpath(clean_path)
90
+
91
+ # Security check: reject paths that still contain traversal sequences after normalization
92
+ # This explicit check helps static analyzers recognize this as a sanitizer
93
+ if ".." in normalized.split(os.sep):
94
+ raise ValueError("Path contains directory traversal sequences")
95
+
96
+ # Get canonical absolute path (resolves symlinks)
97
+ # Security note: This is an intentional path sanitizer for CLI operations.
98
+ # The function validates user-provided paths with multiple security checks above.
99
+ abs_path = os.path.realpath(os.path.abspath(normalized)) # nosec B108 # lgtm[py/path-injection]
100
+
101
+ # Verify the resolved path is not empty
102
+ if not abs_path:
103
+ raise ValueError("Path resolves to empty string")
104
+
105
+ if not os.path.exists(abs_path):
106
+ raise FileNotFoundError(f"Directory does not exist: {abs_path}")
107
+ if not os.path.isdir(abs_path):
108
+ raise ValueError(f"Path is not a directory: {abs_path}")
109
+
110
+ return abs_path
111
+
112
+
113
+ def validate_file_path(path: str) -> str: # lgtm[py/path-injection]
114
+ """Validate and normalize a file path for safe operations.
115
+
116
+ This function performs security checks to ensure the path is safe to use
117
+ for file system operations. It validates the path exists and is a file.
118
+
119
+ Security: This function acts as a sanitizer for path injection attacks by:
120
+ - Rejecting null bytes and control characters
121
+ - Rejecting paths with traversal sequences after normalization
122
+ - Resolving to absolute canonical path (resolves symlinks)
123
+ - Verifying the path exists and is a file
124
+
125
+ Args:
126
+ path: The file path to validate.
127
+
128
+ Returns:
129
+ The normalized absolute path.
130
+
131
+ Raises:
132
+ ValueError: If the path is invalid or contains dangerous patterns.
133
+ FileNotFoundError: If the file does not exist.
134
+ """
135
+ if not isinstance(path, str):
136
+ raise ValueError("Path must be a string")
137
+ if not path or not path.strip():
138
+ raise ValueError("Path cannot be empty")
139
+ if "\x00" in path:
140
+ raise ValueError("Path contains null bytes")
141
+ if any(ord(c) < 32 for c in path if c not in ('\t', '\n', '\r')):
142
+ raise ValueError("Path contains invalid control characters")
143
+
144
+ # Strip whitespace from input
145
+ clean_path = path.strip()
146
+
147
+ # Normalize path components (resolve .., ., etc.)
148
+ normalized = os.path.normpath(clean_path)
149
+
150
+ # Security check: reject paths that still contain traversal sequences after normalization
151
+ # This explicit check helps static analyzers recognize this as a sanitizer
152
+ if ".." in normalized.split(os.sep):
153
+ raise ValueError("Path contains directory traversal sequences")
154
+
155
+ # Get canonical absolute path (resolves symlinks)
156
+ # Security note: This is an intentional path sanitizer for CLI operations.
157
+ # The function validates user-provided paths with multiple security checks above.
158
+ abs_path = os.path.realpath(os.path.abspath(normalized)) # nosec B108 # lgtm[py/path-injection]
159
+
160
+ # Verify the resolved path is not empty
161
+ if not abs_path:
162
+ raise ValueError("Path resolves to empty string")
163
+
164
+ if not os.path.exists(abs_path):
165
+ raise FileNotFoundError(f"File does not exist: {abs_path}")
166
+ if not os.path.isfile(abs_path):
167
+ raise ValueError(f"Path is not a file: {abs_path}")
168
+
169
+ return abs_path
170
+
171
+
172
+ def safe_path_check(path: str) -> str: # lgtm[py/path-injection]
173
+ """Validate and normalize a path, preventing path injection attacks.
174
+
175
+ Security: This function acts as a sanitizer for path injection attacks by:
176
+ - Rejecting null bytes and control characters
177
+ - Rejecting paths with traversal sequences after normalization
178
+ - Normalizing path components (resolve .., ., etc.)
179
+ - Resolving to canonical absolute path
39
180
 
40
181
  Args:
41
182
  path: The path to validate.
@@ -44,23 +185,44 @@ def safe_path_check(path: str) -> str:
44
185
  The normalized absolute path.
45
186
 
46
187
  Raises:
47
- ValueError: If the path contains suspicious traversal patterns.
188
+ ValueError: If the path contains dangerous patterns.
48
189
  """
49
- # Normalize the path
50
- normalized = os.path.normpath(path)
51
- abs_path = os.path.abspath(normalized)
190
+ if not isinstance(path, str):
191
+ raise ValueError("Path must be a string")
192
+
193
+ # Reject null bytes which can be used for path injection
194
+ if "\x00" in path:
195
+ raise ValueError("Path contains null bytes")
196
+
197
+ # Reject other control characters
198
+ if any(ord(c) < 32 for c in path if c not in ('\t', '\n', '\r')):
199
+ raise ValueError("Path contains invalid control characters")
200
+
201
+ # Strip whitespace
202
+ clean_path = path.strip() if path else path
203
+
204
+ # Normalize the path to resolve any .. or . components
205
+ normalized = os.path.normpath(clean_path)
206
+
207
+ # Security check: reject paths that still contain traversal sequences after normalization
208
+ # This explicit check helps static analyzers recognize this as a sanitizer
209
+ if ".." in normalized.split(os.sep):
210
+ raise ValueError("Path contains directory traversal sequences")
211
+
212
+ # Get the real absolute path (resolves symlinks)
213
+ # Security note: This is an intentional path sanitizer for CLI operations.
214
+ # The function validates user-provided paths with multiple security checks above.
215
+ abs_path = os.path.realpath(os.path.abspath(normalized)) # nosec B108 # lgtm[py/path-injection]
52
216
 
53
- # Check for suspicious patterns that might indicate path traversal attempts
54
- if ".." in path.split(os.sep):
55
- # Allow .. only if it results in a valid absolute path
56
- # This is for cases where relative paths are legitimately used
57
- pass
217
+ # Additional check: ensure the resolved path is not empty
218
+ if not abs_path:
219
+ raise ValueError("Path resolves to empty string")
58
220
 
59
221
  return abs_path
60
222
 
61
223
 
62
224
  def get_version():
63
- return "2.5.3"
225
+ return "2.5.4"
64
226
 
65
227
 
66
228
  def make_tarfile(output_filename, source_dir):
@@ -173,9 +335,7 @@ def test_connection(config, type_):
173
335
  client = InsecureClient(url, user=config["user"])
174
336
  client.status(config["path"], strict=False)
175
337
  elif type_ == "folder":
176
- folder_path = safe_path_check(config["path"])
177
- if not os.path.isdir(folder_path):
178
- raise Exception(f"Folder {folder_path} not found")
338
+ folder_path = validate_directory_path(config["path"]) # lgtm[py/path-injection]
179
339
  if not os.access(folder_path, os.R_OK):
180
340
  raise Exception(f"No read access to folder {folder_path}")
181
341
  elif type_ == "oracle":
@@ -191,9 +351,7 @@ def test_connection(config, type_):
191
351
  conn.close()
192
352
  # FTP support removed for security reasons
193
353
  elif type_ == "file":
194
- file_path = safe_path_check(config["path"])
195
- if not os.path.isfile(file_path):
196
- raise Exception(f"File {file_path} not found")
354
+ file_path = validate_file_path(config["path"]) # lgtm[py/path-injection]
197
355
  if not os.access(file_path, os.R_OK):
198
356
  raise Exception(f"No read access to file {file_path}")
199
357
  else:
@@ -57,8 +57,9 @@ def select_context():
57
57
  ok = True
58
58
  message = "Selection cleared"
59
59
  except Exception as exc:
60
+ logger.error(f"Failed to clear selection: {exc}")
60
61
  ok = False
61
- message = f"Failed to clear selection: {exc}"
62
+ message = "Failed to clear selection"
62
63
  else:
63
64
  try:
64
65
  root = qalita_home()
@@ -175,11 +175,20 @@ def push_pack_from_ui():
175
175
  feedback = None
176
176
  feedback_level = "info"
177
177
  if pack_dir:
178
- ok, message = push_from_directory(cfg, pack_dir)
179
- feedback = message or (
180
- "Pack pushed successfully." if ok else "Pack push failed."
181
- )
182
- feedback_level = "info" if ok else "error"
178
+ try:
179
+ ok, message = push_from_directory(cfg, pack_dir)
180
+ # Sanitize message to avoid exposing internal details
181
+ if ok:
182
+ feedback = "Pack pushed successfully."
183
+ else:
184
+ # Log the detailed message but return a generic one to the user
185
+ logger.error(f"Pack push failed: {message}")
186
+ feedback = "Pack push failed. Check the logs for details."
187
+ feedback_level = "info" if ok else "error"
188
+ except Exception:
189
+ logger.exception("Unexpected error during pack push")
190
+ feedback = "An unexpected error occurred during pack push."
191
+ feedback_level = "error"
183
192
  else:
184
193
  feedback = "Please select a pack folder."
185
194
  feedback_level = "error"
@@ -719,10 +719,19 @@ def _find_source_by_id(conf: Dict[str, Any], src_id: str) -> Dict[str, Any] | No
719
719
 
720
720
 
721
721
  def _csv_preview(source: Dict[str, Any], options: Dict[str, Any] | None = None) -> Tuple[Dict[str, Any], int]:
722
+ from qalita.internal.utils import validate_file_path
723
+
722
724
  cfg = source.get("config") if isinstance(source.get("config"), dict) else {}
723
- path = cfg.get("path") or source.get("path")
724
- if not path or not os.path.isfile(path):
725
- return {"ok": False, "message": "CSV file not found"}, 404
725
+ raw_path = cfg.get("path") or source.get("path")
726
+ if not raw_path:
727
+ return {"ok": False, "message": "CSV file path not specified"}, 400
728
+
729
+ # Validate and sanitize the file path to prevent path injection
730
+ try:
731
+ path = validate_file_path(raw_path)
732
+ except (ValueError, FileNotFoundError) as e:
733
+ logger.warning(f"CSV preview path validation failed: {e}")
734
+ return {"ok": False, "message": "CSV file not found or invalid path"}, 404
726
735
  delimiter = cfg.get("delimiter") or ","
727
736
  encoding = cfg.get("encoding") or "utf-8"
728
737
  has_header = bool(cfg.get("has_header", True))
@@ -776,15 +785,24 @@ _PREVIEW_HANDLERS: Dict[str, PreviewHandler] = {
776
785
 
777
786
 
778
787
  def _excel_preview(source: Dict[str, Any], options: Dict[str, Any] | None = None) -> Tuple[Dict[str, Any], int]:
788
+ from qalita.internal.utils import validate_file_path
789
+
779
790
  try:
780
791
  import openpyxl # type: ignore
781
792
  except Exception:
782
793
  return {"ok": False, "message": "Excel preview requires 'openpyxl' to be installed"}, 500
783
794
 
784
795
  cfg = source.get("config") if isinstance(source.get("config"), dict) else {}
785
- path = cfg.get("path") or source.get("path")
786
- if not path or not os.path.isfile(path):
787
- return {"ok": False, "message": "Excel file not found"}, 404
796
+ raw_path = cfg.get("path") or source.get("path")
797
+ if not raw_path:
798
+ return {"ok": False, "message": "Excel file path not specified"}, 400
799
+
800
+ # Validate and sanitize the file path to prevent path injection
801
+ try:
802
+ path = validate_file_path(raw_path)
803
+ except (ValueError, FileNotFoundError) as e:
804
+ logger.warning(f"Excel preview path validation failed: {e}")
805
+ return {"ok": False, "message": "Excel file not found or invalid path"}, 404
788
806
 
789
807
  sheet_name = (cfg.get("sheet") or "").strip()
790
808
  header_row_raw = cfg.get("header_row")
@@ -345,15 +345,29 @@ def worker_summary():
345
345
 
346
346
  @bp.get("/worker/run/<run_name>")
347
347
  def open_worker_run(run_name: str):
348
+ import re
349
+
348
350
  cfg = current_app.config["QALITA_CONFIG_OBJ"]
349
351
  run_root = cfg.get_worker_run_path()
350
-
351
- candidate = os.path.normpath(os.path.join(run_root, run_name))
352
- if not candidate.startswith(os.path.normpath(run_root) + os.sep):
352
+
353
+ # Validate run_name format to prevent path traversal attempts
354
+ # Expected format: YYYYMMDDHHMMSS_XXXXX (timestamp_randomseed)
355
+ if not run_name or not re.match(r'^[\w\-]+$', run_name):
356
+ return jsonify({
357
+ "ok": False,
358
+ "error": "Invalid run name format",
359
+ "message": "Run name contains invalid characters.",
360
+ }), 400
361
+
362
+ # Use realpath to resolve symlinks and get canonical paths
363
+ run_root_real = os.path.realpath(os.path.abspath(run_root))
364
+ candidate = os.path.realpath(os.path.abspath(os.path.join(run_root, run_name)))
365
+
366
+ # Ensure the resolved path is within the run_root directory
367
+ if not candidate.startswith(run_root_real + os.sep) and candidate != run_root_real:
353
368
  return jsonify({
354
369
  "ok": False,
355
370
  "error": "Invalid path",
356
- "path": candidate,
357
371
  "message": "If the path is invalid it means: Your local agent is not the one that ran the analysis you are trying to get files from, or the job failed or was cancelled.",
358
372
  }), 400
359
373
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qalita
3
- Version: 2.5.3
3
+ Version: 2.5.4
4
4
  Summary: QALITA Platform Command Line Interface
5
5
  Author-email: QALITA SAS <contact@qalita.io>
6
6
  License-File: LICENSE
@@ -45,7 +45,7 @@ Requires-Dist: toml>=0.10.2
45
45
  Requires-Dist: waitress==3.0.2
46
46
  Provides-Extra: dev
47
47
  Requires-Dist: black>=25.1.0; extra == 'dev'
48
- Requires-Dist: flake8<4.0,>=3.8.1; extra == 'dev'
48
+ Requires-Dist: flake8<8.0,>=3.8.1; extra == 'dev'
49
49
  Requires-Dist: pre-commit>=3.3.3; extra == 'dev'
50
50
  Requires-Dist: pylint>=2.17.4; extra == 'dev'
51
51
  Requires-Dist: pytest>=7.4.0; extra == 'dev'
@@ -1,7 +1,9 @@
1
1
  qalita/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  qalita/__main__.py,sha256=gs7MmT1d9-WPXK9ForTO37EC2oIKJWI03p5-7HpwGk8,13978
3
- qalita/_frontend/package.json,sha256=zWUPRRHPGlCMynXOyZfYC3bnpa7AW4T3e4yzTp0Wxqs,634
3
+ qalita/_frontend/package.json,sha256=MhIsZiRI5VIfq4UEVP5WRnGmj444vbiFW5ryfAkkqhY,634
4
4
  qalita/_frontend/server.js,sha256=f5pZF0Z-nZbY2aST7eArz7AmUdVtObAPV6x3-p9X8TQ,6417
5
+ qalita/_frontend/.next/static/X4_AlYMbCyee-ZVLjCYMg/_buildManifest.js,sha256=lHbgr9P52mimTm7FhjEZH9PqymLJ9aN351F_6aieWeo,219
6
+ qalita/_frontend/.next/static/X4_AlYMbCyee-ZVLjCYMg/_ssgManifest.js,sha256=Z49s4suAsf5y_GfnQSvm4qtq2ggxEbZPfEDTXjy6XgA,80
5
7
  qalita/_frontend/.next/static/chunks/023d923a37d494fc.js,sha256=Kv4cEeP6FJtSzuGGpMg-gKQD4H7j0ea6mKdMI_YEPzk,214812
6
8
  qalita/_frontend/.next/static/chunks/0f84739db4a8acc7.js,sha256=0Zhp_p6Gsh5e3RYFq9WPRTuNaDsqkWLGGzDpmee8L1c,12946
7
9
  qalita/_frontend/.next/static/chunks/1107bdca1eff6d34.css,sha256=Lkh6CMInMrpk_bFB5PHjbkfzyJVQojtC8b7EemDt710,27391
@@ -16,8 +18,6 @@ qalita/_frontend/.next/static/chunks/cbd55ab9639e1e66.js,sha256=tPgJbUb7fEba3Qbh
16
18
  qalita/_frontend/.next/static/chunks/e393fec0d8ba175d.js,sha256=cHc7vArx3ngdkkppgF6epM9e_JaIkVy5pWpqlC5Qfw8,29965
17
19
  qalita/_frontend/.next/static/chunks/ff1a16fafef87110.js,sha256=6XhrEwWJTkEZu5gRqBbayc9XZ1VZbdmJ7hW0VXSfw14,282
18
20
  qalita/_frontend/.next/static/chunks/turbopack-1ad58da399056f41.js,sha256=0w0vc4UMJogp7-l6SinryZc3wHS8vbQjVJFsPbcalM0,9909
19
- qalita/_frontend/.next/static/fm8OzItmoekYnFcSZ5R8j/_buildManifest.js,sha256=lHbgr9P52mimTm7FhjEZH9PqymLJ9aN351F_6aieWeo,219
20
- qalita/_frontend/.next/static/fm8OzItmoekYnFcSZ5R8j/_ssgManifest.js,sha256=Z49s4suAsf5y_GfnQSvm4qtq2ggxEbZPfEDTXjy6XgA,80
21
21
  qalita/_frontend/node_modules/@img/colour/color.cjs,sha256=oAjEFf-X2T1sHb6sWFYkfq_KGzypc7_NdRy1lujnKaw,44950
22
22
  qalita/_frontend/node_modules/@img/colour/index.cjs,sha256=5uk-f7IJQgog_fLsZ26cZ0E76NWs6Z7sZPlGhIDwNo0,49
23
23
  qalita/_frontend/node_modules/@img/colour/package.json,sha256=bvoBzsVeaHJG179iItF4Y944mYWHQxLmp0ud8aROlSo,1016
@@ -1175,26 +1175,27 @@ qalita/_frontend/node_modules/next/dist/trace/report/index.js,sha256=hVEGzQE7m3b
1175
1175
  qalita/_frontend/node_modules/next/dist/trace/report/to-json-build.js,sha256=hXI3spPSPDwrGZRZl4sm4lOGf93VLt2EM05mVXmQE2I,4181
1176
1176
  qalita/_frontend/node_modules/next/dist/trace/report/to-json.js,sha256=LhWr9g69oMDa1TPamj1T6lAks7kA6qDmY1OT2hsC4SM,4546
1177
1177
  qalita/_frontend/node_modules/next/dist/trace/report/to-telemetry.js,sha256=dqwHUJyKycCqnYC5gHDECKIe4PsfMHonEMIfr-kjC4M,849
1178
- qalita/_frontend/node_modules/react/index.js,sha256=ohbDkL-VbLuQbGbSn9suKtaZlGSlyoPkdlM6byWtDvM,190
1179
- qalita/_frontend/node_modules/react/jsx-dev-runtime.js,sha256=Jafb_bKuV0tn_N0qq4EQgLQV7dHIBD6tRoZqUz6ZMg4,222
1180
- qalita/_frontend/node_modules/react/jsx-runtime.js,sha256=KBcOYPL_C8BFvH0XX1hJ6G9o3QLXbXJVNpQgcYswKVo,214
1181
- qalita/_frontend/node_modules/react/package.json,sha256=1OaKXJa6uyDK0dqv6ot4gi6uRbNBVSz1QY0GpTBPpkw,999
1182
- qalita/_frontend/node_modules/react/cjs/react-jsx-dev-runtime.production.min.js,sha256=xGOG7ebTYY9_4VvpMS-WYJFoKOxOB77Gt2zGY22zSw0,343
1183
- qalita/_frontend/node_modules/react/cjs/react-jsx-runtime.production.min.js,sha256=duM2TEaJXtLEqNXMMy5eTvMdb3MtNbHlaCbcWJUkVFI,859
1184
- qalita/_frontend/node_modules/react/cjs/react.production.min.js,sha256=SzXjePRpxPSSx1FLsQa1gzdG27roBJa9SqmsxumsS0I,6930
1185
- qalita/_frontend/node_modules/react-dom/index.js,sha256=8m_LgbXR3DLG7eo0ZFx1tg0lmRYpPTIIyL5IrDPoguQ,1363
1186
- qalita/_frontend/node_modules/react-dom/package.json,sha256=0vKeMb1IuDPkjNe79B8ZLo7k_42iSf2-ENSsQRS4sS4,1353
1187
- qalita/_frontend/node_modules/react-dom/server.browser.js,sha256=JbKo--Xb9eOPMvsR2wrwxn9sYauz7yKnxxRzICyr9lM,658
1188
- qalita/_frontend/node_modules/react-dom/server.node.js,sha256=oPelZSe3P5WsKDV4gV-tSk5ehJ9FEmf7eg17NHRTcnw,646
1189
- qalita/_frontend/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.min.js,sha256=VfCOHDZfoVJTWUZ-aPcr9Q-QGnwLWWhd8Wj9zrAVk8s,35019
1190
- qalita/_frontend/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.min.js,sha256=Vws2V0rxlkDiQ8gpYlfzutD7QdhMx_Jzv3UszfhrW2A,38796
1191
- qalita/_frontend/node_modules/react-dom/cjs/react-dom-server.browser.production.min.js,sha256=ib3hqg-wRTMEGcfYkA4z1D5pOuM6W6rl_baxJhYP4pI,35597
1192
- qalita/_frontend/node_modules/react-dom/cjs/react-dom-server.node.production.min.js,sha256=ln6c6uKbr5BOUEsQIGHKByAVZ45OBxIfIS1dojoUAK8,39058
1193
- qalita/_frontend/node_modules/react-dom/cjs/react-dom.production.min.js,sha256=y_f-a5cm7mX4vNVxUonMUWHI2s900LiqoNKUElFvX0c,131685
1194
- qalita/_frontend/node_modules/scheduler/index.js,sha256=tBOzqse9IJ_YLL4iQf2hRnVY2q74MHRPRe2DlvrJGF4,198
1195
- qalita/_frontend/node_modules/scheduler/package.json,sha256=CJNkOiRXbXJxdOY5WGuIM6U_KntKIZmnGbnPzr6brog,700
1196
- qalita/_frontend/node_modules/scheduler/cjs/scheduler.development.js,sha256=IuPZWdyfnNWfyk4fyV_suf3k0suOytvSeRN62plZg6w,17497
1197
- qalita/_frontend/node_modules/scheduler/cjs/scheduler.production.min.js,sha256=9suM9CaWmAxwL2uHlBeri9SJZgSF6wErRvZ-kuGKeO4,4235
1178
+ qalita/_frontend/node_modules/react/compiler-runtime.js,sha256=XABFagGAt7ayNmNI9yGLDqLDqFx_7-JMGFM-1UEm9gE,412
1179
+ qalita/_frontend/node_modules/react/index.js,sha256=YMr_3svcXbO8TsToPflIg0XMsnHQdTO5IQv1dQkY2X4,186
1180
+ qalita/_frontend/node_modules/react/jsx-dev-runtime.js,sha256=SGWJEgqWJyHIKBtazsL21J5gzl-dkIzIFN3-ccYWOs4,218
1181
+ qalita/_frontend/node_modules/react/jsx-runtime.js,sha256=s5KVGnfQouamvQF-SddoSBrltjxBCcbDH2Is97JDPtw,210
1182
+ qalita/_frontend/node_modules/react/package.json,sha256=w4Os7WpnyNBvAplgFcChY-eFMqMwqW3_HM_RB7AKsyY,1248
1183
+ qalita/_frontend/node_modules/react/cjs/react-compiler-runtime.production.js,sha256=99UY9cBDzQCFFLT9jGZanS-laGvTbG59RNeBh8BDhwA,463
1184
+ qalita/_frontend/node_modules/react/cjs/react-jsx-dev-runtime.production.js,sha256=ups2cRmXHtdGklpgAlbTQutMw_I4neAT9h42_x81lKU,387
1185
+ qalita/_frontend/node_modules/react/cjs/react-jsx-runtime.production.js,sha256=HkbxUAJpaYXoDGHUeqow2rsDyVQnCmkPX337us-pACs,976
1186
+ qalita/_frontend/node_modules/react/cjs/react.production.js,sha256=h3I2ZdCW_Veil0xkt1TGMRHi5vNVVXlJOeMIzyUrTuc,17217
1187
+ qalita/_frontend/node_modules/react-dom/index.js,sha256=uho-M0ifhoNxVhd3YH4-5Qkd8PfqPxoQ94nWuG5MFQU,1359
1188
+ qalita/_frontend/node_modules/react-dom/package.json,sha256=2SbxWOG-FYharqwlnDlWekb2Ul-90Uw4BShIbDFFmZY,3063
1189
+ qalita/_frontend/node_modules/react-dom/server.browser.js,sha256=91lGDrNXHVvx4anl73ywM-xcZ_JdZKa79amsQ70lgxo,563
1190
+ qalita/_frontend/node_modules/react-dom/server.edge.js,sha256=pjFSdASwp7vhCqedsEhSGW_X_YigVsG7hM8aGIXh6TQ,561
1191
+ qalita/_frontend/node_modules/react-dom/server.node.js,sha256=8t2T9vkfmDzZEjIJjUrFVXhZbTPWgNfLlDjqOwphG0s,669
1192
+ qalita/_frontend/node_modules/react-dom/static.node.js,sha256=OeE07W3JA9iXKzrl4qohqHcsalP-00RX9NwH9rMVcns,445
1193
+ qalita/_frontend/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js,sha256=Iym-PAZByxWU1O1hZ0DekVoxsX_MzUzgDmNoCHFQUp8,237937
1194
+ qalita/_frontend/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js,sha256=LfUTZ0hEXsOlyDPs2UKkjBUdJ2EgTZ3Jw-EDEZ4C5bQ,242793
1195
+ qalita/_frontend/node_modules/react-dom/cjs/react-dom-server.browser.production.js,sha256=xYsMac3eKc3OJ4PRR4th-B5U8mf-EB_RP5OUqGVHKBs,268058
1196
+ qalita/_frontend/node_modules/react-dom/cjs/react-dom-server.edge.production.js,sha256=E6B-Zft43I2rwIB7CoIxb3NJHvQ6WEmkejmS-svqeoo,273614
1197
+ qalita/_frontend/node_modules/react-dom/cjs/react-dom-server.node.production.js,sha256=LLYx4ZQfkr6treNk_qEtNyd91L5LO90xkV6OUGSXAfA,277046
1198
+ qalita/_frontend/node_modules/react-dom/cjs/react-dom.production.js,sha256=r2ZWairoxxYqMyMi5AHAtRWF5tO-wiYdo6LYJbFoQVo,6655
1198
1199
  qalita/_frontend/node_modules/semver/package.json,sha256=fgufa1PgSLU7PKEXSi-vH88CBklyl2Fdm10C_ajyZcU,1663
1199
1200
  qalita/_frontend/node_modules/semver/classes/comparator.js,sha256=BUIClWQw1j1f9Fmfrgl2DORltInk8LXvXOfMesIRV6w,3631
1200
1201
  qalita/_frontend/node_modules/semver/classes/range.js,sha256=z4-OtpNGY64qnQSDjU1xVVQnoqvTyHSO5SGVB5aaa0Y,14977
@@ -1310,24 +1311,24 @@ qalita/_frontend/public/sources-logos/xls.svg,sha256=ckgO7s5pv9numYYFJboxvGQ_P3f
1310
1311
  qalita/_frontend/public/sources-logos/xlsx.svg,sha256=ckgO7s5pv9numYYFJboxvGQ_P3fLA1zz-2xiERcz2bE,2044
1311
1312
  qalita/_frontend/public/sources-logos/yugabyte-db.png,sha256=-sGA5gqwSIHlGqk6LHNQ5EogsywwKXiWbKXS3PluYKI,5212
1312
1313
  qalita/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1313
- qalita/commands/pack.py,sha256=aDxg5QKew73knKsvVFqLvmk3RUKdMWq1qMCuqugPgAw,38836
1314
- qalita/commands/source.py,sha256=SeYyPJITBQukJS4GqtJECW-s6EgXDQPcAsIMlIoQLDQ,31212
1315
- qalita/commands/worker.py,sha256=wDIaljpPpvKikbgC1L4IBTVmSradlWkHu0AeKSIrqXw,50642
1314
+ qalita/commands/pack.py,sha256=ua_QoSz1n7eFfr_f5NFac8mnhgLnCUmjCuEkE-ZQzHo,40292
1315
+ qalita/commands/source.py,sha256=reAeLAchaDu6YT32FmjrDw2X7hlk5c5GtuenMU1591I,31388
1316
+ qalita/commands/worker.py,sha256=Ds-Ih0LNfM8IdwLAXQsyP5ykboL7Lzcx30XcONvrehE,53077
1316
1317
  qalita/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1317
1318
  qalita/internal/config.py,sha256=FVooOC7T-IXOdzl3Q4TPr9ErhqP03QXmRSUo6OhIqIo,4314
1318
1319
  qalita/internal/error_patterns.py,sha256=QDFNZEoTmqVOHkiGFljstY6VOhux7d4p8FIL3pKzwtQ,1447
1319
1320
  qalita/internal/logger.py,sha256=4xWy2BZII89nUw4tKrLXFPOlVGONm8KUBgzX2XZ3u7g,1868
1320
- qalita/internal/request.py,sha256=s-rE9kKRhV9IEveclB2YjAY8FRevKb6qjblM8-PbV20,6209
1321
- qalita/internal/utils.py,sha256=sjr9wc20rWYOQJrd9Yd6ZuIHU68zG--XzdYFL4gXQB0,7367
1321
+ qalita/internal/request.py,sha256=cHRpinjdbVwxIskLVsqmzrDTyxybflhs_4Hk_SsStqo,6408
1322
+ qalita/internal/utils.py,sha256=AU_2Kl7CINomVIB33r5_rq62qxl6shvx_zJd-stTheU,14014
1322
1323
  qalita/web/__init__.py,sha256=NRcQjZSizkqN0a-LutsN7X9LBQ8MhyazXBSuqzUEmXE,62
1323
1324
  qalita/web/app.py,sha256=UvAzP7HcwE7E48ZEQViHHgVPE4Xw4TcIgdC7D_4xW4A,4973
1324
- qalita/web/blueprints/context.py,sha256=DjGRCURdlclkBvRAt0v9VK6ntwOzsOzFQPTi9zCcnhA,4734
1325
- qalita/web/blueprints/dashboard.py,sha256=-2e5SePWxrM5hRRlOmTLioyrzL5UXmpVgi0ZEN0kUCA,6088
1325
+ qalita/web/blueprints/context.py,sha256=dqaigSBLqyMIq_EI5OJ2I62YMlE2jaVVQgmpph0HqlE,4788
1326
+ qalita/web/blueprints/dashboard.py,sha256=LDYXol53YOut0R_KURQ88GYssD16v6MwkkCzTNo9Ucw,6571
1326
1327
  qalita/web/blueprints/helpers.py,sha256=4lSqT01IaWTFkFQB7J2-R5XKKsc4iDAxzeprPhhQIxI,17788
1327
- qalita/web/blueprints/sources.py,sha256=XgZ0iEPIH3UjA2WgTQihCV-Ut_h3NQy4rEGVBLN_Ses,34015
1328
- qalita/web/blueprints/workers.py,sha256=dPzwmTWQ32uslQqKLiS6v56Ik7GgHr-FLC3LHBdc2eE,15879
1329
- qalita-2.5.3.dist-info/METADATA,sha256=dFDQ5xYZqbi03FiF62GNm601s34PdG2kWJBlrvy_QdQ,2524
1330
- qalita-2.5.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
1331
- qalita-2.5.3.dist-info/entry_points.txt,sha256=h_4bMcWq-LKrSLca7IAPQnqlZkxz7BBH8eio4nqZCVU,48
1332
- qalita-2.5.3.dist-info/licenses/LICENSE,sha256=cZt92dnxw87-VK4HB6KnmYV7mpf4JUdBkAHzFn1kQxM,22458
1333
- qalita-2.5.3.dist-info/RECORD,,
1328
+ qalita/web/blueprints/sources.py,sha256=bzdLhwwPbhBzuWw7L6yvOcIT8_cjfvFWp2CGECDPeZo,34771
1329
+ qalita/web/blueprints/workers.py,sha256=5184SrpgT9JPktKZGd2yO4MMdi0SXe220dTGuLA1zLQ,16479
1330
+ qalita-2.5.4.dist-info/METADATA,sha256=Qjk78wFYiwpCOikPA9_lVVxczqIDl7pDxlSDjIuJDZ8,2524
1331
+ qalita-2.5.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
1332
+ qalita-2.5.4.dist-info/entry_points.txt,sha256=h_4bMcWq-LKrSLca7IAPQnqlZkxz7BBH8eio4nqZCVU,48
1333
+ qalita-2.5.4.dist-info/licenses/LICENSE,sha256=cZt92dnxw87-VK4HB6KnmYV7mpf4JUdBkAHzFn1kQxM,22458
1334
+ qalita-2.5.4.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- /**
2
- * @license React
3
- * react-jsx-dev-runtime.production.min.js
4
- *
5
- * Copyright (c) Facebook, Inc. and its affiliates.
6
- *
7
- * This source code is licensed under the MIT license found in the
8
- * LICENSE file in the root directory of this source tree.
9
- */
10
- 'use strict';var a=Symbol.for("react.fragment");exports.Fragment=a;exports.jsxDEV=void 0;