splunk / contentctl

Splunk Content Control Tool
Apache License 2.0
91 stars 25 forks source link

Splunkbase download updates courtesy res260 #322

Open pyth0n1c opened 2 weeks ago

pyth0n1c commented 2 weeks ago

This larger PR aggregates a number of great changes courtesy @Res260 . These updates fix broken logic to allow Apps to be downloaded from Splunkbase at contentctl test runtime

Res260 commented 2 weeks ago

There might be one more bug before it works. I currently have a problem with the "infer app path" part of the ansible task inside docker-splunk. I'm not sure what the fix is yet, I'm investigating. I'll post a screenshot tomorrow when I get back on my work computer, if you have an idea.

Res260 commented 2 weeks ago

Here is the error:

image

Res260 commented 2 weeks ago

Found the bug, will include the patch soon

Res260 commented 2 weeks ago

Here is the patch. Sorry again for the manual work, after this PR I'll probably be able to contribute back normally to upstream.

This fixes the error mentioned in the last comment, as well as adds a proper "wait for app installation" that actually checks for app installation instead of assuming that apps are installed once you get a connection to the splunk API (which is not the case).

From cc6c63fe160bb9f76c740a371be3f4ca5af4c3b2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89milio=20Gonzalez?= <little.moon6016@fastmail.com>
Date: Tue, 5 Nov 2024 20:41:59 -0500
Subject: =?UTF-8?q?Fix=20du=20setup=20initial=20+=20t=C3=A9l=C3=A9chargeme?=
 =?UTF-8?q?nt=20depuis=20splunkbase.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../DetectionTestingInfrastructure.py         | 44 +++++++++++++++++--
 ...DetectionTestingInfrastructureContainer.py |  5 +++
 contentctl/objects/config.py                  |  6 +--
 3 files changed, 48 insertions(+), 7 deletions(-)

diff --git a/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py b/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py
index d443ea2..8bb1f1e 100644
--- a/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py
+++ b/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py
@@ -1,3 +1,4 @@
+import logging
 import time
 import uuid
 import abc
@@ -17,12 +18,13 @@ from pydantic import ConfigDict, BaseModel, PrivateAttr, Field, dataclasses
 import requests                                                                                     # type: ignore
 import splunklib.client as client                                                                   # type: ignore
 from splunklib.binding import HTTPError                                                             # type: ignore
+from splunklib.client import Service
 from splunklib.results import JSONResultsReader, Message                                            # type: ignore
 import splunklib.results
 from urllib3 import disable_warnings
 import urllib.parse

-from contentctl.objects.config import test_common, Infrastructure
+from contentctl.objects.config import test_common, Infrastructure, ENTERPRISE_SECURITY_UID
 from contentctl.objects.enums import PostTestBehavior, AnalyticsType
 from contentctl.objects.detection import Detection
 from contentctl.objects.base_test import BaseTest
@@ -42,6 +44,12 @@ from contentctl.actions.detection_testing.progress_bar import (
     TestingStates
 )

+LOG = logging.getLogger(__name__)
+LOG.setLevel(logging.DEBUG)
+handler = logging.StreamHandler()
+handler.setLevel(logging.DEBUG)
+LOG.addHandler(handler)
+

 class SetupTestGroupResults(BaseModel):
     exception: Union[Exception, None] = None
@@ -107,6 +115,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):

     def __init__(self, **data):
         super().__init__(**data)
+        self._conn: Optional[Service] = None

     # TODO: why not use @abstractmethod
     def start(self):
