|
| 1 | +import re |
1 | 2 | import ssl |
2 | 3 | from abc import ABC, abstractmethod |
3 | 4 | from json import dumps, loads |
4 | 5 | from pathlib import Path |
5 | 6 | from typing import Any, cast |
6 | 7 | from urllib.parse import urljoin |
7 | 8 |
|
| 9 | +import paramiko |
8 | 10 | import requests |
9 | 11 | from aiohttp import BasicAuth, ClientSession, TCPConnector |
10 | 12 | from deprecated import deprecated |
@@ -1112,6 +1114,15 @@ async def _replaced_revid( |
1112 | 1114 | assert revid == response_dict["_cv"] or revid == response_dict["_rev"] |
1113 | 1115 | return cast(dict, response)["_rev"] |
1114 | 1116 |
|
| 1117 | + async def get_server_config(self) -> dict[str, Any]: |
| 1118 | + """ |
| 1119 | + Gets the server-level configuration from the admin API. |
| 1120 | +
|
| 1121 | + Returns: |
| 1122 | + Dictionary containing the server configuration including logging settings |
| 1123 | + """ |
| 1124 | + return await self._send_request("GET", "/_config") |
| 1125 | + |
1115 | 1126 | async def delete_document( |
1116 | 1127 | self, |
1117 | 1128 | doc_id: str, |
@@ -1290,3 +1301,88 @@ async def get_document_revision_public( |
1290 | 1301 | self.__secure, scheme, self.__hostname, 4984, auth |
1291 | 1302 | ) as session: |
1292 | 1303 | return await self._send_request("GET", path, params=params, session=session) |
| 1304 | + |
| 1305 | + def fetch_log_file( |
| 1306 | + self, |
| 1307 | + remote_log_path: str, |
| 1308 | + ssh_key_path: str, |
| 1309 | + ssh_username: str = "ec2-user", |
| 1310 | + ) -> str: |
| 1311 | + """ |
| 1312 | + Fetches a log file from the remote Sync Gateway server via SSH |
| 1313 | +
|
| 1314 | + :param remote_log_path: Path to the log file on the remote server |
| 1315 | + :param ssh_key_path: Path to SSH private key for authentication |
| 1316 | + :param ssh_username: SSH username (default: ec2-user) |
| 1317 | + :return: Contents of the log file as a string |
| 1318 | + """ |
| 1319 | + with self.__tracer.start_as_current_span( |
| 1320 | + "fetch_log_file", |
| 1321 | + attributes={ |
| 1322 | + "remote.path": remote_log_path, |
| 1323 | + "ssh.username": ssh_username, |
| 1324 | + }, |
| 1325 | + ): |
| 1326 | + ssh = paramiko.SSHClient() |
| 1327 | + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) |
| 1328 | + |
| 1329 | + # Load private key |
| 1330 | + private_key = paramiko.Ed25519Key.from_private_key_file(ssh_key_path) |
| 1331 | + |
| 1332 | + # Connect to the remote server |
| 1333 | + ssh.connect( |
| 1334 | + self.__hostname, |
| 1335 | + username=ssh_username, |
| 1336 | + pkey=private_key, |
| 1337 | + ) |
| 1338 | + |
| 1339 | + # Read the log file |
| 1340 | + sftp = ssh.open_sftp() |
| 1341 | + try: |
| 1342 | + with sftp.open(remote_log_path, "r") as remote_file: |
| 1343 | + log_contents = remote_file.read().decode("utf-8") |
| 1344 | + finally: |
| 1345 | + sftp.close() |
| 1346 | + ssh.close() |
| 1347 | + |
| 1348 | + return log_contents |
| 1349 | + |
| 1350 | + |
| 1351 | +def scan_logs_for_untagged_sensitive_data( |
| 1352 | + log_content: str, |
| 1353 | + sensitive_patterns: list[str], |
| 1354 | +) -> list[str]: |
| 1355 | + """ |
| 1356 | + Scans log content for sensitive data that is NOT wrapped in <ud>...</ud> tags |
| 1357 | +
|
| 1358 | + :param log_content: The log file content as a string |
| 1359 | + :param sensitive_patterns: List of sensitive strings to look for (e.g., doc IDs, usernames) |
| 1360 | + :return: List of violations found (sensitive data without <ud> tags) |
| 1361 | + """ |
| 1362 | + violations = [] |
| 1363 | + for pattern in sensitive_patterns: |
| 1364 | + # Escape special regex characters in the pattern |
| 1365 | + escaped_pattern = re.escape(pattern) |
| 1366 | + for match in re.finditer(escaped_pattern, log_content): |
| 1367 | + start_pos = match.start() |
| 1368 | + end_pos = match.end() |
| 1369 | + |
| 1370 | + # Check if this occurrence is within <ud>...</ud> tags |
| 1371 | + # Look backwards for <ud> and forwards for </ud> |
| 1372 | + before_text = log_content[max(0, start_pos - 100) : start_pos] |
| 1373 | + after_text = log_content[end_pos : min(len(log_content), end_pos + 100)] |
| 1374 | + |
| 1375 | + # Check if there's an opening <ud> before and closing </ud> after |
| 1376 | + has_opening_tag = "<ud>" in before_text and before_text.rfind( |
| 1377 | + "<ud>" |
| 1378 | + ) > before_text.rfind("</ud>") |
| 1379 | + has_closing_tag = "</ud>" in after_text |
| 1380 | + |
| 1381 | + if not (has_opening_tag and has_closing_tag): |
| 1382 | + context_start = max(0, start_pos - 50) |
| 1383 | + context_end = min(len(log_content), end_pos + 50) |
| 1384 | + context = log_content[context_start:context_end] |
| 1385 | + violations.append( |
| 1386 | + f"Untagged '{pattern}' at position {start_pos}: ...{context}..." |
| 1387 | + ) |
| 1388 | + return violations |
0 commit comments