1
+import time
2
from typing import Any
3
from typing import Dict
4
from typing import Optional
5
+from typing import Tuple
6
7
import requests
8
from fastapi import HTTPException
14
15
HEADERS = {"X-Requested-By": "CoPilot"}
16
17
+# Cache of the detected Graylog major version, keyed by connector name.
18
+# Graylog 7.0 introduced breaking changes to entity-creation POSTs (the
19
+# CreateEntityRequest wrapper) and renamed urlwhitelist -> urlallowlist, so we
20
+# branch on the server's major version. The short TTL lets an in-place Graylog
21
+# upgrade be picked up without restarting CoPilot.
22
+_GRAYLOG_VERSION_CACHE: Dict[str, Tuple[int, float]] = {}
23
+_GRAYLOG_VERSION_TTL_SECONDS = 300
24
+
25
26
async def verify_graylog_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
27
"""
344
"success": False,
345
"message": f"Failed to send PUT request to {endpoint} with error: {e}",
346
}
347
+
348
+
349
+async def get_graylog_major_version(connector_name: Optional[str] = None) -> int:
350
+ """
351
+ Detects the major version of the configured Graylog server.
352
+
353
+ Graylog 7.0 changed several entity-creation requests in a backwards-incompatible
354
+ way (see ``send_post_request_create_entity``). Callers use this to build the
355
+ correct request shape for the running server. The result is cached per connector
356
+ for a short TTL.
357
+
358
+ Falls back to major version ``6`` when the version cannot be determined, so
359
+ existing Graylog 6.x deployments keep working unchanged.
360
+
361
+ Args:
362
+ connector_name (Optional[str]): The connector to probe. Falls back to the
363
+ current context when not provided.
364
+
365
+ Returns:
366
+ int: The detected Graylog major version (e.g. ``6`` or ``7``).
367
+ """
368
+ if connector_name is None:
369
+ connector_name = get_current_graylog_connector()
370
+
371
+ cached = _GRAYLOG_VERSION_CACHE.get(connector_name)
372
+ now = time.monotonic()
373
+ if cached is not None and now - cached[1] < _GRAYLOG_VERSION_TTL_SECONDS:
374
+ return cached[0]
375
+
376
+ major = 6 # safe default — preserves pre-7.x behavior on detection failure
377
+ try:
378
+ response = await send_get_request(endpoint="/api/system", connector_name=connector_name)
379
+ # version looks like "7.0.1+abc123" or "6.1.4"
380
+ version_str = str(response["data"]["version"])
381
+ major = int(version_str.split("+")[0].split(".")[0].strip())
382
+ except Exception as e:
383
+ logger.warning(
384
+ f"Could not determine Graylog version for connector '{connector_name}', " f"defaulting to major version {major}: {e}",
385
+ )
386
+
387
+ _GRAYLOG_VERSION_CACHE[connector_name] = (major, now)
388
+ logger.info(f"Detected Graylog major version {major} for connector '{connector_name}'")
389
+ return major
390
+
391
+
392
+async def send_post_request_create_entity(
393
+ endpoint: str,
394
+ entity: Dict[str, Any],
395
+ share_request: Optional[Dict[str, Any]] = None,
396
+ connector_name: Optional[str] = None,
397
+) -> Dict[str, Any]:
398
+ """
399
+ Sends an entity-creation POST request, adapting the body to the Graylog version.
400
+
401
+ Graylog 7.0 wraps entity-creation payloads in a ``CreateEntityRequest``
402
+ (``{"entity": {...}, "share_request": {...}}``); Graylog 6.x expects the entity
403
+ fields at the top level. Neither version accepts the other's shape, so this
404
+ helper builds the correct body based on the detected server version, letting a
405
+ single call site support both Graylog 6 and Graylog 7.
406
+
407
+ Confirmed affected endpoints: ``POST /api/streams`` and content-pack installation
408
+ (``POST /api/system/content_packs/{id}/{revision}/installations``). Index-set
409
+ creation is NOT wrapped in either version and must keep using
410
+ ``send_post_request`` directly.
411
+
412
+ Args:
413
+ endpoint (str): The entity-creation endpoint.
414
+ entity (Dict[str, Any]): The entity fields (the flat Graylog 6.x body).
415
+ share_request (Optional[Dict[str, Any]]): Optional Graylog 7.x sharing
416
+ settings; omitted when ``None`` (it is nullable server-side).
417
+ connector_name (Optional[str]): The connector to use. Falls back to context.
418
+
419
+ Returns:
420
+ Dict[str, Any]: The response from the POST request.
421
+ """
422
+ major = await get_graylog_major_version(connector_name)
423
+ if major >= 7:
424
+ body: Dict[str, Any] = {"entity": entity}
425
+ if share_request is not None:
426
+ body["share_request"] = share_request
427
+ else:
428
+ body = entity
429
+ return await send_post_request(endpoint=endpoint, data=body, connector_name=connector_name)