1- import re
1+ import asyncio
22import ssl
33from abc import ABC , abstractmethod
44from json import dumps , loads
55from pathlib import Path
66from typing import Any , cast
77from urllib .parse import urljoin
88
9- import paramiko
109import requests
1110from aiohttp import BasicAuth , ClientSession , TCPConnector
1211from opentelemetry .trace import get_tracer
@@ -183,6 +182,32 @@ def __init__(self, key: str, id: str, revid: str | None, cv: str | None) -> None
183182 self .__cv = cv
184183
185184
185+ class DatabaseStatusResponse :
186+ """
187+ A class representing a database status response from Sync Gateway
188+ """
189+
190+ @property
191+ def db_name (self ) -> str :
192+ """Gets the database name"""
193+ return self .__db_name
194+
195+ @property
196+ def state (self ) -> str :
197+ """Gets the database state ('Online', 'Offline', etc.)"""
198+ return self .__state
199+
200+ @property
201+ def update_seq (self ) -> int :
202+ """Gets the update sequence number"""
203+ return self .__update_seq
204+
205+ def __init__ (self , response : dict ):
206+ self .__db_name = response .get ("db_name" , "" )
207+ self .__state = response .get ("state" , "Unknown" )
208+ self .__update_seq = response .get ("update_seq" , 0 )
209+
210+
186211class AllDocumentsResponse :
187212 """
188213 A class representing an all_docs response from Sync Gateway
@@ -629,20 +654,24 @@ async def put_database(self, db_name: str, payload: PutDatabasePayload) -> None:
629654 """
630655 await self ._put_database (db_name , payload , 0 )
631656
632- async def database_exists (self , db_name : str ) -> bool :
657+ async def get_database_status (self , db_name : str ) -> DatabaseStatusResponse | None :
633658 """
634- Checks if a database exists in Sync Gateway's configuration .
659+ Gets the status of a database including its online/offline state .
635660
636- :param db_name: The name of the Database to check
637- :return: True if the database exists, False otherwise
661+ :param db_name: The name of the Database
662+ :return: DatabaseStatusResponse with state, sequences, etc. Returns None if database doesn't exist (404/403)
638663 """
639- try :
640- await self ._send_request ("get" , f"/{ db_name } " )
641- return True
642- except CblSyncGatewayBadResponseError as e :
643- if e .code == 403 : # Database does not exist
644- return False
645- raise
664+ with self .__tracer .start_as_current_span (
665+ "get_database_status" , attributes = {"cbl.database.name" : db_name }
666+ ):
667+ try :
668+ resp = await self ._send_request ("get" , f"/{ db_name } /" )
669+ assert isinstance (resp , dict )
670+ return DatabaseStatusResponse (cast (dict , resp ))
671+ except CblSyncGatewayBadResponseError as e :
672+ if e .code in [403 , 404 ]: # Database doesn't exist
673+ return None
674+ raise
646675
647676 async def _delete_database (self , db_name : str , retry_count : int = 0 ) -> None :
648677 with self .__tracer .start_as_current_span (
@@ -656,8 +685,6 @@ async def _delete_database(self, db_name: str, retry_count: int = 0) -> None:
656685 f"Sync gateway returned 500 from DELETE database call, retrying ({ retry_count + 1 } )..."
657686 )
658687 current_span .add_event ("SGW returned 500, retry" )
659- import asyncio
660-
661688 await asyncio .sleep (2 )
662689 await self ._delete_database (db_name , retry_count + 1 )
663690 elif e .code == 403 :
@@ -1286,96 +1313,3 @@ async def get_document_revision_public(
12861313 self .__secure , scheme , self .__hostname , 4984 , auth
12871314 ) as session :
12881315 return await self ._send_request ("GET" , path , params = params , session = session )
1289-
1290- async def fetch_log_file (
1291- self ,
1292- log_type : str ,
1293- ssh_key_path : str ,
1294- ssh_username : str = "ec2-user" ,
1295- ) -> str :
1296- """
1297- Fetches a log file from the remote Sync Gateway server via SSH
1298-
1299- :param log_type: The type of log to fetch (e.g., 'debug', 'info', 'error', 'warn')
1300- :param ssh_key_path: Path to SSH private key for authentication
1301- :param ssh_username: SSH username (default: ec2-user)
1302- :return: Contents of the log file as a string
1303- """
1304- # Get log directory from SG configuration
1305- server_config = await self ._send_request ("GET" , "/_config" )
1306- log_dir = server_config .get ("logging" , {}).get (
1307- "log_file_path" , "/home/ec2-user/log"
1308- )
1309- remote_log_path = f"{ log_dir } /sg_{ log_type } .log"
1310-
1311- with self .__tracer .start_as_current_span (
1312- "fetch_log_file" ,
1313- attributes = {
1314- "log.type" : log_type ,
1315- "remote.path" : remote_log_path ,
1316- "ssh.username" : ssh_username ,
1317- },
1318- ):
1319- ssh = paramiko .SSHClient ()
1320- ssh .set_missing_host_key_policy (paramiko .AutoAddPolicy ())
1321-
1322- # Load private key
1323- private_key = paramiko .Ed25519Key .from_private_key_file (ssh_key_path )
1324-
1325- # Connect to the remote server
1326- ssh .connect (
1327- self .__hostname ,
1328- username = ssh_username ,
1329- pkey = private_key ,
1330- )
1331-
1332- # Read the log file
1333- sftp = ssh .open_sftp ()
1334- try :
1335- with sftp .open (remote_log_path , "r" ) as remote_file :
1336- log_contents = remote_file .read ().decode ("utf-8" )
1337- finally :
1338- sftp .close ()
1339- ssh .close ()
1340-
1341- return log_contents
1342-
1343-
1344- def scan_logs_for_untagged_sensitive_data (
1345- log_content : str ,
1346- sensitive_patterns : list [str ],
1347- ) -> list [str ]:
1348- """
1349- Scans log content for sensitive data that is NOT wrapped in <ud>...</ud> tags
1350-
1351- :param log_content: The log file content as a string
1352- :param sensitive_patterns: List of sensitive strings to look for (e.g., doc IDs, usernames)
1353- :return: List of violations found (sensitive data without <ud> tags)
1354- """
1355- violations = []
1356- for pattern in sensitive_patterns :
1357- # Escape special regex characters in the pattern
1358- escaped_pattern = re .escape (pattern )
1359- for match in re .finditer (escaped_pattern , log_content ):
1360- start_pos = match .start ()
1361- end_pos = match .end ()
1362-
1363- # Check if this occurrence is within <ud>...</ud> tags
1364- # Look backwards for <ud> and forwards for </ud>
1365- before_text = log_content [max (0 , start_pos - 100 ) : start_pos ]
1366- after_text = log_content [end_pos : min (len (log_content ), end_pos + 100 )]
1367-
1368- # Check if there's an opening <ud> before and closing </ud> after
1369- has_opening_tag = "<ud>" in before_text and before_text .rfind (
1370- "<ud>"
1371- ) > before_text .rfind ("</ud>" )
1372- has_closing_tag = "</ud>" in after_text
1373-
1374- if not (has_opening_tag and has_closing_tag ):
1375- context_start = max (0 , start_pos - 50 )
1376- context_end = min (len (log_content ), end_pos + 50 )
1377- context = log_content [context_start :context_end ]
1378- violations .append (
1379- f"Untagged '{ pattern } ' at position { start_pos } : ...{ context } ..."
1380- )
1381- return violations
0 commit comments