Skip to content

Commit 65170c3

Browse files
authored
fix: removing outdated code in Kibana client auth (#4495)
* Simplify kibana session management * Drop removed options from `kibana_args` set * Style fix * Patch version bump * Bumping kibana lib version * Relax CLI requirement, making `api_key` optional, to allow `help` to run
1 parent db78756 commit 65170c3

File tree

5 files changed

+59
-142
lines changed

5 files changed

+59
-142
lines changed

detection_rules/misc.py

Lines changed: 13 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,7 @@
1616
import click
1717
import requests
1818

19-
20-
# this is primarily for type hinting - all use of the github client should come from GithubClient class
21-
try:
22-
from github import Github
23-
from github.Repository import Repository
24-
from github.GitRelease import GitRelease
25-
from github.GitReleaseAsset import GitReleaseAsset
26-
except ImportError:
27-
# for type hinting
28-
Github = None # noqa: N806
29-
Repository = None # noqa: N806
30-
GitRelease = None # noqa: N806
31-
GitReleaseAsset = None # noqa: N806
19+
from kibana import Kibana
3220

3321
from .utils import add_params, cached, get_path, load_etc_dump
3422

@@ -348,57 +336,28 @@ def get_elasticsearch_client(cloud_id: str = None, elasticsearch_url: str = None
348336
client_error(error_msg, e, ctx=ctx, err=True)
349337

350338

351-
def get_kibana_client(cloud_id: str, kibana_url: str, kibana_user: str, kibana_password: str, kibana_cookie: str,
352-
space: str, ignore_ssl_errors: bool, provider_type: str, provider_name: str, api_key: str,
353-
**kwargs):
339+
def get_kibana_client(
340+
*,
341+
api_key: str,
342+
cloud_id: str | None = None,
343+
kibana_url: str | None = None,
344+
space: str | None = None,
345+
ignore_ssl_errors: bool = False,
346+
**kwargs
347+
):
354348
"""Get an authenticated Kibana client."""
355-
from requests import HTTPError
356-
from kibana import Kibana
357-
358349
if not (cloud_id or kibana_url):
359350
client_error("Missing required --cloud-id or --kibana-url")
360351

361-
if not (kibana_cookie or api_key):
362-
# don't prompt for these until there's a cloud id or Kibana URL
363-
kibana_user = kibana_user or click.prompt("kibana_user")
364-
kibana_password = kibana_password or click.prompt("kibana_password", hide_input=True)
365-
366352
verify = not ignore_ssl_errors
367-
368-
with Kibana(cloud_id=cloud_id, kibana_url=kibana_url, space=space, verify=verify, **kwargs) as kibana:
369-
if kibana_cookie:
370-
kibana.add_cookie(kibana_cookie)
371-
return kibana
372-
elif api_key:
373-
kibana.add_api_key(api_key)
374-
return kibana
375-
376-
try:
377-
kibana.login(kibana_user, kibana_password, provider_type=provider_type, provider_name=provider_name)
378-
except HTTPError as exc:
379-
if exc.response.status_code == 401:
380-
err_msg = f'Authentication failed for {kibana_url}. If credentials are valid, check --provider-name'
381-
client_error(err_msg, exc, err=True)
382-
else:
383-
raise
384-
385-
return kibana
353+
return Kibana(cloud_id=cloud_id, kibana_url=kibana_url, space=space, verify=verify, api_key=api_key, **kwargs)
386354

387355

388356
client_options = {
389357
'kibana': {
390-
'cloud_id': click.Option(['--cloud-id'], default=getdefault('cloud_id'),
391-
help="ID of the cloud instance."),
392-
'api_key': click.Option(['--api-key'], default=getdefault('api_key')),
393-
'kibana_cookie': click.Option(['--kibana-cookie', '-kc'], default=getdefault('kibana_cookie'),
394-
help='Cookie from an authed session'),
395-
'kibana_password': click.Option(['--kibana-password', '-kp'], default=getdefault('kibana_password')),
396358
'kibana_url': click.Option(['--kibana-url'], default=getdefault('kibana_url')),
397-
'kibana_user': click.Option(['--kibana-user', '-ku'], default=getdefault('kibana_user')),
398-
'provider_type': click.Option(['--provider-type'], default=getdefault('provider_type'),
399-
help="Elastic Cloud providers: basic and saml (for SSO)"),
400-
'provider_name': click.Option(['--provider-name'], default=getdefault('provider_name'),
401-
help="Elastic Cloud providers: cloud-basic and cloud-saml (for SSO)"),
359+
'cloud_id': click.Option(['--cloud-id'], default=getdefault('cloud_id'), help="ID of the cloud instance."),
360+
'api_key': click.Option(['--api-key'], default=getdefault('api_key')),
402361
'space': click.Option(['--space'], default=None, help='Kibana space'),
403362
'ignore_ssl_errors': click.Option(['--ignore-ssl-errors'], default=getdefault('ignore_ssl_errors'))
404363
},

detection_rules/remote_validation.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,7 @@ class RemoteConnector:
4141

4242
def __init__(self, parse_config: bool = False, **kwargs):
4343
es_args = ['cloud_id', 'ignore_ssl_errors', 'elasticsearch_url', 'es_user', 'es_password', 'timeout']
44-
kibana_args = [
45-
'cloud_id', 'ignore_ssl_errors', 'kibana_url', 'kibana_user', 'kibana_password', 'space', 'kibana_cookie',
46-
'provider_type', 'provider_name'
47-
]
44+
kibana_args = ['cloud_id', 'ignore_ssl_errors', 'kibana_url', 'api_key', 'space']
4845

4946
if parse_config:
5047
es_kwargs = {arg: getdefault(arg)() for arg in es_args}
@@ -73,17 +70,25 @@ def auth_es(self, *, cloud_id: Optional[str] = None, ignore_ssl_errors: Optional
7370
es_password=es_password, timeout=timeout, **kwargs)
7471
return self.es_client
7572

76-
def auth_kibana(self, *, cloud_id: Optional[str] = None, ignore_ssl_errors: Optional[bool] = None,
77-
kibana_url: Optional[str] = None, kibana_user: Optional[str] = None,
78-
kibana_password: Optional[str] = None, space: Optional[str] = None,
79-
kibana_cookie: Optional[str] = None, provider_type: Optional[str] = None,
80-
provider_name: Optional[str] = None, **kwargs) -> Kibana:
73+
def auth_kibana(
74+
self,
75+
*,
76+
api_key: str,
77+
cloud_id: str | None = None,
78+
kibana_url: str | None = None,
79+
space: str | None = None,
80+
ignore_ssl_errors: bool = False,
81+
**kwargs
82+
) -> Kibana:
8183
"""Return an authenticated Kibana client."""
82-
self.kibana_client = get_kibana_client(cloud_id=cloud_id, ignore_ssl_errors=ignore_ssl_errors,
83-
kibana_url=kibana_url, kibana_user=kibana_user,
84-
kibana_password=kibana_password, space=space,
85-
kibana_cookie=kibana_cookie, provider_type=provider_type,
86-
provider_name=provider_name, **kwargs)
84+
self.kibana_client = get_kibana_client(
85+
cloud_id=cloud_id,
86+
ignore_ssl_errors=ignore_ssl_errors,
87+
kibana_url=kibana_url,
88+
api_key=api_key,
89+
space=space,
90+
**kwargs
91+
)
8792
return self.kibana_client
8893

8994

lib/kibana/kibana/connector.py

Lines changed: 25 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,30 @@
1212
import uuid
1313
from typing import List, Optional, Union
1414

15-
from urllib.parse import urljoin
1615
import requests
1716
from elasticsearch import Elasticsearch
1817

1918
_context = threading.local()
2019

2120

22-
class Kibana(object):
21+
class Kibana:
2322
"""Wrapper around the Kibana SIEM APIs."""
2423

25-
CACHED = False
26-
27-
def __init__(self, cloud_id=None, kibana_url=None, verify=True, elasticsearch=None, space=None):
24+
def __init__(self, cloud_id=None, kibana_url=None, api_key=None, verify=True, elasticsearch=None, space=None):
2825
""""Open a session to the platform."""
2926
self.authenticated = False
27+
3028
self.session = requests.Session()
3129
self.session.verify = verify
30+
31+
if api_key:
32+
self.session.headers.update(
33+
{
34+
"kbn-xsrf": "true",
35+
"Authorization": f"ApiKey {api_key}",
36+
}
37+
)
38+
3239
self.verify = verify
3340

3441
self.cloud_id = cloud_id
@@ -37,9 +44,6 @@ def __init__(self, cloud_id=None, kibana_url=None, verify=True, elasticsearch=No
3744
self.space = space if space and space.lower() != 'default' else None
3845
self.status = None
3946

40-
self.provider_name = None
41-
self.provider_type = None
42-
4347
if self.cloud_id:
4448
self.cluster_name, cloud_info = self.cloud_id.split(":")
4549
self.domain, self.es_uuid, self.kibana_uuid = \
@@ -50,18 +54,24 @@ def __init__(self, cloud_id=None, kibana_url=None, verify=True, elasticsearch=No
5054

5155
kibana_url_from_cloud = f"https://{self.kibana_uuid}.{self.domain}:9243"
5256
if self.kibana_url and self.kibana_url != kibana_url_from_cloud:
53-
raise ValueError(f'kibana_url provided ({self.kibana_url}) does not match url derived from cloud_id '
54-
f'{kibana_url_from_cloud}')
57+
raise ValueError(
58+
f'kibana_url provided ({self.kibana_url}) does not match url derived from cloud_id '
59+
f'{kibana_url_from_cloud}'
60+
)
5561
self.kibana_url = kibana_url_from_cloud
56-
5762
self.elastic_url = f"https://{self.es_uuid}.{self.domain}:9243"
5863

59-
self.provider_name = 'cloud-basic'
60-
self.provider_type = 'basic'
61-
6264
self.session.headers.update({'Content-Type': "application/json", "kbn-xsrf": str(uuid.uuid4())})
6365
self.elasticsearch = elasticsearch
6466

67+
if not self.elasticsearch and self.elastic_url:
68+
self.elasticsearch = Elasticsearch(
69+
hosts=[self.elastic_url],
70+
api_key=api_key,
71+
verify_certs=self.verify,
72+
)
73+
self.elasticsearch.info()
74+
6575
if not verify:
6676
from requests.packages.urllib3.exceptions import \
6777
InsecureRequestWarning
@@ -75,7 +85,7 @@ def version(self):
7585
return self.status.get("version", {}).get("number")
7686

7787
@staticmethod
78-
def ndjson_file_data_prep(lines: List[dict], filename: str) -> (dict, str):
88+
def ndjson_file_data_prep(lines: List[dict], filename: str) -> tuple[dict, str]:
7989
"""Prepare a request for an ndjson file upload to Kibana."""
8090
data = ('\n'.join(json.dumps(r) for r in lines) + '\n')
8191
boundary = '----JustAnotherBoundary'
@@ -144,63 +154,6 @@ def delete(self, uri, params=None, error=True, **kwargs):
144154
"""Perform an HTTP DELETE."""
145155
return self.request('DELETE', uri, params=params, error=error, **kwargs)
146156

147-
def login(self, kibana_username, kibana_password, provider_type=None, provider_name=None):
148-
"""Authenticate to Kibana using the API to update our cookies."""
149-
payload = {'username': kibana_username, 'password': kibana_password}
150-
path = '/internal/security/login'
151-
152-
try:
153-
self.post(path, data=payload, error=True, verbose=False)
154-
except requests.HTTPError as e:
155-
# 7.10 changed the structure of the auth data
156-
# providers dictated by Kibana configs in:
157-
# https://www.elastic.co/guide/en/kibana/current/security-settings-kb.html#authentication-security-settings
158-
# more details: https://discuss.elastic.co/t/kibana-7-10-login-issues/255201/2
159-
if e.response.status_code == 400 and '[undefined]' in e.response.text:
160-
provider_type = provider_type or self.provider_type or 'basic'
161-
provider_name = provider_name or self.provider_name or 'basic'
162-
163-
payload = {
164-
'params': payload,
165-
'currentURL': '',
166-
'providerType': provider_type,
167-
'providerName': provider_name
168-
}
169-
self.post(path, data=payload, error=True)
170-
else:
171-
raise
172-
173-
# Kibana will authenticate against URLs which contain invalid spaces
174-
if self.space:
175-
self.verify_space(self.space)
176-
177-
self.authenticated = True
178-
self.status = self.get("/api/status")
179-
180-
# create ES and force authentication
181-
if self.elasticsearch is None and self.elastic_url is not None:
182-
self.elasticsearch = Elasticsearch(hosts=[self.elastic_url], http_auth=(kibana_username, kibana_password),
183-
verify_certs=self.verify)
184-
self.elasticsearch.info()
185-
186-
# make chaining easier
187-
return self
188-
189-
def add_cookie(self, cookie):
190-
"""Add cookie to be used for auth (such as from an SSO session)."""
191-
# https://www.elastic.co/guide/en/kibana/7.10/security-settings-kb.html#security-session-and-cookie-settings
192-
self.session.headers['sid'] = cookie
193-
self.session.cookies.set('sid', cookie)
194-
self.status = self.get('/api/status')
195-
self.authenticated = True
196-
197-
def add_api_key(self, api_key: str) -> bool:
198-
"""Add an API key to be used for auth."""
199-
self.session.headers['Authorization'] = f'ApiKey {api_key}'
200-
self.status = self.get('/api/status')
201-
self.authenticated = True
202-
return bool(self.status)
203-
204157
def logout(self):
205158
"""Quit the current session."""
206159
try:

lib/kibana/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "detection-rules-kibana"
3-
version = "0.4.1"
3+
version = "0.4.2"
44
description = "Kibana API utilities for Elastic Detection Rules"
55
license = {text = "Elastic License v2"}
66
keywords = ["Elastic", "Kibana", "Detection Rules", "Security", "Elasticsearch"]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "detection_rules"
3-
version = "0.4.26"
3+
version = "1.0.0"
44
description = "Detection Rules is the home for rules used by Elastic Security. This repository is used for the development, maintenance, testing, validation, and release of rules for Elastic Security’s Detection Engine."
55
readme = "README.md"
66
requires-python = ">=3.12"

0 commit comments

Comments
 (0)