Files
Nexus/server/api/search.py
T
Your Name 752a24497c feat: Phase 2 security audit fixes + script execution platform
Security (P0/P1/P2):
- WebSSH: dedicated short-lived token, server_id binding, 4003 close code
- Remove /api/agent/exec from control plane (RCE surface eliminated)
- Global JWT middleware (JwtAuthMiddleware) with install-mode bypass
- decrypt_value: failure returns None, never leaks ciphertext
- Telegram: sanitize_external_message strips sensitive fields
- Agent auth: startup enforces non-empty API key, compare_digest
- Credentials: password_set bool flag, no plaintext in API responses
- audit_log: writes admin_username + ip_address on all CUD ops
- Install lock: all /api/install/* except GET /status return 403 post-install
- WebSocket dedup: publish once, subscribers deliver locally

Script execution platform:
- script_jobs.py / script_job_callback.py: async batching and callback
- script_execution_store.py / script_callback_rate.py: Redis-backed state
- script_execution_flush.py: background flusher to MySQL
- scripts.html / script-executions.html: full execution UI
- agent_url.py: centralised URL builder

Frontend:
- All 13 pages migrated from CDN to /app/vendor/ (no external deps)
- vendor/: alpinejs, tailwindcss-browser, xterm, xterm-addon-*, qrious
- Dashboard WebSocket real-time alerts; 8h JWT session timeout

Tests:
- test_security_unit.py: 15 unit tests (JWT, sanitize, vendor, install 403)
- test_api.py: env-configurable admin credentials

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 15:26:56 +08:00

120 lines
3.9 KiB
Python

"""Nexus — Global Search API Route
Cross-entity search across servers, scripts, credentials, and schedules.
All operations require JWT authentication.
"""
import logging
from fastapi import APIRouter, Depends, Query
from server.api.dependencies import get_db
from server.api.auth_jwt import get_current_admin
from server.domain.models import Admin, Server, Script, DbCredential, PushSchedule
from sqlalchemy import or_
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter(prefix="/api/search", tags=["search"])
logger = logging.getLogger("nexus.search")
def _escape_like(s: str) -> str:
"""Escape LIKE wildcard characters in user input.
Without this, a user searching for ``%`` matches all rows and ``_``
matches any single character — a form of information disclosure.
"""
return s.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
@router.get("/", response_model=dict)
async def global_search(
q: str = Query(..., min_length=1, max_length=100, description="Search query"),
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""Global search across servers, scripts, credentials, and schedules.
Returns top 10 matches per entity type.
"""
from sqlalchemy import select
pattern = f"%{_escape_like(q)}%"
results = {"servers": [], "scripts": [], "credentials": [], "schedules": []}
# Servers
try:
stmt = select(Server).where(
or_(
Server.name.ilike(pattern, escape="\\"),
Server.domain.ilike(pattern, escape="\\"),
Server.description.ilike(pattern, escape="\\"),
Server.category.ilike(pattern, escape="\\"),
)
).limit(10)
res = await db.execute(stmt)
for s in res.scalars().all():
results["servers"].append({
"id": s.id, "name": s.name, "domain": s.domain,
"category": s.category, "is_online": s.is_online,
})
except Exception:
logger.warning("Search failed for 'servers' entity", exc_info=True)
# Scripts
try:
stmt = select(Script).where(
or_(
Script.name.ilike(pattern, escape="\\"),
Script.category.ilike(pattern, escape="\\"),
Script.description.ilike(pattern, escape="\\"),
)
).limit(10)
res = await db.execute(stmt)
for s in res.scalars().all():
results["scripts"].append({
"id": s.id, "name": s.name, "category": s.category,
})
except Exception:
logger.warning("Search failed for 'scripts' entity", exc_info=True)
# Credentials
try:
stmt = select(DbCredential).where(
or_(
DbCredential.name.ilike(pattern, escape="\\"),
DbCredential.host.ilike(pattern, escape="\\"),
)
).limit(10)
res = await db.execute(stmt)
for c in res.scalars().all():
results["credentials"].append({
"id": c.id, "name": c.name, "db_type": c.db_type, "host": c.host,
})
except Exception:
logger.warning("Search failed for 'credentials' entity", exc_info=True)
# Schedules
try:
stmt = select(PushSchedule).where(
or_(
PushSchedule.name.ilike(pattern, escape="\\"),
PushSchedule.source_path.ilike(pattern, escape="\\"),
)
).limit(10)
res = await db.execute(stmt)
for s in res.scalars().all():
results["schedules"].append({
"id": s.id, "name": s.name, "cron_expr": s.cron_expr, "enabled": s.enabled,
})
except Exception:
logger.warning("Search failed for 'schedules' entity", exc_info=True)
# Total count
total = sum(len(v) for v in results.values())
results["total"] = total
results["query"] = q
return results