@@ -138,7 +147,8 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
         try:
             for func, msg in [
                 (self.start, "Starting"),
-                (self.get_conn, "Waiting for App Installation"),
+                (self.get_conn, "Getting initial connection"),
+                (self.wait_for_app_installation, "Waiting for App Installation"),
                 (self.configure_conf_file_datamodels, "Configuring Datamodels"),
                 (self.create_replay_index, f"Create index '{self.sync_obj.replay_index}'"),
                 (self.get_all_indexes, "Getting all indexes from server"),
@@ -210,6 +220,29 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
         except Exception as e:
             raise (Exception(f"Failure getting indexes: {str(e)}"))

+    def wait_for_app_installation(self):
+        config_apps = self.global_config.apps
+        installed_config_apps = []
+        while len(installed_config_apps) < len(config_apps):
+            try:
+                # Get apps installed in the Splunk instance
+                splunk_instance_apps = self.get_conn().apps.list()
+
+                # Try to find all the apps we want to be installed (config_apps)
+                installed_config_apps = []
+                for config_app in config_apps:
+                    for splunk_instance_app in splunk_instance_apps:
+                        if config_app.appid == splunk_instance_app.name:
+                            # For Enterprise Security, we need to make sure the app is also configured.
+                            if config_app.uid == ENTERPRISE_SECURITY_UID and splunk_instance_app.content.get('configured') != '1':
+                                continue
+                            installed_config_apps.append(config_app.appid)
+                LOG.debug("Apps in the Splunk instance: " + str(list(map(lambda x: x.name, splunk_instance_apps))))
+                LOG.debug(f"apps in contentctl package found in Splunk instance: {installed_config_apps}")
+            except Exception as e:
+                LOG.exception(e)
+            time.sleep(5)
+
     def get_conn(self) -> client.Service:
         try:
             if not self._conn:
@@ -218,9 +251,11 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
                 # continue trying to re-establish a connection until after
                 # the server has restarted
                 self.connect_to_api()
-        except Exception:
+        except Exception as e:
             # there was some issue getting the connection. Try again just once
+            LOG.exception(e)
             self.connect_to_api()
+
         return self._conn

     def check_for_teardown(self):
@@ -295,7 +330,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
     ):  
         try:
             # Set which roles should be configured. For Enterprise Security/Integration Testing,
-            # we must add some extra foles.
+            # we must add some extra roles.
             if self.global_config.enable_integration_testing:
                 roles = imported_roles + enterprise_security_roles
             else:
@@ -334,6 +369,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
             self.check_for_teardown()
             time.sleep(1)
             try:
                 _ = self.get_conn().get(
                     f"configs/conf-{conf_file_name}", app=app_name
                 )
diff --git a/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py b/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py
index f588703..37326f9 100644
--- a/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py
+++ b/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py
@@ -1,3 +1,5 @@
+import time
+
 from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructure import (
     DetectionTestingInfrastructure,
 )
@@ -25,6 +27,9 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):

         self.container = self.make_container()
         self.container.start()
+        # There might be a small delay between the starting of the container and the binding of the ports for splunk.
+        # To avoid a "connection refused" error, wait a little bit before finishing the method call.
+        time.sleep(20)

     def finish(self):
         if self.container is not None:
diff --git a/contentctl/objects/config.py b/contentctl/objects/config.py
index 4228ba9..1931c15 100644
--- a/contentctl/objects/config.py
+++ b/contentctl/objects/config.py
@@ -31,7 +31,8 @@ from contentctl.helper.splunk_app import SplunkApp
 ENTERPRISE_SECURITY_UID = 263
 COMMON_INFORMATION_MODEL_UID = 1621

-SPLUNKBASE_URL = "https://splunkbase.splunk.com/app/{uid}/release/{version}/download"
+SPLUNKBASE_BASE_URL = "https://splunkbase.splunk.com"
+SPLUNKBASE_URL = SPLUNKBASE_BASE_URL + "/app/{uid}/release/{version}/download"

 # TODO (#266): disable the use_enum_values configuration
@@ -836,7 +837,6 @@ class test(test_common):

     def __init__(self, **kwargs):
         if "SPLUNKBASE_USERNAME" in os.environ:
-            breakpoint()
             kwargs['splunk_api_username'] = os.environ["SPLUNKBASE_USERNAME"]
         if "SPLUNKBASE_PASSWORD" in os.environ:
             kwargs['splunk_api_password'] = os.environ["SPLUNKBASE_PASSWORD"]
@@ -886,7 +886,7 @@ class test(test_common):

         container_paths = []
         for path in paths:
-            if path.startswith(SPLUNKBASE_URL):
+            if path.startswith(SPLUNKBASE_BASE_URL):
                 container_paths.append(path)
             else:
                 container_paths.append((self.getContainerAppDir()/pathlib.Path(path).name).as_posix())
-- 
2.34.1
Res260 commented 1 week ago

This PR should be closed and is replaced by #327