This commit is contained in:
phact 2025-09-08 20:52:52 -04:00
parent 8ae543ac69
commit acdfd3c8ca

View file

@ -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