From 0cac605979bdd64205435cae0b639ad58cbe8e64 Mon Sep 17 00:00:00 2001 From: ankushy-metron Date: Fri, 21 Aug 2026 11:36:43 +0530 Subject: [PATCH 1/6] splunk 10.4.2 bug fixed --- .../src/utils/setupConfiguration.ts | 43 ++++++- packages/flare_splunk_app/bin/build.js | 2 +- .../splunk/bin/cron_job_ingest_events.py | 31 +++++ .../resources/splunk/bin/flare_sdk_client.py | 8 +- .../main/resources/splunk/bin/flare_ssl.py | 38 ++++++ .../resources/splunk/bin/ingestion_config.py | 50 +++++++- .../resources/splunk/bin/splunk_storage.py | 117 ++++++++++++++++-- 7 files changed, 272 insertions(+), 17 deletions(-) create mode 100644 packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py diff --git a/packages/configuration/src/utils/setupConfiguration.ts b/packages/configuration/src/utils/setupConfiguration.ts index bbc12d9..d07d311 100644 --- a/packages/configuration/src/utils/setupConfiguration.ts +++ b/packages/configuration/src/utils/setupConfiguration.ts @@ -42,6 +42,11 @@ function createService(): Service { return service; } +async function fetchCurrentUsername(service: Service): Promise { + const user = await promisify(service.currentUser)(); + return user.name; +} + export interface ProxyValidationConfig { proxyEnabled?: boolean; proxyType?: string; @@ -236,6 +241,28 @@ async function saveConfiguration( await savePassword(storagePasswords, PasswordKeys.SSL_VERIFY, `${sslVerify}`); await savePassword(storagePasswords, PasswordKeys.INDEX_NAME, indexName); + const currentUsername = await fetchCurrentUsername(service); + console.info('[Flare setup] Saved configuration', { + apiKeyPresent: apiKey.trim().length > 0, + apiKeyLength: apiKey.length, + tenantCount: tenantIds.length, + tenantNameCount: Object.keys(tenantNamesMap).length, + indexName, + ingestionInterval: ingestionInterval ?? '', + numberOfDaysToBackfill: numberOfDaysToBackfill ?? '', + logLevel: logLevel ?? 'INFO', + ingestFullEventData: isIngestingFullEventData, + severityFilterCount: severitiesFilter.length, + sourceTypeFilterCount: sourceTypesFilter.length, + proxyEnabled: proxyEnabled ?? false, + proxyHostPresent: Boolean(proxyHost), + proxyPortPresent: Boolean(proxyPort), + proxyUsernamePresent: Boolean(proxyUsername), + proxyPasswordPresent: Boolean(proxyPassword), + sslVerify, + passAuth: currentUsername, + }); + await fetchIsFirstConfiguration(); const activeInterval = ingestionInterval && ingestionInterval.trim().length > 0 ? ingestionInterval : '60'; @@ -264,11 +291,19 @@ async function saveConfiguration( 'disabled', 'true', ); + const currentPassAuth = await getConfigurationStanzaValue( + service, + 'inputs', + inputsStanza, + 'passAuth', + '', + ); const inputsNeedUpdate = currentIndex !== indexName || currentInterval !== activeInterval || - currentDisabled !== 'false'; + currentDisabled !== 'false' || + currentPassAuth !== currentUsername; if (inputsNeedUpdate) { // Single batched write to inputs.conf — one reload instead of four @@ -276,6 +311,7 @@ async function saveConfiguration( index: indexName, interval: activeInterval, disabled: 'false', + passAuth: currentUsername, }); try { @@ -297,9 +333,8 @@ async function saveConfiguration( } } -// updateEventIngestionCronJobInterval and updatePassAuthUsername -// have been consolidated into the batched inputs.conf update inside -// saveConfiguration() to prevent burst-spawning of script processes. +// Inputs settings, including passAuth for the current Splunk user, are consolidated +// into the batched inputs.conf update inside saveConfiguration(). export async function fetchSslVerify(): Promise { const service = createService(); diff --git a/packages/flare_splunk_app/bin/build.js b/packages/flare_splunk_app/bin/build.js index 9557565..a93398a 100644 --- a/packages/flare_splunk_app/bin/build.js +++ b/packages/flare_splunk_app/bin/build.js @@ -22,7 +22,7 @@ if (!commands.includes(arg)) { const runCommands = { win32: { build: () => shell.exec('set NODE_ENV=production&&.\\node_modules\\.bin\\webpack --mode=production'), - link: () => shell.exec('mklink /D "%SPLUNK_HOME%\\etc\\apps\\flare" "%cd%\\stage"'), + link: () => shell.exec('mklink /D "%SPLUNK_HOME%\\etc\\apps\\flare_splunk_app" "%cd%\\stage"'), }, nix: { build: () => shell.exec('export NODE_ENV=production && ./node_modules/.bin/webpack --mode=production'), diff --git a/packages/flare_splunk_app/src/main/resources/splunk/bin/cron_job_ingest_events.py b/packages/flare_splunk_app/src/main/resources/splunk/bin/cron_job_ingest_events.py index c2e1082..f9ad243 100644 --- a/packages/flare_splunk_app/src/main/resources/splunk/bin/cron_job_ingest_events.py +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/cron_job_ingest_events.py @@ -45,6 +45,18 @@ def main() -> None: ) # 1: Authenticate with Splunk + logger.info( + "[DIAG] runtime: python=%s, cwd=%s, script_dir=%s, SPLUNK_HOME=%s, " + "host=%s, port=%s, app=%s", + sys.version.split()[0], + os.getcwd(), + os.path.dirname(__file__), + os.environ.get("SPLUNK_HOME", ""), + const.HOST, + const.SPLUNK_PORT, + const.APP_NAME, + ) + splunk_session_token = get_session_token_from_stdin() if not splunk_session_token: @@ -52,6 +64,11 @@ def main() -> None: "We couldn't securely identify this session. " "Please make sure the app is fully configured." ) + logger.info( + "[DIAG] Empty session token. This usually means 'passAuth' is not set " + "to a valid Splunk user on the scripted input, or Splunk did not pass " + "a session key on stdin for this run." + ) return storage_passwords = get_storage_passwords(splunk_session_token) @@ -60,12 +77,26 @@ def main() -> None: "We couldn't find your saved app configuration. " "Please visit the setup page to save your credentials." ) + logger.info( + "[DIAG] storage/passwords returned no entries. If the [DIAG] lines " + "above show an SSL/connection error, this is a REST/transport failure " + "(NOT missing config). If the call succeeded with 0 entries, the " + "setup page was never saved under realm '%s'.", + const.STORAGE_REALM, + ) return # 2: Parse all config in one go config = get_all_storage_values(storage_passwords) ingestion_cfg = parse_ingestion_config(config) if ingestion_cfg is None: + logger.info( + "[DIAG] parse_ingestion_config returned None. Recovered config keys=%s " + "(api_key present=%s, tenant_ids present=%s).", + sorted(config.keys()), + bool(config.get(const.KEY_API_KEY)), + bool(config.get(const.KEY_TENANT_IDS)), + ) return # parse_ingestion_config already logged the reason api_key = ingestion_cfg["api_key"] diff --git a/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_sdk_client.py b/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_sdk_client.py index 8fc3498..f7330f1 100644 --- a/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_sdk_client.py +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_sdk_client.py @@ -22,6 +22,7 @@ import requests +from flare_ssl import UnverifiedHTTPAdapter from flareio import FlareApiClient from requests.adapters import HTTPAdapter from typing import Optional @@ -58,7 +59,12 @@ def _build_session( if hasattr(retry, "backoff_max"): retry.backoff_max = 15 - session.mount("https://", HTTPAdapter(max_retries=retry)) + adapter = ( + HTTPAdapter(max_retries=retry) + if ssl_verify + else UnverifiedHTTPAdapter(max_retries=retry) + ) + session.mount("https://", adapter) return session diff --git a/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py b/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py new file mode 100644 index 0000000..7b670e1 --- /dev/null +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py @@ -0,0 +1,38 @@ +"""TLS helpers for requests sessions that skip certificate verification. + +Passing ``verify=False`` to requests leaves urllib3's SSLContext unset, and +urllib3 then falls back to ``SSLContext.load_default_certs()``. On Windows that +reads the OS certificate store, so a single malformed entry aborts the handshake +with "[ASN1: NOT_ENOUGH_DATA]" even though verification was meant to be off. +Splunk's bundled Python 3.9 rejects that entry while 3.13 parses it, which is +why the failure only surfaces in the scheduled input. Supplying an explicit +context keeps verification disabled without ever reading the OS store. +""" + +import ssl + +from requests.adapters import HTTPAdapter +from typing import Any + + +def build_unverified_ssl_context() -> ssl.SSLContext: + """Build an SSLContext with verification disabled that loads no CA store.""" + # ssl.create_default_context() would itself call load_default_certs(), so the + # context is constructed directly. + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + # check_hostname must be cleared before verify_mode, otherwise stdlib raises. + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + return context + + +class UnverifiedHTTPAdapter(HTTPAdapter): + """HTTPAdapter that injects an explicit unverified SSLContext.""" + + def init_poolmanager(self, *args: Any, **kwargs: Any) -> Any: + kwargs["ssl_context"] = build_unverified_ssl_context() + return super().init_poolmanager(*args, **kwargs) + + def proxy_manager_for(self, *args: Any, **kwargs: Any) -> Any: + kwargs["ssl_context"] = build_unverified_ssl_context() + return super().proxy_manager_for(*args, **kwargs) diff --git a/packages/flare_splunk_app/src/main/resources/splunk/bin/ingestion_config.py b/packages/flare_splunk_app/src/main/resources/splunk/bin/ingestion_config.py index 71b346c..8a25dc7 100644 --- a/packages/flare_splunk_app/src/main/resources/splunk/bin/ingestion_config.py +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/ingestion_config.py @@ -44,6 +44,11 @@ def parse_ingestion_config(config: dict) -> Optional[dict]: or None if a critical field (api_key, tenant_ids) is missing. """ + # DIAGNOSTIC: show every key we received (keys only, no secret values). + logger.info( + "[DIAG] parse_ingestion_config received keys=%s", sorted(config.keys()) + ) + # Critical: API Key api_key = config.get(const.KEY_API_KEY) if not api_key: @@ -51,22 +56,46 @@ def parse_ingestion_config(config: dict) -> Optional[dict]: "Configuration has been removed or API key is missing. " "Data ingestion is stopped." ) + logger.info( + "[DIAG] api_key missing/empty. key='%s' present_in_config=%s. " + "Available keys=%s", + const.KEY_API_KEY, + const.KEY_API_KEY in config, + sorted(config.keys()), + ) return None # Critical: Tenant IDs tenant_ids: list = [] tenant_ids_str = config.get(const.KEY_TENANT_IDS) + logger.info( + "[DIAG] tenant_ids raw present=%s, raw_len=%d", + bool(tenant_ids_str), + len(tenant_ids_str or ""), + ) if tenant_ids_str: try: tenant_ids = json.loads(tenant_ids_str) - except Exception: + except Exception as e: logger.warning("We had trouble reading the Tenant IDs from the config.") + logger.info( + "[DIAG] tenant_ids JSON parse failed: error_type=%s, error=%s, " + "raw='%s'", + type(e).__name__, + e, + tenant_ids_str, + ) if not tenant_ids: logger.error( "No Tenant IDs were found. We don't know which environments " "to fetch data for." ) + logger.info( + "[DIAG] tenant_ids empty after parse. parsed_type=%s, value=%r", + type(tenant_ids).__name__, + tenant_ids, + ) return None # Optional: Tenant Names Map @@ -146,6 +175,25 @@ def parse_ingestion_config(config: dict) -> Optional[dict]: ingest_full_event_data, " (Proxy on)" if proxies else "", ) + logger.info( + "[DIAG] parsed config: api_key_present=%s, api_key_len=%d, " + "tenant_count=%d, tenant_name_count=%d, index=%r, " + "full_event_data=%s, severity_filter_count=%d, " + "source_type_filter_count=%d, backfill_days=%d, proxy_enabled=%s, " + "ssl_verify=%s, log_level=%s", + bool(api_key), + len(api_key), + len(tenant_ids), + len(tenant_names_map), + index_name, + ingest_full_event_data, + len(severities_filter), + len(source_types_filter), + backfill_days, + bool(proxies), + ssl_verify, + log_level_str, + ) return { "api_key": api_key, diff --git a/packages/flare_splunk_app/src/main/resources/splunk/bin/splunk_storage.py b/packages/flare_splunk_app/src/main/resources/splunk/bin/splunk_storage.py index edaa10e..1270754 100644 --- a/packages/flare_splunk_app/src/main/resources/splunk/bin/splunk_storage.py +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/splunk_storage.py @@ -1,47 +1,130 @@ import flare_constants as const import logging + import requests as http_requests +from flare_ssl import UnverifiedHTTPAdapter + logger = logging.getLogger("flare_cron_job") +def _local_splunk_session() -> http_requests.Session: + """Build a session for local splunkd REST calls with a pinned TLS context. + + Verification is disabled because splunkd presents a self-signed certificate + on the loopback management port. + """ + session = http_requests.Session() + session.mount("https://", UnverifiedHTTPAdapter()) + return session + + def get_session_token_from_stdin() -> str: """Reads and parses the Splunk session key provided securely via standard input.""" import sys session_key = "" + line_count = 0 for line in sys.stdin: + line_count += 1 session_key = line raw_token_line = session_key.strip() + + # DIAGNOSTIC: show what Splunk handed us on stdin without leaking the token. + logger.info( + "[DIAG] stdin session token: lines_read=%d, raw_len=%d, " + "has_sessionKey_prefix=%s", + line_count, + len(raw_token_line), + raw_token_line.startswith("sessionKey="), + ) + if raw_token_line.startswith("sessionKey="): - return raw_token_line.split("=", 1)[1] - return raw_token_line + token = raw_token_line.split("=", 1)[1] + else: + token = raw_token_line + + logger.info( + "[DIAG] parsed session token present=%s, token_len=%d", + bool(token), + len(token), + ) + return token def get_storage_passwords(token: str) -> list: """Fetch storage/passwords from the local Splunk REST API.""" headers = {"Authorization": f"Splunk {token}"} + url = ( + f"https://{const.HOST}:{const.SPLUNK_PORT}/servicesNS/nobody/" + f"{const.APP_NAME}/storage/passwords?output_mode=json" + ) + + # DIAGNOSTIC: capture exactly which library/endpoint we are using at runtime. + logger.info( + "[DIAG] storage/passwords GET url=%s | requests=%s (%s) | urllib3=%s | " + "token_present=%s token_len=%d", + url, + getattr(http_requests, "__version__", "?"), + getattr(http_requests, "__file__", "?"), + _urllib3_version(), + bool(token), + len(token or ""), + ) + try: - logger.debug("Fetching storage passwords from Splunk REST API") - response = http_requests.get( - f"https://{const.HOST}:{const.SPLUNK_PORT}/servicesNS/nobody/" - f"{const.APP_NAME}/storage/passwords?output_mode=json", + response = _local_splunk_session().get( + url, headers=headers, verify=False, timeout=10, ) + logger.info( + "[DIAG] storage/passwords HTTP status=%s, elapsed=%.3fs, body_len=%d", + response.status_code, + response.elapsed.total_seconds(), + len(response.text or ""), + ) response.raise_for_status() data = response.json() entries = data.get("entry", []) - logger.debug("Retrieved %d storage password entries", len(entries)) + realms = sorted( + { + e.get("content", {}).get("realm") + for e in entries + if e.get("content", {}).get("realm") + } + ) + logger.info( + "[DIAG] storage/passwords parsed entries=%d, realms=%s", + len(entries), + realms, + ) return entries except Exception as e: - logger.error("Failed to fetch storage passwords: %s", e) + # DIAGNOSTIC: full exception type + traceback so SSL vs auth vs network + # failures are distinguishable from the log alone. + logger.error( + "[DIAG] Failed to fetch storage passwords. error_type=%s, error=%s", + type(e).__name__, + e, + exc_info=True, + ) return [] +def _urllib3_version() -> str: + """Best-effort urllib3 version string for diagnostics.""" + try: + import urllib3 + + return getattr(urllib3, "__version__", "?") + except Exception: + return "?" + + def save_storage_password_value( splunk_session_token: str, key: str, value: str ) -> None: @@ -49,10 +132,11 @@ def save_storage_password_value( base_url = f"https://{const.HOST}:{const.SPLUNK_PORT}/servicesNS/nobody/{const.APP_NAME}/storage/passwords" headers = {"Authorization": f"Splunk {splunk_session_token}"} password_id = f"{const.STORAGE_REALM}:{key}:" + session = _local_splunk_session() # Try to delete the old entry first (ignore errors if it doesn't exist) try: - http_requests.delete( + session.delete( f"{base_url}/{password_id}", headers=headers, verify=False, @@ -64,7 +148,7 @@ def save_storage_password_value( # Create the new entry try: - http_requests.post( + session.post( base_url, headers=headers, data={"name": key, "realm": const.STORAGE_REALM, "password": value}, @@ -80,10 +164,23 @@ def save_storage_password_value( def get_all_storage_values(entries: list) -> dict: """Extract all Flare config values from storage passwords in a single pass.""" values: dict = {} + matched_realm = 0 for entry in entries: content = entry.get("content", {}) if content.get("realm") == const.STORAGE_REALM: + matched_realm += 1 key = content.get("username") if key: values[key] = content.get("clear_password") + + # DIAGNOSTIC: show what matched our realm and which config keys we recovered + # (keys only, never the secret values). + logger.info( + "[DIAG] storage values: total_entries=%d, matched_realm(%s)=%d, " + "keys_found=%s", + len(entries), + const.STORAGE_REALM, + matched_realm, + sorted(values.keys()), + ) return values From be0ef56a46b40343e3a858ff5e40c50947ed387b Mon Sep 17 00:00:00 2001 From: ankushy-metron Date: Fri, 21 Aug 2026 19:06:11 +0530 Subject: [PATCH 2/6] Fix scheduled ingest session auth and Splunk Python SSL compatibility --- RELEASE_NOTES.md | 9 +- .../src/utils/setupConfiguration.ts | 20 ----- .../splunk/bin/cron_job_ingest_events.py | 31 ------- .../resources/splunk/bin/ingestion_config.py | 51 +---------- .../resources/splunk/bin/splunk_storage.py | 86 +------------------ 5 files changed, 12 insertions(+), 185 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d203d08..11b19ce 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,13 @@ # Flare -## 1.3.4 +## 1.3.5 + +- Restore `passAuth` on save so the scheduled ingest job receives a Splunk session token. +- Update SSL handling for compatibility with current Splunk Python runtimes. + + 1.3.4 + +--- - Update the minimum version of the Flare SDK. diff --git a/packages/configuration/src/utils/setupConfiguration.ts b/packages/configuration/src/utils/setupConfiguration.ts index d07d311..ff58edd 100644 --- a/packages/configuration/src/utils/setupConfiguration.ts +++ b/packages/configuration/src/utils/setupConfiguration.ts @@ -242,26 +242,6 @@ async function saveConfiguration( await savePassword(storagePasswords, PasswordKeys.INDEX_NAME, indexName); const currentUsername = await fetchCurrentUsername(service); - console.info('[Flare setup] Saved configuration', { - apiKeyPresent: apiKey.trim().length > 0, - apiKeyLength: apiKey.length, - tenantCount: tenantIds.length, - tenantNameCount: Object.keys(tenantNamesMap).length, - indexName, - ingestionInterval: ingestionInterval ?? '', - numberOfDaysToBackfill: numberOfDaysToBackfill ?? '', - logLevel: logLevel ?? 'INFO', - ingestFullEventData: isIngestingFullEventData, - severityFilterCount: severitiesFilter.length, - sourceTypeFilterCount: sourceTypesFilter.length, - proxyEnabled: proxyEnabled ?? false, - proxyHostPresent: Boolean(proxyHost), - proxyPortPresent: Boolean(proxyPort), - proxyUsernamePresent: Boolean(proxyUsername), - proxyPasswordPresent: Boolean(proxyPassword), - sslVerify, - passAuth: currentUsername, - }); await fetchIsFirstConfiguration(); const activeInterval = diff --git a/packages/flare_splunk_app/src/main/resources/splunk/bin/cron_job_ingest_events.py b/packages/flare_splunk_app/src/main/resources/splunk/bin/cron_job_ingest_events.py index f9ad243..c2e1082 100644 --- a/packages/flare_splunk_app/src/main/resources/splunk/bin/cron_job_ingest_events.py +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/cron_job_ingest_events.py @@ -45,18 +45,6 @@ def main() -> None: ) # 1: Authenticate with Splunk - logger.info( - "[DIAG] runtime: python=%s, cwd=%s, script_dir=%s, SPLUNK_HOME=%s, " - "host=%s, port=%s, app=%s", - sys.version.split()[0], - os.getcwd(), - os.path.dirname(__file__), - os.environ.get("SPLUNK_HOME", ""), - const.HOST, - const.SPLUNK_PORT, - const.APP_NAME, - ) - splunk_session_token = get_session_token_from_stdin() if not splunk_session_token: @@ -64,11 +52,6 @@ def main() -> None: "We couldn't securely identify this session. " "Please make sure the app is fully configured." ) - logger.info( - "[DIAG] Empty session token. This usually means 'passAuth' is not set " - "to a valid Splunk user on the scripted input, or Splunk did not pass " - "a session key on stdin for this run." - ) return storage_passwords = get_storage_passwords(splunk_session_token) @@ -77,26 +60,12 @@ def main() -> None: "We couldn't find your saved app configuration. " "Please visit the setup page to save your credentials." ) - logger.info( - "[DIAG] storage/passwords returned no entries. If the [DIAG] lines " - "above show an SSL/connection error, this is a REST/transport failure " - "(NOT missing config). If the call succeeded with 0 entries, the " - "setup page was never saved under realm '%s'.", - const.STORAGE_REALM, - ) return # 2: Parse all config in one go config = get_all_storage_values(storage_passwords) ingestion_cfg = parse_ingestion_config(config) if ingestion_cfg is None: - logger.info( - "[DIAG] parse_ingestion_config returned None. Recovered config keys=%s " - "(api_key present=%s, tenant_ids present=%s).", - sorted(config.keys()), - bool(config.get(const.KEY_API_KEY)), - bool(config.get(const.KEY_TENANT_IDS)), - ) return # parse_ingestion_config already logged the reason api_key = ingestion_cfg["api_key"] diff --git a/packages/flare_splunk_app/src/main/resources/splunk/bin/ingestion_config.py b/packages/flare_splunk_app/src/main/resources/splunk/bin/ingestion_config.py index 8a25dc7..55ff299 100644 --- a/packages/flare_splunk_app/src/main/resources/splunk/bin/ingestion_config.py +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/ingestion_config.py @@ -44,11 +44,6 @@ def parse_ingestion_config(config: dict) -> Optional[dict]: or None if a critical field (api_key, tenant_ids) is missing. """ - # DIAGNOSTIC: show every key we received (keys only, no secret values). - logger.info( - "[DIAG] parse_ingestion_config received keys=%s", sorted(config.keys()) - ) - # Critical: API Key api_key = config.get(const.KEY_API_KEY) if not api_key: @@ -56,46 +51,22 @@ def parse_ingestion_config(config: dict) -> Optional[dict]: "Configuration has been removed or API key is missing. " "Data ingestion is stopped." ) - logger.info( - "[DIAG] api_key missing/empty. key='%s' present_in_config=%s. " - "Available keys=%s", - const.KEY_API_KEY, - const.KEY_API_KEY in config, - sorted(config.keys()), - ) return None # Critical: Tenant IDs tenant_ids: list = [] tenant_ids_str = config.get(const.KEY_TENANT_IDS) - logger.info( - "[DIAG] tenant_ids raw present=%s, raw_len=%d", - bool(tenant_ids_str), - len(tenant_ids_str or ""), - ) if tenant_ids_str: try: tenant_ids = json.loads(tenant_ids_str) - except Exception as e: + except Exception: logger.warning("We had trouble reading the Tenant IDs from the config.") - logger.info( - "[DIAG] tenant_ids JSON parse failed: error_type=%s, error=%s, " - "raw='%s'", - type(e).__name__, - e, - tenant_ids_str, - ) if not tenant_ids: logger.error( "No Tenant IDs were found. We don't know which environments " "to fetch data for." ) - logger.info( - "[DIAG] tenant_ids empty after parse. parsed_type=%s, value=%r", - type(tenant_ids).__name__, - tenant_ids, - ) return None # Optional: Tenant Names Map @@ -175,26 +146,6 @@ def parse_ingestion_config(config: dict) -> Optional[dict]: ingest_full_event_data, " (Proxy on)" if proxies else "", ) - logger.info( - "[DIAG] parsed config: api_key_present=%s, api_key_len=%d, " - "tenant_count=%d, tenant_name_count=%d, index=%r, " - "full_event_data=%s, severity_filter_count=%d, " - "source_type_filter_count=%d, backfill_days=%d, proxy_enabled=%s, " - "ssl_verify=%s, log_level=%s", - bool(api_key), - len(api_key), - len(tenant_ids), - len(tenant_names_map), - index_name, - ingest_full_event_data, - len(severities_filter), - len(source_types_filter), - backfill_days, - bool(proxies), - ssl_verify, - log_level_str, - ) - return { "api_key": api_key, "tenant_ids": tenant_ids, diff --git a/packages/flare_splunk_app/src/main/resources/splunk/bin/splunk_storage.py b/packages/flare_splunk_app/src/main/resources/splunk/bin/splunk_storage.py index 1270754..5038e55 100644 --- a/packages/flare_splunk_app/src/main/resources/splunk/bin/splunk_storage.py +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/splunk_storage.py @@ -25,33 +25,13 @@ def get_session_token_from_stdin() -> str: import sys session_key = "" - line_count = 0 for line in sys.stdin: - line_count += 1 session_key = line raw_token_line = session_key.strip() - - # DIAGNOSTIC: show what Splunk handed us on stdin without leaking the token. - logger.info( - "[DIAG] stdin session token: lines_read=%d, raw_len=%d, " - "has_sessionKey_prefix=%s", - line_count, - len(raw_token_line), - raw_token_line.startswith("sessionKey="), - ) - if raw_token_line.startswith("sessionKey="): - token = raw_token_line.split("=", 1)[1] - else: - token = raw_token_line - - logger.info( - "[DIAG] parsed session token present=%s, token_len=%d", - bool(token), - len(token), - ) - return token + return raw_token_line.split("=", 1)[1] + return raw_token_line def get_storage_passwords(token: str) -> list: @@ -62,18 +42,6 @@ def get_storage_passwords(token: str) -> list: f"{const.APP_NAME}/storage/passwords?output_mode=json" ) - # DIAGNOSTIC: capture exactly which library/endpoint we are using at runtime. - logger.info( - "[DIAG] storage/passwords GET url=%s | requests=%s (%s) | urllib3=%s | " - "token_present=%s token_len=%d", - url, - getattr(http_requests, "__version__", "?"), - getattr(http_requests, "__file__", "?"), - _urllib3_version(), - bool(token), - len(token or ""), - ) - try: response = _local_splunk_session().get( url, @@ -81,50 +49,15 @@ def get_storage_passwords(token: str) -> list: verify=False, timeout=10, ) - logger.info( - "[DIAG] storage/passwords HTTP status=%s, elapsed=%.3fs, body_len=%d", - response.status_code, - response.elapsed.total_seconds(), - len(response.text or ""), - ) response.raise_for_status() data = response.json() entries = data.get("entry", []) - realms = sorted( - { - e.get("content", {}).get("realm") - for e in entries - if e.get("content", {}).get("realm") - } - ) - logger.info( - "[DIAG] storage/passwords parsed entries=%d, realms=%s", - len(entries), - realms, - ) return entries except Exception as e: - # DIAGNOSTIC: full exception type + traceback so SSL vs auth vs network - # failures are distinguishable from the log alone. - logger.error( - "[DIAG] Failed to fetch storage passwords. error_type=%s, error=%s", - type(e).__name__, - e, - exc_info=True, - ) + logger.error("Failed to fetch storage passwords: %s", e) return [] -def _urllib3_version() -> str: - """Best-effort urllib3 version string for diagnostics.""" - try: - import urllib3 - - return getattr(urllib3, "__version__", "?") - except Exception: - return "?" - - def save_storage_password_value( splunk_session_token: str, key: str, value: str ) -> None: @@ -164,23 +97,10 @@ def save_storage_password_value( def get_all_storage_values(entries: list) -> dict: """Extract all Flare config values from storage passwords in a single pass.""" values: dict = {} - matched_realm = 0 for entry in entries: content = entry.get("content", {}) if content.get("realm") == const.STORAGE_REALM: - matched_realm += 1 key = content.get("username") if key: values[key] = content.get("clear_password") - - # DIAGNOSTIC: show what matched our realm and which config keys we recovered - # (keys only, never the secret values). - logger.info( - "[DIAG] storage values: total_entries=%d, matched_realm(%s)=%d, " - "keys_found=%s", - len(entries), - const.STORAGE_REALM, - matched_realm, - sorted(values.keys()), - ) return values From c72b953c2ce2997fef0b04ba9fcd53905141289e Mon Sep 17 00:00:00 2001 From: ankushy-metron Date: Fri, 21 Aug 2026 19:31:22 +0530 Subject: [PATCH 3/6] Updated change log --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9842068..6e3823b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 1.0.1 – 2026‑08‑21 + +- Restore `passAuth` on save so the scheduled ingest job receives a Splunk session token. +- Update SSL handling for compatibility with current Splunk Python runtimes. + ## 1.0.0 – 2026‑05‑28 - Added **tenant filter** support in the Search UI. From e10205f3e05b93ce89f14fbacef9bda7b57a2eb2 Mon Sep 17 00:00:00 2001 From: ankushy-metron Date: Fri, 21 Aug 2026 19:55:20 +0530 Subject: [PATCH 4/6] lint fix for flare_ssl.py --- .../flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py b/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py index 7b670e1..10b108b 100644 --- a/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py @@ -35,4 +35,4 @@ def init_poolmanager(self, *args: Any, **kwargs: Any) -> Any: def proxy_manager_for(self, *args: Any, **kwargs: Any) -> Any: kwargs["ssl_context"] = build_unverified_ssl_context() - return super().proxy_manager_for(*args, **kwargs) + return super().proxy_manager_for(*args, **kwargs) \ No newline at end of file From 5ba6442d74e64da49996411a5797737fc17067c3 Mon Sep 17 00:00:00 2001 From: ankushy-metron Date: Fri, 21 Aug 2026 20:54:00 +0530 Subject: [PATCH 5/6] fix: silence mypy no-untyped-call on UnverifiedHTTPAdapter --- .../src/main/resources/splunk/bin/flare_ssl.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py b/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py index 10b108b..a547a3b 100644 --- a/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py @@ -1,14 +1,3 @@ -"""TLS helpers for requests sessions that skip certificate verification. - -Passing ``verify=False`` to requests leaves urllib3's SSLContext unset, and -urllib3 then falls back to ``SSLContext.load_default_certs()``. On Windows that -reads the OS certificate store, so a single malformed entry aborts the handshake -with "[ASN1: NOT_ENOUGH_DATA]" even though verification was meant to be off. -Splunk's bundled Python 3.9 rejects that entry while 3.13 parses it, which is -why the failure only surfaces in the scheduled input. Supplying an explicit -context keeps verification disabled without ever reading the OS store. -""" - import ssl from requests.adapters import HTTPAdapter @@ -31,8 +20,8 @@ class UnverifiedHTTPAdapter(HTTPAdapter): def init_poolmanager(self, *args: Any, **kwargs: Any) -> Any: kwargs["ssl_context"] = build_unverified_ssl_context() - return super().init_poolmanager(*args, **kwargs) + return super().init_poolmanager(*args, **kwargs) # type: ignore[no-untyped-call, unused-ignore] def proxy_manager_for(self, *args: Any, **kwargs: Any) -> Any: kwargs["ssl_context"] = build_unverified_ssl_context() - return super().proxy_manager_for(*args, **kwargs) \ No newline at end of file + return super().proxy_manager_for(*args, **kwargs) # type: ignore[no-untyped-call, unused-ignore] \ No newline at end of file From 7242baa5fce254cfbd71fd4cf89e64a1b02c76bd Mon Sep 17 00:00:00 2001 From: ankushy-metron Date: Fri, 21 Aug 2026 21:13:34 +0530 Subject: [PATCH 6/6] fix: satisfy ruff format and I001 import order --- .../src/main/resources/splunk/bin/flare_ssl.py | 8 ++++++-- .../src/main/resources/splunk/bin/splunk_storage.py | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py b/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py index a547a3b..259cff5 100644 --- a/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py @@ -20,8 +20,12 @@ class UnverifiedHTTPAdapter(HTTPAdapter): def init_poolmanager(self, *args: Any, **kwargs: Any) -> Any: kwargs["ssl_context"] = build_unverified_ssl_context() - return super().init_poolmanager(*args, **kwargs) # type: ignore[no-untyped-call, unused-ignore] + return super().init_poolmanager( # type: ignore[no-untyped-call, unused-ignore] + *args, **kwargs + ) def proxy_manager_for(self, *args: Any, **kwargs: Any) -> Any: kwargs["ssl_context"] = build_unverified_ssl_context() - return super().proxy_manager_for(*args, **kwargs) # type: ignore[no-untyped-call, unused-ignore] \ No newline at end of file + return super().proxy_manager_for( # type: ignore[no-untyped-call, unused-ignore] + *args, **kwargs + ) diff --git a/packages/flare_splunk_app/src/main/resources/splunk/bin/splunk_storage.py b/packages/flare_splunk_app/src/main/resources/splunk/bin/splunk_storage.py index 5038e55..3210841 100644 --- a/packages/flare_splunk_app/src/main/resources/splunk/bin/splunk_storage.py +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/splunk_storage.py @@ -1,6 +1,5 @@ import flare_constants as const import logging - import requests as http_requests from flare_ssl import UnverifiedHTTPAdapter