-
-
Notifications
You must be signed in to change notification settings - Fork 658
[Connectors] Implement health check for connectors #3811
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7884203
eea1f92
ac66a5f
d29f263
f04ebe1
872a67b
264f643
a935c85
30997d7
b33167f
7a58ed2
f769a75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| # This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl | ||
| # See the file 'LICENSE' for copying permission. | ||
|
|
||
| import logging | ||
| from typing import Dict | ||
|
|
||
| import pycti | ||
|
|
@@ -11,6 +12,8 @@ | |
| from api_app.choices import Classification | ||
| from api_app.connectors_manager import classes | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| INTELOWL_OPENCTI_TYPE_MAP = { | ||
| Classification.IP: { | ||
| "v4": "ipv4-addr", | ||
|
|
@@ -180,6 +183,49 @@ def _link_report_entities(self, report_id, observable_id, external_ref_id): | |
| id=report_id, stixObjectOrStixRelationshipId=observable_id | ||
| ) | ||
|
|
||
| def health_check(self, user=None) -> bool: | ||
| if settings.STAGE_CI or settings.MOCK_CONNECTIONS: | ||
| return True | ||
|
|
||
| params = self._config.parameters.annotate_configured(self._config, user).annotate_value_for_user( | ||
| self._config, user | ||
| ) | ||
|
|
||
| url = None | ||
| token = None | ||
| ssl_verify = False | ||
| proxies = None | ||
|
|
||
| for param in params: | ||
| if param.name == "url_key_name": | ||
| url = param.value | ||
| elif param.name == "api_key_name": | ||
| token = param.value | ||
| elif param.name == "ssl_verify": | ||
| ssl_verify = str(param.value).lower() == "true" | ||
| elif param.name == "proxies": | ||
| proxies = param.value | ||
|
|
||
| if not url: | ||
| logger.info("Healthcheck failed: Missing config url") | ||
| return False | ||
| if not token: | ||
| logger.info("Healthcheck failed: Missing config api key") | ||
| return False | ||
|
|
||
| try: | ||
| client = pycti.OpenCTIApiClient(url, token, ssl_verify=ssl_verify, proxies=proxies) | ||
|
|
||
| # pycti has a built-in method (health_check) that | ||
| # returns boolean True/False based on validity of | ||
| # API key and reachability of the OpenCTI instance | ||
| # Ref: https://opencti-python-client.readthedocs.io/en/latest/pycti/pycti.api.opencti_api_client.html#pycti.api.opencti_api_client.OpenCTIApiClient.health_check | ||
| resp = client.health_check() | ||
| return resp | ||
|
Comment on lines
+216
to
+224
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for showing the demo. Very helpful. Is there any chance to modify the toast shown to the user to explain which is the issue? Otherwise it would be difficult for an user to understand the cause. Admins have logs but a classic user can't see them.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also here, please add a comment and a link about what you explained regarding the health check
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yaa I was also thinking about this while doing it that it would better if the cause was visible in the ui itself, I will check and let you know |
||
| except Exception as e: | ||
| logger.info(f"OpenCTI health check failed: {e}") | ||
| return False | ||
|
|
||
| def run(self): | ||
| # Initialize OpenCTI client for this run. | ||
| self.opencti_instance = pycti.OpenCTIApiClient( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,17 @@ | ||
| # This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl | ||
| # See the file 'LICENSE' for copying permission. | ||
|
|
||
| import logging | ||
| from typing import Dict | ||
|
|
||
| import slack_sdk | ||
| from django.conf import settings | ||
| from slack_sdk.errors import SlackApiError | ||
|
|
||
| from api_app.connectors_manager.classes import Connector | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class Slack(Connector): | ||
| _channel: str | ||
|
|
@@ -31,6 +38,37 @@ def body(self) -> str: | |
| f"for <{self._job.url}/raw|{self._job.analyzable.name}>" | ||
| ) | ||
|
|
||
| def health_check(self, user=None) -> bool: | ||
| if settings.STAGE_CI or settings.MOCK_CONNECTIONS: | ||
| return True | ||
|
|
||
| params = self._config.parameters.annotate_configured(self._config, user).annotate_value_for_user( | ||
| self._config, user | ||
| ) | ||
| token = None | ||
| for param in params: | ||
| if param.name == "token": | ||
| token = param.value | ||
| break | ||
|
|
||
| if not token: | ||
| logger.info("Slack health check failed: Missing token configuration.") | ||
| return False | ||
|
|
||
| try: | ||
| client = slack_sdk.WebClient(token=token) | ||
|
|
||
| # slack sdk has a built-in method (auth_test) to | ||
| # test the authentication and connectivity to Slack | ||
| # (auth_test returns identity information of the | ||
| # authenticated user if the token is valid) | ||
| # Ref: https://docs.slack.dev/tools/python-slack-sdk/reference/#slack_sdk.WebClient.auth_test | ||
| client.auth_test() | ||
| return True | ||
|
Comment on lines
+58
to
+67
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Slack:
we had two more params (channel and slack_username) channel just wants a valid channel name on the slack channel and slack_username could be anything (this is just added to the info we send to slack). So I have not added these two to the test + auth_test() requires only the token Official docs here
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, add a comment please |
||
| except Exception as e: | ||
| logger.info(f"Slack health check failed: {e}") | ||
| return False | ||
|
|
||
| def run(self) -> dict: | ||
| self.client.chat_postMessage(text=f"{self.title}\n{self.body}", channel=self._channel, mrkdwn=True) | ||
| return {} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,19 +2,79 @@ | |
| # See the file 'LICENSE' for copying permission. | ||
|
|
||
| import ipaddress | ||
| import logging | ||
|
|
||
| import requests | ||
| from django.conf import settings | ||
|
|
||
| from api_app.connectors_manager import classes | ||
| from api_app.connectors_manager.exceptions import ConnectorRunException | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class YETI(classes.Connector): | ||
| verify_ssl: bool | ||
| _url_key_name: str | ||
| _api_key_name: str | ||
|
|
||
| def health_check(self, user=None) -> bool: | ||
| params = self._config.parameters.annotate_configured(self._config, user).annotate_value_for_user( | ||
| self._config, user | ||
| ) | ||
| url = None | ||
| api_key = None | ||
|
|
||
| for param in params: | ||
| if param.name == "url_key_name": | ||
| url = param.value | ||
| elif param.name == "api_key_name": | ||
| api_key = param.value | ||
|
|
||
| if not url: | ||
| logger.info("Healthcheck failed: Missing config url") | ||
| return False | ||
| if not api_key: | ||
| logger.info("Healthcheck failed: Missing config api key") | ||
| return False | ||
|
|
||
| if settings.STAGE_CI or settings.MOCK_CONNECTIONS: | ||
| return True | ||
|
|
||
| base_url = url.rstrip("/") | ||
| auth_url = f"{base_url}/api/v2/auth/api-token" | ||
|
|
||
| auth_headers = {"x-yeti-apikey": api_key, "User-Agent": "IntelOwl"} | ||
|
|
||
| try: | ||
| verify_ssl = getattr(self, "verify_ssl", False) | ||
|
|
||
| # Posting the API key to YETI's authentication endpoint returns an | ||
| # access token on success (YETI API v2). A valid access token confirms | ||
| # that the API key is valid and the YETI instance is reachable. | ||
| # Ref: https://yeti-platform.io/docs/api/#authentication | ||
| auth_resp = requests.post( | ||
| url=auth_url, | ||
| headers=auth_headers, | ||
| verify=verify_ssl, | ||
| timeout=10, | ||
| ) | ||
| auth_resp.raise_for_status() | ||
| access_token = auth_resp.json().get("access_token") | ||
|
|
||
|
Comment on lines
+56
to
+64
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For yeti: Check this
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here ,a comment in the code about your decision of using the access token and why, just like you did in this github comment |
||
| if access_token: | ||
| return True | ||
| else: | ||
| logger.info(f"Healthcheck failed for {self}: No access token in response.") | ||
| return False | ||
|
|
||
| except requests.RequestException as e: | ||
| logger.info(f"Healthcheck failed: YETI Auth Request failed for {self}. Error: {e}") | ||
| return False | ||
| except Exception as e: | ||
| logger.exception(f"Unexpected error in YETI health_check: {e}") | ||
| return False | ||
|
|
||
| def run(self): | ||
| # get observable value and type | ||
| if self._job.is_sample: | ||
|
|
||









Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MISP:
docs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logs:

missing url
invalid api key

video:
https://github.com/user-attachments/assets/ba622624-eb47-4e9d-9a13-6aedccf6bc14
tests:

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add the information about misp_instance_version as a comment? Otherwise that info would be lost for future maintainers.