Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
9 changes: 8 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
23 changes: 19 additions & 4 deletions packages/configuration/src/utils/setupConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ function createService(): Service {
return service;
}

async function fetchCurrentUsername(service: Service): Promise<string> {
const user = await promisify(service.currentUser)();
return user.name;
}

export interface ProxyValidationConfig {
proxyEnabled?: boolean;
proxyType?: string;
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -264,18 +271,27 @@ 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
await updateConfigurationFile(service, 'inputs', inputsStanza, {
index: indexName,
interval: activeInterval,
disabled: 'false',
passAuth: currentUsername,
});

try {
Expand All @@ -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<boolean> {
const service = createService();
Expand Down
2 changes: 1 addition & 1 deletion packages/flare_splunk_app/bin/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import requests

from flare_ssl import UnverifiedHTTPAdapter
from flareio import FlareApiClient
from requests.adapters import HTTPAdapter
from typing import Optional
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,19 +36,21 @@ 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,
)
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)
Expand All @@ -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,
Expand All @@ -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},
Expand Down
Loading