From acdfd3c8ca776945477a9314df89d7eec5ab80d1 Mon Sep 17 00:00:00 2001 From: phact Date: Mon, 8 Sep 2025 20:52:52 -0400 Subject: [PATCH] os diag --- src/tui/screens/diagnostics.py | 206 +++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/src/tui/screens/diagnostics.py b/src/tui/screens/diagnostics.py index 5091654a..3be628f2 100644 --- a/src/tui/screens/diagnostics.py +++ b/src/tui/screens/diagnostics.py @@ -63,6 +63,7 @@ class DiagnosticsScreen(Screen): yield Button("Refresh", variant="primary", id="refresh-btn") yield Button("Check Podman", variant="default", id="check-podman-btn") yield Button("Check Docker", variant="default", id="check-docker-btn") + yield Button("Check OpenSearch Security", variant="default", id="check-opensearch-security-btn") yield Button("Copy to Clipboard", variant="default", id="copy-btn") yield Button("Save to File", variant="default", id="save-btn") yield Button("Back", variant="default", id="back-btn") @@ -92,6 +93,8 @@ class DiagnosticsScreen(Screen): asyncio.create_task(self.check_podman()) elif event.button.id == "check-docker-btn": asyncio.create_task(self.check_docker()) + elif event.button.id == "check-opensearch-security-btn": + asyncio.create_task(self.check_opensearch_security()) elif event.button.id == "copy-btn": self.copy_to_clipboard() elif event.button.id == "save-btn": @@ -415,5 +418,208 @@ class DiagnosticsScreen(Screen): log.write("") + async def check_opensearch_security(self) -> None: + """Run OpenSearch security configuration diagnostics.""" + log = self.query_one("#diagnostics-log", Log) + log.write("[bold green]OpenSearch Security Diagnostics[/bold green]") + + # Get OpenSearch password from environment or prompt user that it's needed + opensearch_password = os.getenv("OPENSEARCH_PASSWORD") + if not opensearch_password: + log.write("[red]OPENSEARCH_PASSWORD environment variable not set[/red]") + log.write("[yellow]Set OPENSEARCH_PASSWORD to test security configuration[/yellow]") + log.write("") + return + + # Test basic authentication + log.write("Testing basic authentication...") + cmd = [ + "curl", "-s", "-k", "-w", "%{http_code}", + "-u", f"admin:{opensearch_password}", + "https://localhost:9200" + ] + process = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await process.communicate() + + if process.returncode == 0: + response = stdout.decode().strip() + # Extract HTTP status code (last 3 characters) + if len(response) >= 3: + status_code = response[-3:] + response_body = response[:-3] + if status_code == "200": + log.write("[green]✓ Basic authentication successful[/green]") + try: + import json + info = json.loads(response_body) + if "version" in info and "distribution" in info["version"]: + log.write(f" OpenSearch version: {info['version']['number']}") + except: + pass + else: + log.write(f"[red]✗ Basic authentication failed with status {status_code}[/red]") + else: + log.write("[red]✗ Unexpected response from OpenSearch[/red]") + else: + log.write(f"[red]✗ Failed to connect to OpenSearch: {stderr.decode().strip()}[/red]") + + # Test security plugin account info + log.write("Testing security plugin account info...") + cmd = [ + "curl", "-s", "-k", "-w", "%{http_code}", + "-u", f"admin:{opensearch_password}", + "https://localhost:9200/_plugins/_security/api/account" + ] + process = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await process.communicate() + + if process.returncode == 0: + response = stdout.decode().strip() + if len(response) >= 3: + status_code = response[-3:] + response_body = response[:-3] + if status_code == "200": + log.write("[green]✓ Security plugin accessible[/green]") + try: + import json + user_info = json.loads(response_body) + if "user_name" in user_info: + log.write(f" Current user: {user_info['user_name']}") + if "roles" in user_info: + log.write(f" Roles: {', '.join(user_info['roles'])}") + if "tenants" in user_info: + tenants = list(user_info['tenants'].keys()) + log.write(f" Tenants: {', '.join(tenants)}") + except: + log.write(" Account info retrieved but couldn't parse JSON") + else: + log.write(f"[red]✗ Security plugin returned status {status_code}[/red]") + else: + log.write(f"[red]✗ Failed to access security plugin: {stderr.decode().strip()}[/red]") + + # Test internal users + log.write("Testing internal users configuration...") + cmd = [ + "curl", "-s", "-k", "-w", "%{http_code}", + "-u", f"admin:{opensearch_password}", + "https://localhost:9200/_plugins/_security/api/internalusers" + ] + process = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await process.communicate() + + if process.returncode == 0: + response = stdout.decode().strip() + if len(response) >= 3: + status_code = response[-3:] + response_body = response[:-3] + if status_code == "200": + try: + import json + users = json.loads(response_body) + if "admin" in users: + log.write("[green]✓ Admin user configured[/green]") + admin_user = users["admin"] + if admin_user.get("reserved"): + log.write(" Admin user is reserved (protected)") + log.write(f" Total internal users: {len(users)}") + except: + log.write("[green]✓ Internal users endpoint accessible[/green]") + else: + log.write(f"[red]✗ Internal users returned status {status_code}[/red]") + else: + log.write(f"[red]✗ Failed to access internal users: {stderr.decode().strip()}[/red]") + + # Test authentication domains configuration + log.write("Testing authentication configuration...") + cmd = [ + "curl", "-s", "-k", "-w", "%{http_code}", + "-u", f"admin:{opensearch_password}", + "https://localhost:9200/_plugins/_security/api/securityconfig" + ] + process = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await process.communicate() + + if process.returncode == 0: + response = stdout.decode().strip() + if len(response) >= 3: + status_code = response[-3:] + response_body = response[:-3] + if status_code == "200": + try: + import json + config = json.loads(response_body) + if "config" in config and "dynamic" in config["config"] and "authc" in config["config"]["dynamic"]: + authc = config["config"]["dynamic"]["authc"] + if "openid_auth_domain" in authc: + log.write("[green]✓ OpenID Connect authentication domain configured[/green]") + oidc_config = authc["openid_auth_domain"].get("http_authenticator", {}).get("config", {}) + if "openid_connect_url" in oidc_config: + log.write(f" OIDC URL: {oidc_config['openid_connect_url']}") + if "subject_key" in oidc_config: + log.write(f" Subject key: {oidc_config['subject_key']}") + if "basic_internal_auth_domain" in authc: + log.write("[green]✓ Basic internal authentication domain configured[/green]") + + # Check for multi-tenancy + if "kibana" in config["config"]["dynamic"]: + kibana_config = config["config"]["dynamic"]["kibana"] + if kibana_config.get("multitenancy_enabled"): + log.write("[green]✓ Multi-tenancy enabled[/green]") + else: + log.write("[yellow]⚠ Authentication configuration not found in expected format[/yellow]") + except Exception as e: + log.write("[green]✓ Security config endpoint accessible[/green]") + log.write(f" (Could not parse JSON: {str(e)[:50]}...)") + else: + log.write(f"[red]✗ Security config returned status {status_code}[/red]") + else: + log.write(f"[red]✗ Failed to access security config: {stderr.decode().strip()}[/red]") + + # Test indices with potential security filtering + log.write("Testing index access...") + cmd = [ + "curl", "-s", "-k", "-w", "%{http_code}", + "-u", f"admin:{opensearch_password}", + "https://localhost:9200/_cat/indices?v" + ] + process = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await process.communicate() + + if process.returncode == 0: + response = stdout.decode().strip() + if len(response) >= 3: + status_code = response[-3:] + response_body = response[:-3] + if status_code == "200": + log.write("[green]✓ Index listing accessible[/green]") + lines = response_body.strip().split('\n') + if len(lines) > 1: # Skip header + indices_found = [] + for line in lines[1:]: + if 'documents' in line: + indices_found.append('documents') + elif 'knowledge_filters' in line: + indices_found.append('knowledge_filters') + elif '.opendistro_security' in line: + indices_found.append('.opendistro_security') + if indices_found: + log.write(f" Key indices found: {', '.join(indices_found)}") + else: + log.write(f"[red]✗ Index listing returned status {status_code}[/red]") + else: + log.write(f"[red]✗ Failed to list indices: {stderr.decode().strip()}[/red]") + + log.write("") + # Made with Bob