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. 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 bbc12d9..ff58edd 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,8 @@ async function saveConfiguration( await savePassword(storagePasswords, PasswordKeys.SSL_VERIFY, `${sslVerify}`); await savePassword(storagePasswords, PasswordKeys.INDEX_NAME, indexName); + const currentUsername = await fetchCurrentUsername(service); + await fetchIsFirstConfiguration(); const activeInterval = ingestionInterval && ingestionInterval.trim().length > 0 ? ingestionInterval : '60'; @@ -264,11 +271,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 +291,7 @@ async function saveConfiguration( index: indexName, interval: activeInterval, disabled: 'false', + passAuth: currentUsername, }); try { @@ -297,9 +313,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/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..259cff5 --- /dev/null +++ b/packages/flare_splunk_app/src/main/resources/splunk/bin/flare_ssl.py @@ -0,0 +1,31 @@ +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( # 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( # type: ignore[no-untyped-call, unused-ignore] + *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..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 @@ -146,7 +146,6 @@ def parse_ingestion_config(config: dict) -> Optional[dict]: ingest_full_event_data, " (Proxy on)" if proxies else "", ) - 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 edaa10e..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 @@ -2,10 +2,23 @@ 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 @@ -23,11 +36,14 @@ def get_session_token_from_stdin() -> str: 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" + ) + 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, @@ -35,7 +51,6 @@ def get_storage_passwords(token: str) -> list: response.raise_for_status() data = response.json() entries = data.get("entry", []) - logger.debug("Retrieved %d storage password entries", len(entries)) return entries except Exception as e: logger.error("Failed to fetch storage passwords: %s", e) @@ -49,10 +64,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 +80,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},