feat(splunk): add Splunk Enterprise and Cloud integration (#6743)

Adds a Splunk block with 12 REST operations: run search (oneshot), create/get/cancel search job, get search results, list/get/dispatch saved searches, list/get fired alerts, list indexes, and list apps. Bearer-token or basic auth, with optional /servicesNS namespace scoping.

Every tool was validated against the Splunk REST reference. Results use search/v2/jobs/{sid}/results because the v1 endpoint is deprecated and disabled from Splunk Enterprise 9.0.1. A half-specified namespace fills the missing node with the documented - wildcard rather than nobody/search, which would have hidden user-private objects. Dispatching endpoints fail loudly instead of reporting success with a null sid, and Create Search Job rejects exec_mode=oneshot since that mode returns results rather than a search ID. The results and control endpoints tolerate an empty body. saved/searches sends the f field filter the reference prescribes for it.
This commit is contained in:
Waleed
2026-08-15 17:07:59 -07:00
committed by GitHub
parent 611df8b8a1
commit 852906ec91
31 changed files with 3651 additions and 3 deletions
+27
View File
@@ -8726,6 +8726,33 @@ export function BigQueryIcon(props: SVGProps<SVGSVGElement>) {
)
}
/**
* Splunk wordmark. The lettering is drawn with `currentColor` so it stays legible
* bare in both light and dark mode; the chevron keeps its brand green.
*/
export function SplunkIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox='0 0 48 48'
fillRule='evenodd'
clipRule='evenodd'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path
fill='currentColor'
d='M13.432 18L13.5 28 15 28 15 18zM32 18v10.005h1.805c0 0 .015-3.609.035-3.608.019.001 2.69 3.743 2.716 3.733.027-.01 1.599-.576 1.599-.59 0-.014-2.684-3.441-2.684-3.448 0-.006 2.251-2.509 2.231-2.529-.06-.06-1.281-.586-1.314-.566-.018.011-2.582 2.796-2.582 2.796V18H32zM2.099 21.05c-.651.137-1.259.552-1.509 1.031-.23.44-.227 1.043.007 1.491.16.305.625.735 1.348 1.246.782.552 1.078.832 1.153 1.092.111.385-.06.752-.434.931-.187.089-.27.101-.603.086-.433-.02-.729-.132-1.172-.446-.14-.099-.269-.167-.285-.152-.016.016-.155.224-.311.466L.01 27.23l.109.081c.06.045.248.158.422.255 1.187.66 2.944.555 3.835-.229.475-.418.697-1.025.602-1.647-.089-.582-.409-.965-1.417-1.695-.791-.573-1.166-.889-1.253-1.059-.167-.326-.049-.643.307-.827.178-.092.251-.102.608-.087.347.015.453.041.741.179l.335.161.263-.41c.144-.224.252-.426.239-.445-.048-.071-.598-.299-.941-.391C3.402 20.995 2.521 20.961 2.099 21.05zM10.031 21.935c.788.369 1.212 1.528 1.041 2.852-.06.467-.227 1.032-.383 1.296-.159.268-.463.56-.705.675-.444.212-1.107.13-1.523-.187-.577-.442-.893-1.524-.772-2.649.111-1.032.483-1.684 1.133-1.986C9.156 21.781 9.7 21.781 10.031 21.935zM9.313 20.784c-.472.106-.85.319-1.254.705l-.366.35V21H6.814 6v5 5h1.693v-4.08l.324.319c.592.583 1.287.814 2.101.701.532-.074.904-.21 1.3-.475 1.111-.743 1.721-2.221 1.554-3.771-.158-1.467-.912-2.484-2.128-2.871C10.531 20.724 9.679 20.702 9.313 20.784zM16.258 21c0 0 .055 5.284.091 5.438.136.583.525 1.137.964 1.374.403.218.727.287 1.333.284.861-.004 1.386-.211 2.027-.8L21 27v1h1.742l-.023-7H21c0 0 .025 4.347 0 4.5-.1.611-.573 1.109-1.053 1.27-.287.096-.786.174-1.086.127C18.549 26.848 18.033 26.507 18 26v-4.977L16.258 21zM30.742 28c0 0-.055-5.284-.091-5.438-.136-.583-.525-1.137-.964-1.374-.403-.218-.727-.287-1.333-.284-.861.004-1.386.211-2.027.8L26 22v-1h-1.742l.023 7H26c0 0-.025-4.347 0-4.5.1-.611.573-1.109 1.053-1.27.287-.096.661-.17 1.086-.127C28.566 22.147 29.031 22.535 29 23v4.977L30.742 28z'
/>
<path
fill='#77b539'
fillRule='evenodd'
clipRule='evenodd'
d='M40 20.5L40 22 46 24 40 26.601 40 28 47 25 47 23z'
/>
</svg>
)
}
export function SnowflakeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox='0 0 146.36 139.16' xmlns='http://www.w3.org/2000/svg' {...props}>
+2
View File
@@ -216,6 +216,7 @@ import {
SmartleadIcon,
SmtpIcon,
SnowflakeIcon,
SplunkIcon,
SportmonksIcon,
SQSIcon,
SquareIcon,
@@ -507,6 +508,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
smartlead: SmartleadIcon,
smtp: SmtpIcon,
snowflake: SnowflakeIcon,
splunk: SplunkIcon,
sportmonks: SportmonksIcon,
sqs: SQSIcon,
square: SquareIcon,
@@ -230,6 +230,7 @@
"smtp",
"snowflake",
"snowflake-service-account",
"splunk",
"sportmonks",
"sqs",
"square",
@@ -0,0 +1,437 @@
---
title: Splunk
description: Run SPL searches and manage saved searches and alerts in Splunk
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="splunk"
color="#FFFFFF"
/>
## Usage Instructions
Integrate Splunk Enterprise or Splunk Cloud into workflows. Run SPL searches synchronously or as asynchronous jobs, fetch results, dispatch saved searches, and inspect fired alerts and indexes.
## Actions
### Splunk Run Search
Run an SPL search synchronously and return its results in a single call (oneshot mode). Use for short searches; use Create Search Job for long-running ones.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `search` | string | Yes | SPL search string \(e.g. index=main error \| stats count by host\). The leading "search" command is added automatically when omitted. |
| `earliestTime` | string | No | Earliest \(inclusive\) time bound — relative \(e.g. -24h, -7d@d\) or absolute epoch/formatted time |
| `latestTime` | string | No | Latest \(exclusive\) time bound — relative \(e.g. now\) or absolute time |
| `adhocSearchLevel` | string | No | Search mode: verbose, fast, or smart. Defaults to fast. |
| `autoCancel` | number | No | Cancel the search after this many seconds of inactivity \(e.g. 60\). 0 never auto-cancels. |
| `maxCount` | number | No | Maximum number of results the search stores and returns. Defaults to 10000. Lower it to bound large oneshot responses. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search result rows, each holding the fields the search produced |
| `resultCount` | number | Number of result rows returned |
| `preview` | boolean | Whether the results are previews |
| `initOffset` | number | Offset of the first returned row |
| `messages` | json | Messages returned with the response \(\[\{type, text\}\]\) |
| `sid` | string | Search ID of the job |
| `label` | string | Custom name of the search job |
| `dispatchState` | string | Current state of the search job |
| `doneProgress` | number | Approximate job progress between 0 and 1 |
| `isDone` | boolean | Whether the search has completed |
| `isFailed` | boolean | Whether the search failed |
| `isFinalized` | boolean | Whether the search was finalized |
| `isPaused` | boolean | Whether the search is paused |
| `isZombie` | boolean | Whether the search process died |
| `isSaved` | boolean | Whether the job artifacts are saved |
| `isSavedSearch` | boolean | Whether the job came from a saved search |
| `isRealTimeSearch` | boolean | Whether this is a real-time search |
| `eventCount` | number | Number of events returned |
| `eventAvailableCount` | number | Number of events available for export |
| `eventFieldCount` | number | Number of fields found in the results |
| `resultPreviewCount` | number | Number of rows in the latest preview |
| `scanCount` | number | Number of events scanned off disk |
| `runDuration` | number | Seconds the search took to complete |
| `priority` | number | Search priority between 0 and 10 |
| `earliestTime` | string | Earliest time bound of the job |
| `latestTime` | string | Latest time bound of the job |
| `searchEarliestTime` | string | Earliest time as specified in the search command |
| `searchLatestTime` | string | Latest time as specified in the search command |
| `savedSearches` | json | Saved searches \(\[\{name, id, author, updated, search, description, disabled, isScheduled, cronSchedule, alertType\}\]\) |
| `name` | string | Saved search name |
| `id` | string | Fully qualified REST URI of the resource |
| `author` | string | Owner of the saved search |
| `updated` | string | Last update timestamp |
| `search` | string | SPL the saved search runs |
| `qualifiedSearch` | string | Exact search string the scheduler runs |
| `description` | string | Saved search description |
| `disabled` | boolean | Whether the saved search is disabled |
| `isScheduled` | boolean | Whether the search runs on a schedule |
| `isVisible` | boolean | Whether the search is listed as visible |
| `cronSchedule` | string | Cron schedule for the search |
| `nextScheduledTime` | string | Next scheduled run time |
| `alertType` | string | Alert condition type |
| `dispatchEarliestTime` | string | Earliest time used when dispatching |
| `dispatchLatestTime` | string | Latest time used when dispatching |
| `alerts` | json | Saved searches with currently triggered alerts \(\[\{name, id, updated, triggeredAlertCount\}\]\) |
| `firedAlerts` | json | Triggered instances of an alert \(\[\{name, savedSearchName, alertType, severity, sid, triggerTime\}\]\) |
| `indexes` | json | Indexes configured on the instance \(\[\{name, datatype, disabled, totalEventCount, currentDBSizeMB, maxTotalDataSizeMB, minTime, maxTime\}\]\) |
| `apps` | json | Apps installed on the instance \(name, label, version, author, disabled\) |
### Splunk Create Search Job
Start a Splunk search job and return its search ID (sid). The search runs asynchronously — poll its status and fetch results separately.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `search` | string | Yes | SPL search string \(e.g. index=main sourcetype=access_combined \| timechart count\). The leading "search" command is added automatically when omitted. |
| `earliestTime` | string | No | Earliest \(inclusive\) time bound — relative \(e.g. -24h\) or absolute time |
| `latestTime` | string | No | Latest \(exclusive\) time bound — relative \(e.g. now\) or absolute time |
| `execMode` | string | No | Execution mode: normal \(returns the sid immediately\) or blocking \(returns the sid once the job completes\). Defaults to normal. oneshot is rejected here because it returns results instead of a sid — use Splunk Run Search for that. |
| `adhocSearchLevel` | string | No | Search mode: verbose, fast, or smart. Defaults to fast. |
| `searchId` | string | No | Custom search ID to assign to the job. A random ID is generated when omitted. |
| `indexEarliest` | string | No | Earliest \(inclusive\) time bound based on index time rather than event time |
| `indexLatest` | string | No | Latest \(exclusive\) time bound based on index time rather than event time |
| `enableLookups` | boolean | No | Whether lookups are applied to events. Defaults to true. |
| `allowPartialResults` | boolean | No | Whether the job may return partial results when a search peer fails. Defaults to true. |
| `autoCancel` | number | No | Cancel the job after this many seconds of inactivity \(e.g. 300\). 0 never auto-cancels. |
| `maxCount` | number | No | Number of events accessible in any given status bucket, and in transforming mode the maximum number of results to store. Defaults to 10000. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `sid` | string | Search ID of the created job, used to poll status and fetch results |
### Splunk Get Search Job
Get the status and progress of a Splunk search job by search ID, including dispatch state, completion progress, and event/result counts.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `sid` | string | Yes | Search ID of the job to inspect \(e.g. 1457683115.100\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `sid` | string | Search ID of the job |
| `label` | string | Custom name created for this search |
| `dispatchState` | string | Job state: QUEUED, PARSING, RUNNING, FINALIZING, PAUSE, INTERNAL_CANCEL, USER_CANCEL, BAD_INPUT_CANCEL, QUIT, FAILED, or DONE |
| `doneProgress` | number | Approximate progress between 0 and 1.0 |
| `isDone` | boolean | Whether the search has completed |
| `isFailed` | boolean | Whether a fatal error occurred running the search |
| `isFinalized` | boolean | Whether the search was finalized \(stopped before completion\) |
| `isPaused` | boolean | Whether the search is paused |
| `isZombie` | boolean | Whether the search process died before the search finished |
| `isSaved` | boolean | Whether the search job artifacts are saved to disk |
| `isSavedSearch` | boolean | Whether this is a saved search run by the scheduler |
| `isRealTimeSearch` | boolean | Whether this is a real-time search |
| `eventCount` | number | Number of events returned by the search |
| `eventAvailableCount` | number | Number of events available for export |
| `eventFieldCount` | number | Number of fields found in the search results |
| `resultCount` | number | Total number of results returned by the search |
| `resultPreviewCount` | number | Number of result rows in the latest preview results |
| `scanCount` | number | Number of events scanned or read off disk |
| `runDuration` | number | Time in seconds the search took to complete |
| `priority` | number | Search priority between 0 and 10 |
| `earliestTime` | string | Earliest \(inclusive\) time bound for the search |
| `latestTime` | string | Latest \(exclusive\) time bound for the search |
| `searchEarliestTime` | string | Earliest time as specified in the search command itself |
| `searchLatestTime` | string | Latest time as specified in the search command itself |
| `messages` | json | Errors and debug messages recorded for the job |
### Splunk Get Search Results
Fetch the transformed results of a completed Splunk search job by search ID, with pagination.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `sid` | string | Yes | Search ID of the job whose results to fetch \(e.g. 1457683115.100\) |
| `count` | number | No | Maximum number of result rows to return. Defaults to 100. Page through larger result sets with offset rather than raising this — a completed job can hold millions of rows. |
| `offset` | number | No | First result row \(0-indexed\) from which to begin returning data |
| `fields` | string | No | Comma-separated list of fields to return for each row \(e.g. _time,host,source\). Returns all fields when omitted. |
| `addSummaryToMetadata` | boolean | No | Include field summary statistics in the response |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search result rows, each holding the fields the search produced |
| `resultCount` | number | Number of result rows returned |
| `preview` | boolean | Whether the results are previews |
| `initOffset` | number | Offset of the first returned row |
| `messages` | json | Messages returned with the response \(\[\{type, text\}\]\) |
| `sid` | string | Search ID of the job |
| `label` | string | Custom name of the search job |
| `dispatchState` | string | Current state of the search job |
| `doneProgress` | number | Approximate job progress between 0 and 1 |
| `isDone` | boolean | Whether the search has completed |
| `isFailed` | boolean | Whether the search failed |
| `isFinalized` | boolean | Whether the search was finalized |
| `isPaused` | boolean | Whether the search is paused |
| `isZombie` | boolean | Whether the search process died |
| `isSaved` | boolean | Whether the job artifacts are saved |
| `isSavedSearch` | boolean | Whether the job came from a saved search |
| `isRealTimeSearch` | boolean | Whether this is a real-time search |
| `eventCount` | number | Number of events returned |
| `eventAvailableCount` | number | Number of events available for export |
| `eventFieldCount` | number | Number of fields found in the results |
| `resultPreviewCount` | number | Number of rows in the latest preview |
| `scanCount` | number | Number of events scanned off disk |
| `runDuration` | number | Seconds the search took to complete |
| `priority` | number | Search priority between 0 and 10 |
| `earliestTime` | string | Earliest time bound of the job |
| `latestTime` | string | Latest time bound of the job |
| `searchEarliestTime` | string | Earliest time as specified in the search command |
| `searchLatestTime` | string | Latest time as specified in the search command |
| `savedSearches` | json | Saved searches \(\[\{name, id, author, updated, search, description, disabled, isScheduled, cronSchedule, alertType\}\]\) |
| `name` | string | Saved search name |
| `id` | string | Fully qualified REST URI of the resource |
| `author` | string | Owner of the saved search |
| `updated` | string | Last update timestamp |
| `search` | string | SPL the saved search runs |
| `qualifiedSearch` | string | Exact search string the scheduler runs |
| `description` | string | Saved search description |
| `disabled` | boolean | Whether the saved search is disabled |
| `isScheduled` | boolean | Whether the search runs on a schedule |
| `isVisible` | boolean | Whether the search is listed as visible |
| `cronSchedule` | string | Cron schedule for the search |
| `nextScheduledTime` | string | Next scheduled run time |
| `alertType` | string | Alert condition type |
| `dispatchEarliestTime` | string | Earliest time used when dispatching |
| `dispatchLatestTime` | string | Latest time used when dispatching |
| `alerts` | json | Saved searches with currently triggered alerts \(\[\{name, id, updated, triggeredAlertCount\}\]\) |
| `firedAlerts` | json | Triggered instances of an alert \(\[\{name, savedSearchName, alertType, severity, sid, triggerTime\}\]\) |
| `indexes` | json | Indexes configured on the instance \(\[\{name, datatype, disabled, totalEventCount, currentDBSizeMB, maxTotalDataSizeMB, minTime, maxTime\}\]\) |
| `apps` | json | Apps installed on the instance \(name, label, version, author, disabled\) |
### Splunk Cancel Search Job
Cancel a running Splunk search job and delete its result cache.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `sid` | string | Yes | Search ID of the job to cancel \(e.g. 1457683115.100\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `sid` | string | Search ID of the cancelled job |
### Splunk List Saved Searches
List saved searches and reports configured in Splunk, including their SPL, schedule, and alert configuration.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `search` | string | No | Filter saved searches. A bare term matches as a substring across fields \(e.g. Errors\); field_name=field_value matches one field \(e.g. is_scheduled=1\). |
| `count` | number | No | Maximum number of saved searches to return \(e.g. 50\). 0 returns all. |
| `offset` | number | No | Index of the first saved search to return, for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `savedSearches` | array | Saved searches configured in Splunk |
| ↳ `name` | string | Saved search name |
| ↳ `id` | string | Fully qualified REST URI of the saved search |
| ↳ `author` | string | Owner of the saved search |
| ↳ `updated` | string | Last update timestamp |
| ↳ `search` | string | The SPL the saved search runs |
| ↳ `qualifiedSearch` | string | The exact search string the scheduler runs |
| ↳ `description` | string | Saved search description |
| ↳ `disabled` | boolean | Whether the saved search is disabled |
| ↳ `isScheduled` | boolean | Whether the search runs on a schedule |
| ↳ `isVisible` | boolean | Whether the search appears in the visible saved search list |
| ↳ `cronSchedule` | string | Cron schedule for the search |
| ↳ `nextScheduledTime` | string | Time the scheduler runs this search again |
| ↳ `alertType` | string | Alert condition type \(e.g. always, custom, number of events\) |
| ↳ `dispatchEarliestTime` | string | Earliest time bound used when the search is dispatched |
| ↳ `dispatchLatestTime` | string | Latest time bound used when the search is dispatched |
### Splunk Get Saved Search
Get the configuration of a single Splunk saved search by name, including its SPL, schedule, and alert settings.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `name` | string | Yes | Name of the saved search \(e.g. Errors in the last 24 hours\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `name` | string | Saved search name |
| `id` | string | Fully qualified REST URI of the saved search |
| `author` | string | Owner of the saved search |
| `updated` | string | Last update timestamp |
| `search` | string | The SPL the saved search runs |
| `qualifiedSearch` | string | The exact search string the scheduler runs |
| `description` | string | Saved search description |
| `disabled` | boolean | Whether the saved search is disabled |
| `isScheduled` | boolean | Whether the search runs on a schedule |
| `isVisible` | boolean | Whether the search appears in the visible saved search list |
| `cronSchedule` | string | Cron schedule for the search |
| `nextScheduledTime` | string | Time the scheduler runs this search again |
| `alertType` | string | Alert condition type \(e.g. always, custom, number of events\) |
| `dispatchEarliestTime` | string | Earliest time bound used when the search is dispatched |
| `dispatchLatestTime` | string | Latest time bound used when the search is dispatched |
### Splunk Dispatch Saved Search
Run a Splunk saved search immediately and return the search ID (sid) of the dispatched job.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `name` | string | Yes | Name of the saved search to run \(e.g. Errors in the last 24 hours\) |
| `triggerActions` | boolean | No | Whether to trigger the saved search alert actions on this run |
| `dispatchEarliestTime` | string | No | Override the earliest time bound for this run — relative \(e.g. -24h\) or absolute time |
| `dispatchLatestTime` | string | No | Override the latest time bound for this run — relative \(e.g. now\) or absolute time |
| `dispatchMaxCount` | number | No | Maximum number of results before the search is finalized \(e.g. 10000\) |
| `dispatchMaxTime` | number | No | Maximum number of seconds before the search is finalized \(e.g. 300\) |
| `dispatchTtl` | number | No | Time to live in seconds for the search artifacts when no actions are triggered \(e.g. 600\) |
| `forceDispatch` | boolean | No | Start a new search even when another instance of this saved search is already running |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `sid` | string | Search ID of the dispatched job, used to poll status and fetch results |
### Splunk List Fired Alerts
List the saved searches with currently triggered (unexpired) Splunk alerts and how many times each has fired.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `count` | number | No | Maximum number of entries to return \(e.g. 50\). 0 returns all. |
| `offset` | number | No | Index of the first entry to return, for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `alerts` | array | Saved searches with currently triggered alerts |
| ↳ `name` | string | Name of the alerting saved search |
| ↳ `id` | string | Fully qualified REST URI of the entry |
| ↳ `updated` | string | Last update timestamp |
| ↳ `triggeredAlertCount` | number | Trigger count for this alert |
### Splunk Get Fired Alerts
List the unexpired triggered instances of a Splunk alert by saved search name, including severity, trigger time, and the search ID of each firing.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `name` | string | Yes | Name of the alerting saved search \(e.g. Errors in the last 24 hours\). Use - to return the fired alerts of every saved search. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `firedAlerts` | array | Unexpired triggered instances of the alert |
| ↳ `name` | string | Name of the fired alert entry |
| ↳ `id` | string | Fully qualified REST URI of the entry |
| ↳ `updated` | string | Last update timestamp |
| ↳ `savedSearchName` | string | Name of the saved search that triggered the alert |
| ↳ `alertType` | string | Whether the alert was historical or real-time |
| ↳ `severity` | number | Severity level of the alert |
| ↳ `sid` | string | Search ID of the search that triggered the alert |
| ↳ `triggerTime` | number | Time the alert was triggered |
| ↳ `triggerTimeRendered` | string | Human-readable time the alert was triggered |
| ↳ `expirationTimeRendered` | string | Human-readable time this triggered alert record expires |
| ↳ `triggeredAlerts` | number | Number of alerts included in this triggered instance |
| ↳ `actions` | string | Additional alert actions triggered by this alert |
### Splunk List Indexes
List the indexes configured on the Splunk instance with their size, event count, and retention settings.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `datatype` | string | No | Filter indexes by type: all, event, or metric. Splunk defaults to event, so pass all to include metric indexes. |
| `count` | number | No | Maximum number of indexes to return \(e.g. 50\). 0 returns all. |
| `offset` | number | No | Index of the first entry to return, for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `indexes` | array | Indexes configured on the instance |
| ↳ `name` | string | Index name |
| ↳ `id` | string | Fully qualified REST URI of the index |
| ↳ `updated` | string | Last update timestamp |
| ↳ `datatype` | string | Index data type \(event or metric\) |
| ↳ `disabled` | boolean | Whether the index is disabled |
| ↳ `isInternal` | boolean | Whether this is an internal Splunk index |
| ↳ `totalEventCount` | number | Total number of events in the index |
| ↳ `currentDBSizeMB` | number | Current index size in megabytes |
| ↳ `maxTotalDataSizeMB` | number | Maximum index size in megabytes before rolling to frozen |
| ↳ `frozenTimePeriodInSecs` | number | Age in seconds at which data rolls to frozen |
| ↳ `minTime` | string | Timestamp of the earliest event in the index |
| ↳ `maxTime` | string | Timestamp of the latest event in the index |
| ↳ `homePath` | string | Path to the hot and warm buckets |
| ↳ `coldPath` | string | Path to the cold buckets |
| ↳ `thawedPath` | string | Path to the thawed buckets |
### Splunk List Apps
List the apps installed on the Splunk instance with their label, version, author, and enabled state.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `count` | number | No | Maximum number of apps to return \(e.g. 50\). 0 returns all. |
| `offset` | number | No | Index of the first app to return, for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `apps` | array | Apps installed on the Splunk instance |
| ↳ `name` | string | App directory name, usable as the app namespace |
| ↳ `id` | string | Fully qualified REST URI of the app |
| ↳ `updated` | string | Last update timestamp |
| ↳ `label` | string | Display name of the app |
| ↳ `version` | string | App version |
| ↳ `author` | string | App author |
| ↳ `description` | string | App description |
| ↳ `details` | string | URL with detailed information about the app |
| ↳ `disabled` | boolean | Whether the app is disabled |
| ↳ `visible` | boolean | Whether the app is visible and navigable from Splunk Web |
| ↳ `configured` | boolean | Whether the custom app setup has been completed |
| ↳ `checkForUpdates` | boolean | Whether Splunkbase is checked for app updates |
| ↳ `stateChangeRequiresRestart` | boolean | Whether changing the app state requires a restart |
+138
View File
@@ -0,0 +1,138 @@
/**
* @vitest-environment node
*/
import { describe, expect, it, vi } from 'vitest'
vi.mock('@/triggers', () => ({
getTrigger: () => ({ subBlocks: [] }),
}))
import { SplunkBlock } from '@/blocks/blocks/splunk'
const toParams = SplunkBlock.tools.config?.params
function mapParams(params: Record<string, unknown>) {
if (!toParams) throw new Error('SplunkBlock is missing tools.config.params')
return toParams(params as Parameters<typeof toParams>[0])
}
/**
* What the tool actually receives. The generic handler merges the mapper's return
* over the raw serialized subBlock values (`{ ...inputs, ...transformedParams }`), so a
* key the mapper only assigns conditionally leaves the raw subBlock string in place.
* Assertions about dropping a value are only meaningful against this merged result.
*/
function mergedInputs(params: Record<string, unknown>) {
return { ...params, ...mapParams(params) }
}
describe('SplunkBlock tools.config.params', () => {
describe('search-job toggles', () => {
it('preserves a typed boolean false from a variable or agent tool call', () => {
const result = mapParams({
operation: 'splunk_create_search_job',
enableLookups: false,
allowPartialResults: false,
})
expect(result.enableLookups).toBe(false)
expect(result.allowPartialResults).toBe(false)
})
it('reads the dropdown string form', () => {
expect(
mapParams({
operation: 'splunk_create_search_job',
enableLookups: 'false',
allowPartialResults: 'true',
})
).toMatchObject({ enableLookups: false, allowPartialResults: true })
})
it('leaves an untouched toggle undefined so Splunk applies its own default', () => {
const result = mapParams({
operation: 'splunk_create_search_job',
enableLookups: null,
allowPartialResults: '',
})
expect(result.enableLookups).toBeUndefined()
expect(result.allowPartialResults).toBeUndefined()
})
})
describe('pagination', () => {
it('omits Max Results when untouched rather than asking for every row', () => {
const result = mapParams({ operation: 'splunk_list_indexes', count: null, offset: null })
expect(result).not.toHaveProperty('count')
expect(result).not.toHaveProperty('offset')
})
it('coerces a typed Max Results, including an explicit 0', () => {
expect(
mapParams({ operation: 'splunk_list_indexes', count: '50', offset: '10' })
).toMatchObject({ count: 50, offset: 10 })
expect(mapParams({ operation: 'splunk_list_indexes', count: '0' })).toMatchObject({
count: 0,
})
})
})
describe('switch-typed toggles', () => {
it('converts the switch string form so the tool sees a real boolean', () => {
expect(
mergedInputs({
operation: 'splunk_dispatch_saved_search',
savedSearchName: 'Errors',
triggerActions: 'true',
forceDispatch: 'false',
})
).toMatchObject({ triggerActions: true, forceDispatch: false })
expect(
mergedInputs({
operation: 'splunk_get_search_results',
sid: '1.1',
addSummaryToMetadata: 'false',
})
).toMatchObject({ addSummaryToMetadata: false })
})
it('drops an untouched switch from the merged inputs, not just from the mapper', () => {
const merged = mergedInputs({
operation: 'splunk_dispatch_saved_search',
savedSearchName: 'Errors',
triggerActions: null,
forceDispatch: null,
})
expect(merged.triggerActions).toBeUndefined()
expect(merged.forceDispatch).toBeUndefined()
})
})
describe('subBlock to tool param remapping', () => {
it('maps the saved-search and alert name subBlocks onto the tool name param', () => {
expect(
mapParams({ operation: 'splunk_dispatch_saved_search', savedSearchName: 'Errors' })
).toMatchObject({ name: 'Errors' })
expect(
mapParams({ operation: 'splunk_get_fired_alerts', alertName: 'Errors' })
).toMatchObject({ name: 'Errors' })
})
})
})
describe('SplunkBlock subBlocks', () => {
it('has no duplicate subBlock ids', () => {
const ids = SplunkBlock.subBlocks.map((subBlock) => subBlock.id)
expect(new Set(ids).size).toBe(ids.length)
})
it('exposes every tool in tools.access as an operation option', () => {
const operation = SplunkBlock.subBlocks.find((subBlock) => subBlock.id === 'operation')
const optionIds = (operation?.options as { id: string }[]).map((option) => option.id)
expect([...optionIds].sort()).toEqual([...SplunkBlock.tools.access].sort())
})
})
+721
View File
@@ -0,0 +1,721 @@
import { SplunkIcon } from '@/components/icons'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { SplunkResponse } from '@/tools/splunk/types'
/**
* Normalize a Splunk toggle. A dropdown supplies the strings `'true'`/`'false'`,
* while a workflow variable, an agent tool call, or a block created over the API
* supplies a real boolean — so both forms must survive. An untouched subBlock
* resolves to `null`, which stays `undefined` here so the field is omitted from
* the request and Splunk applies its own documented default.
*/
function toSplunkToggle(value: unknown): boolean | undefined {
if (value == null || value === '') return undefined
if (typeof value === 'boolean') return value
return value !== 'false' && value !== '0'
}
export const SplunkBlock: BlockConfig<SplunkResponse> = {
type: 'splunk',
name: 'Splunk',
description: 'Run SPL searches and manage saved searches and alerts in Splunk',
authMode: AuthMode.ApiKey,
longDescription:
'Integrate Splunk Enterprise or Splunk Cloud into workflows. Run SPL searches synchronously or as asynchronous jobs, fetch results, dispatch saved searches, and inspect fired alerts and indexes.',
docsLink: 'https://docs.sim.ai/integrations/splunk',
category: 'tools',
integrationType: IntegrationType.Observability,
bgColor: '#FFFFFF',
icon: SplunkIcon,
canvasPresentation: {
defaultTitle: 'Splunk',
sentences: {
byOperation: {
splunk_run_search: [
{ text: 'Run search', field: 'search', core: true },
{ text: ', from', field: 'earliestTime' },
{ text: ', until', field: 'latestTime' },
],
splunk_create_search_job: [
{ text: 'Start search job for', field: 'search', core: true },
{ text: ', from', field: 'earliestTime' },
{ text: ', until', field: 'latestTime' },
],
splunk_get_search_job: [{ text: 'Check search job', field: 'sid', core: true }],
splunk_get_search_results: [
{ text: 'Fetch results of search job', field: 'sid', core: true },
{ text: ', returning at most', field: 'count' },
{ text: ', starting at', field: 'offset' },
],
splunk_cancel_search_job: [{ text: 'Cancel search job', field: 'sid', core: true }],
splunk_list_saved_searches: [
'List saved searches',
{ text: ', matching', field: 'savedSearchFilter' },
{ text: ', returning at most', field: 'count' },
],
splunk_get_saved_search: [
{ text: 'Fetch saved search', field: 'savedSearchName', core: true },
],
splunk_dispatch_saved_search: [
{ text: 'Run saved search', field: 'savedSearchName', core: true },
{ text: ', from', field: 'dispatchEarliestTime' },
{ text: ', until', field: 'dispatchLatestTime' },
],
splunk_list_fired_alerts: [
'List fired alerts',
{ text: ', returning at most', field: 'count' },
],
splunk_get_fired_alerts: [
{ text: 'Fetch fired alerts for', field: 'alertName', core: true },
],
splunk_list_indexes: [
'List indexes',
{ text: ', of type', field: 'datatype' },
{ text: ', returning at most', field: 'count' },
],
splunk_list_apps: ['List apps', { text: ', returning at most', field: 'count' }],
},
},
},
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Run Search', id: 'splunk_run_search' },
{ label: 'Create Search Job', id: 'splunk_create_search_job' },
{ label: 'Get Search Job', id: 'splunk_get_search_job' },
{ label: 'Get Search Results', id: 'splunk_get_search_results' },
{ label: 'Cancel Search Job', id: 'splunk_cancel_search_job' },
{ label: 'List Saved Searches', id: 'splunk_list_saved_searches' },
{ label: 'Get Saved Search', id: 'splunk_get_saved_search' },
{ label: 'Dispatch Saved Search', id: 'splunk_dispatch_saved_search' },
{ label: 'List Fired Alerts', id: 'splunk_list_fired_alerts' },
{ label: 'Get Fired Alerts', id: 'splunk_get_fired_alerts' },
{ label: 'List Indexes', id: 'splunk_list_indexes' },
{ label: 'List Apps', id: 'splunk_list_apps' },
],
value: () => 'splunk_run_search',
},
{
id: 'baseUrl',
title: 'Splunk URL',
type: 'short-input',
placeholder: 'https://splunk.example.com:8089',
required: true,
},
{
id: 'authToken',
title: 'Authentication Token',
type: 'short-input',
placeholder: 'Splunk bearer token',
password: true,
},
{
id: 'username',
title: 'Username',
type: 'short-input',
placeholder: 'Only needed without a token',
mode: 'advanced',
},
{
id: 'password',
title: 'Password',
type: 'short-input',
placeholder: 'Only needed without a token',
password: true,
mode: 'advanced',
},
{
id: 'owner',
title: 'Namespace Owner',
type: 'short-input',
placeholder: 'nobody',
mode: 'advanced',
},
{
id: 'app',
title: 'Namespace App',
type: 'short-input',
placeholder: 'search',
mode: 'advanced',
},
{
id: 'search',
title: 'SPL Search',
type: 'long-input',
placeholder: 'index=main error | stats count by host',
required: true,
condition: {
field: 'operation',
value: ['splunk_run_search', 'splunk_create_search_job'],
},
wandConfig: {
enabled: true,
prompt: `Generate a Splunk SPL search from the user's description.
Rules:
- Return ONLY the SPL string, no explanations, no quotes, no code fences.
- Start with an index filter when the user names a data source (e.g. index=main).
- Use pipes for transforming commands (e.g. | stats count by host).
Examples:
- "errors in the main index in the last hour grouped by host" -> index=main error | stats count by host
- "top 10 slowest web requests" -> index=web | sort - duration | head 10`,
placeholder: 'Describe the search you want to run...',
},
},
{
id: 'earliestTime',
title: 'Earliest Time',
type: 'short-input',
placeholder: '-24h',
condition: {
field: 'operation',
value: ['splunk_run_search', 'splunk_create_search_job'],
},
},
{
id: 'latestTime',
title: 'Latest Time',
type: 'short-input',
placeholder: 'now',
condition: {
field: 'operation',
value: ['splunk_run_search', 'splunk_create_search_job'],
},
},
{
id: 'adhocSearchLevel',
title: 'Search Mode',
type: 'dropdown',
options: [
{ label: 'Fast', id: 'fast' },
{ label: 'Smart', id: 'smart' },
{ label: 'Verbose', id: 'verbose' },
],
mode: 'advanced',
condition: {
field: 'operation',
value: ['splunk_run_search', 'splunk_create_search_job'],
},
},
{
id: 'autoCancel',
title: 'Auto-Cancel After (seconds)',
type: 'short-input',
placeholder: '0 never auto-cancels',
mode: 'advanced',
condition: {
field: 'operation',
value: ['splunk_run_search', 'splunk_create_search_job'],
},
},
{
id: 'maxCount',
title: 'Max Stored Results',
type: 'short-input',
placeholder: '10000',
mode: 'advanced',
condition: {
field: 'operation',
value: ['splunk_run_search', 'splunk_create_search_job'],
},
},
{
id: 'execMode',
title: 'Execution Mode',
type: 'dropdown',
options: [
{ label: 'Normal (returns sid immediately)', id: 'normal' },
{ label: 'Blocking (returns sid when done)', id: 'blocking' },
],
mode: 'advanced',
condition: { field: 'operation', value: 'splunk_create_search_job' },
},
{
id: 'searchId',
title: 'Custom Search ID',
type: 'short-input',
placeholder: 'Generated automatically when empty',
mode: 'advanced',
condition: { field: 'operation', value: 'splunk_create_search_job' },
},
{
id: 'indexEarliest',
title: 'Index Earliest Time',
type: 'short-input',
placeholder: '-24h',
mode: 'advanced',
condition: { field: 'operation', value: 'splunk_create_search_job' },
},
{
id: 'indexLatest',
title: 'Index Latest Time',
type: 'short-input',
placeholder: 'now',
mode: 'advanced',
condition: { field: 'operation', value: 'splunk_create_search_job' },
},
{
id: 'enableLookups',
title: 'Enable Lookups',
type: 'dropdown',
options: [
{ label: 'Yes', id: 'true' },
{ label: 'No', id: 'false' },
],
value: () => 'true',
mode: 'advanced',
condition: { field: 'operation', value: 'splunk_create_search_job' },
},
{
id: 'allowPartialResults',
title: 'Allow Partial Results',
type: 'dropdown',
options: [
{ label: 'Yes', id: 'true' },
{ label: 'No', id: 'false' },
],
value: () => 'true',
mode: 'advanced',
condition: { field: 'operation', value: 'splunk_create_search_job' },
},
{
id: 'sid',
title: 'Search ID',
type: 'short-input',
placeholder: '1457683115.100',
required: true,
condition: {
field: 'operation',
value: ['splunk_get_search_job', 'splunk_get_search_results', 'splunk_cancel_search_job'],
},
},
{
id: 'fields',
title: 'Fields',
type: 'short-input',
placeholder: '_time, host, source (comma-separated)',
mode: 'advanced',
condition: { field: 'operation', value: 'splunk_get_search_results' },
},
{
id: 'addSummaryToMetadata',
title: 'Include Field Summary',
type: 'switch',
mode: 'advanced',
condition: { field: 'operation', value: 'splunk_get_search_results' },
},
{
id: 'savedSearchName',
title: 'Saved Search Name',
type: 'short-input',
placeholder: 'Errors in the last 24 hours',
required: true,
condition: {
field: 'operation',
value: ['splunk_get_saved_search', 'splunk_dispatch_saved_search'],
},
},
{
id: 'savedSearchFilter',
title: 'Filter',
type: 'short-input',
placeholder: 'name=Errors*',
condition: { field: 'operation', value: 'splunk_list_saved_searches' },
},
{
id: 'triggerActions',
title: 'Trigger Alert Actions',
type: 'switch',
condition: { field: 'operation', value: 'splunk_dispatch_saved_search' },
},
{
id: 'dispatchEarliestTime',
title: 'Earliest Time Override',
type: 'short-input',
placeholder: '-24h',
condition: { field: 'operation', value: 'splunk_dispatch_saved_search' },
},
{
id: 'dispatchLatestTime',
title: 'Latest Time Override',
type: 'short-input',
placeholder: 'now',
condition: { field: 'operation', value: 'splunk_dispatch_saved_search' },
},
{
id: 'dispatchMaxCount',
title: 'Max Results',
type: 'short-input',
placeholder: '10000',
mode: 'advanced',
condition: { field: 'operation', value: 'splunk_dispatch_saved_search' },
},
{
id: 'dispatchMaxTime',
title: 'Max Run Time (seconds)',
type: 'short-input',
placeholder: '300',
mode: 'advanced',
condition: { field: 'operation', value: 'splunk_dispatch_saved_search' },
},
{
id: 'dispatchTtl',
title: 'Artifact TTL (seconds)',
type: 'short-input',
placeholder: '600',
mode: 'advanced',
condition: { field: 'operation', value: 'splunk_dispatch_saved_search' },
},
{
id: 'forceDispatch',
title: 'Force Dispatch',
type: 'switch',
mode: 'advanced',
condition: { field: 'operation', value: 'splunk_dispatch_saved_search' },
},
{
id: 'alertName',
title: 'Alerting Saved Search',
type: 'short-input',
placeholder: 'Errors in the last 24 hours',
required: true,
condition: { field: 'operation', value: 'splunk_get_fired_alerts' },
},
{
id: 'datatype',
title: 'Index Type',
type: 'dropdown',
options: [
{ label: 'All', id: 'all' },
{ label: 'Event', id: 'event' },
{ label: 'Metric', id: 'metric' },
],
value: () => 'all',
condition: { field: 'operation', value: 'splunk_list_indexes' },
},
{
id: 'count',
title: 'Max Results',
type: 'short-input',
placeholder: '100 (0 returns all)',
condition: {
field: 'operation',
value: [
'splunk_get_search_results',
'splunk_list_saved_searches',
'splunk_list_fired_alerts',
'splunk_list_indexes',
'splunk_list_apps',
],
},
},
{
id: 'offset',
title: 'Offset',
type: 'short-input',
placeholder: '0',
mode: 'advanced',
condition: {
field: 'operation',
value: [
'splunk_get_search_results',
'splunk_list_saved_searches',
'splunk_list_fired_alerts',
'splunk_list_indexes',
'splunk_list_apps',
],
},
},
],
tools: {
access: [
'splunk_run_search',
'splunk_create_search_job',
'splunk_get_search_job',
'splunk_get_search_results',
'splunk_cancel_search_job',
'splunk_list_saved_searches',
'splunk_get_saved_search',
'splunk_dispatch_saved_search',
'splunk_list_fired_alerts',
'splunk_get_fired_alerts',
'splunk_list_indexes',
'splunk_list_apps',
],
config: {
tool: (params) => params.operation,
params: (params) => {
const result: Record<string, unknown> = {}
if (params.count != null && params.count !== '') result.count = Number(params.count)
if (params.offset != null && params.offset !== '') result.offset = Number(params.offset)
switch (params.operation) {
case 'splunk_run_search':
if (params.autoCancel) result.autoCancel = Number(params.autoCancel)
if (params.maxCount) result.maxCount = Number(params.maxCount)
break
case 'splunk_create_search_job':
if (params.autoCancel) result.autoCancel = Number(params.autoCancel)
if (params.maxCount) result.maxCount = Number(params.maxCount)
result.enableLookups = toSplunkToggle(params.enableLookups)
result.allowPartialResults = toSplunkToggle(params.allowPartialResults)
break
case 'splunk_get_search_results':
result.addSummaryToMetadata = toSplunkToggle(params.addSummaryToMetadata)
break
case 'splunk_list_saved_searches':
result.search = params.savedSearchFilter ?? ''
break
case 'splunk_get_saved_search':
case 'splunk_dispatch_saved_search':
result.name = params.savedSearchName
result.triggerActions = toSplunkToggle(params.triggerActions)
result.forceDispatch = toSplunkToggle(params.forceDispatch)
if (params.dispatchMaxCount) result.dispatchMaxCount = Number(params.dispatchMaxCount)
if (params.dispatchMaxTime) result.dispatchMaxTime = Number(params.dispatchMaxTime)
if (params.dispatchTtl) result.dispatchTtl = Number(params.dispatchTtl)
break
case 'splunk_get_fired_alerts':
result.name = params.alertName
break
}
return result
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
baseUrl: { type: 'string', description: 'Splunk management URL including port' },
authToken: { type: 'string', description: 'Splunk authentication (bearer) token' },
username: { type: 'string', description: 'Username for basic authentication' },
password: { type: 'string', description: 'Password for basic authentication' },
owner: { type: 'string', description: 'Namespace owner for /servicesNS requests' },
app: { type: 'string', description: 'Namespace app context for /servicesNS requests' },
search: { type: 'string', description: 'SPL search string' },
earliestTime: { type: 'string', description: 'Earliest time bound for the search' },
latestTime: { type: 'string', description: 'Latest time bound for the search' },
adhocSearchLevel: { type: 'string', description: 'Search mode: fast, smart, or verbose' },
autoCancel: { type: 'number', description: 'Seconds of inactivity before auto-cancelling' },
execMode: { type: 'string', description: 'Execution mode: normal or blocking' },
searchId: { type: 'string', description: 'Custom search ID for the new job' },
indexEarliest: { type: 'string', description: 'Earliest index-time bound' },
indexLatest: { type: 'string', description: 'Latest index-time bound' },
enableLookups: { type: 'boolean', description: 'Whether lookups are applied to events' },
allowPartialResults: {
type: 'boolean',
description: 'Whether partial results are allowed when a search peer fails',
},
maxCount: {
type: 'number',
description: 'Maximum number of results the search stores and returns',
},
sid: { type: 'string', description: 'Search ID of an existing job' },
fields: { type: 'string', description: 'Comma-separated fields to return per result row' },
addSummaryToMetadata: {
type: 'boolean',
description: 'Include field summary statistics with the results',
},
name: { type: 'string', description: 'Saved search or alerting saved search name' },
savedSearchName: { type: 'string', description: 'Name of the saved search' },
savedSearchFilter: { type: 'string', description: 'Filter expression for saved searches' },
alertName: { type: 'string', description: 'Name of the alerting saved search' },
triggerActions: {
type: 'boolean',
description: 'Whether to trigger alert actions when dispatching',
},
dispatchEarliestTime: {
type: 'string',
description: 'Earliest time override for the dispatch',
},
dispatchLatestTime: { type: 'string', description: 'Latest time override for the dispatch' },
dispatchMaxCount: { type: 'number', description: 'Maximum results before finalizing' },
dispatchMaxTime: { type: 'number', description: 'Maximum run time in seconds' },
dispatchTtl: { type: 'number', description: 'Time to live for the search artifacts' },
forceDispatch: {
type: 'boolean',
description: 'Whether to dispatch even when the saved search is already running',
},
datatype: { type: 'string', description: 'Index type filter: all, event, or metric' },
count: { type: 'number', description: 'Maximum number of entries to return' },
offset: { type: 'number', description: 'Index of the first entry to return' },
},
outputs: {
results: {
type: 'json',
description: 'Search result rows, each holding the fields the search produced',
},
resultCount: { type: 'number', description: 'Number of result rows returned' },
preview: { type: 'boolean', description: 'Whether the results are previews' },
initOffset: { type: 'number', description: 'Offset of the first returned row' },
messages: { type: 'json', description: 'Messages returned with the response ([{type, text}])' },
sid: { type: 'string', description: 'Search ID of the job' },
label: { type: 'string', description: 'Custom name of the search job' },
dispatchState: { type: 'string', description: 'Current state of the search job' },
doneProgress: { type: 'number', description: 'Approximate job progress between 0 and 1' },
isDone: { type: 'boolean', description: 'Whether the search has completed' },
isFailed: { type: 'boolean', description: 'Whether the search failed' },
isFinalized: { type: 'boolean', description: 'Whether the search was finalized' },
isPaused: { type: 'boolean', description: 'Whether the search is paused' },
isZombie: { type: 'boolean', description: 'Whether the search process died' },
isSaved: { type: 'boolean', description: 'Whether the job artifacts are saved' },
isSavedSearch: { type: 'boolean', description: 'Whether the job came from a saved search' },
isRealTimeSearch: { type: 'boolean', description: 'Whether this is a real-time search' },
eventCount: { type: 'number', description: 'Number of events returned' },
eventAvailableCount: { type: 'number', description: 'Number of events available for export' },
eventFieldCount: { type: 'number', description: 'Number of fields found in the results' },
resultPreviewCount: { type: 'number', description: 'Number of rows in the latest preview' },
scanCount: { type: 'number', description: 'Number of events scanned off disk' },
runDuration: { type: 'number', description: 'Seconds the search took to complete' },
priority: { type: 'number', description: 'Search priority between 0 and 10' },
earliestTime: { type: 'string', description: 'Earliest time bound of the job' },
latestTime: { type: 'string', description: 'Latest time bound of the job' },
searchEarliestTime: {
type: 'string',
description: 'Earliest time as specified in the search command',
},
searchLatestTime: {
type: 'string',
description: 'Latest time as specified in the search command',
},
savedSearches: {
type: 'json',
description:
'Saved searches ([{name, id, author, updated, search, description, disabled, isScheduled, cronSchedule, alertType}])',
},
name: { type: 'string', description: 'Saved search name' },
id: { type: 'string', description: 'Fully qualified REST URI of the resource' },
author: { type: 'string', description: 'Owner of the saved search' },
updated: { type: 'string', description: 'Last update timestamp' },
search: { type: 'string', description: 'SPL the saved search runs' },
qualifiedSearch: { type: 'string', description: 'Exact search string the scheduler runs' },
description: { type: 'string', description: 'Saved search description' },
disabled: { type: 'boolean', description: 'Whether the saved search is disabled' },
isScheduled: { type: 'boolean', description: 'Whether the search runs on a schedule' },
isVisible: { type: 'boolean', description: 'Whether the search is listed as visible' },
cronSchedule: { type: 'string', description: 'Cron schedule for the search' },
nextScheduledTime: { type: 'string', description: 'Next scheduled run time' },
alertType: { type: 'string', description: 'Alert condition type' },
dispatchEarliestTime: { type: 'string', description: 'Earliest time used when dispatching' },
dispatchLatestTime: { type: 'string', description: 'Latest time used when dispatching' },
alerts: {
type: 'json',
description:
'Saved searches with currently triggered alerts ([{name, id, updated, triggeredAlertCount}])',
},
firedAlerts: {
type: 'json',
description:
'Triggered instances of an alert ([{name, savedSearchName, alertType, severity, sid, triggerTime}])',
},
indexes: {
type: 'json',
description:
'Indexes configured on the instance ([{name, datatype, disabled, totalEventCount, currentDBSizeMB, maxTotalDataSizeMB, minTime, maxTime}])',
},
apps: {
type: 'json',
description: 'Apps installed on the instance (name, label, version, author, disabled)',
},
},
}
export const SplunkBlockMeta = {
tags: ['monitoring', 'data-analytics'],
url: 'https://www.splunk.com',
templates: [
{
icon: SplunkIcon,
title: 'Splunk error spike triage',
prompt:
'Build a scheduled workflow that runs a Splunk SPL search for error spikes every 15 minutes, summarizes the top offending hosts and sourcetypes with an agent, and posts the triage summary to Slack.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'engineering',
tags: ['devops', 'monitoring'],
alsoIntegrations: ['slack'],
},
{
icon: SplunkIcon,
title: 'Splunk alert to incident',
prompt:
'Create a scheduled workflow that polls Splunk fired alerts, deduplicates them by saved search, and opens a PagerDuty incident with the alert severity and the search ID attached.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'engineering',
tags: ['devops', 'incident-response'],
alsoIntegrations: ['pagerduty'],
},
{
icon: SplunkIcon,
title: 'Splunk security digest',
prompt:
'Build a scheduled daily workflow that dispatches a Splunk saved search for failed authentication attempts, waits for the job to finish, and emails a security digest with the top source IPs.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'engineering',
tags: ['security', 'reporting'],
alsoIntegrations: ['gmail'],
},
{
icon: SplunkIcon,
title: 'Splunk log export to tables',
prompt:
'Create a scheduled workflow that runs a Splunk search for the previous hour of transaction logs and writes the results into a Sim table for downstream reporting.',
modules: ['scheduled', 'tables', 'workflows'],
category: 'engineering',
tags: ['analysis', 'sync'],
},
{
icon: SplunkIcon,
title: 'Splunk index capacity watch',
prompt:
'Build a scheduled weekly workflow that lists Splunk indexes, flags any index close to its maximum data size or retention limit, and posts a capacity warning to the platform team in Slack.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['devops', 'monitoring'],
alsoIntegrations: ['slack'],
},
{
icon: SplunkIcon,
title: 'Splunk saved search inventory',
prompt:
'Create a scheduled monthly workflow that lists every Splunk saved search, flags scheduled searches that have not run recently or are disabled, and writes a cleanup queue to a Sim table.',
modules: ['scheduled', 'tables', 'agent', 'workflows'],
category: 'operations',
tags: ['devops', 'automation'],
},
{
icon: SplunkIcon,
title: 'Splunk incident context agent',
prompt:
'Build an agent workflow that takes an incident description, runs targeted Splunk searches for the affected service, and returns a timeline of the relevant log events with the matching search ID.',
modules: ['agent', 'workflows'],
category: 'engineering',
tags: ['devops', 'analysis'],
},
{
icon: SplunkIcon,
title: 'Splunk + Jira defect linking',
prompt:
'Create a scheduled workflow that runs a Splunk search for recurring application exceptions and opens or updates a Jira bug for each distinct stack trace with the event count attached.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'engineering',
tags: ['engineering', 'ticketing'],
alsoIntegrations: ['jira'],
},
],
} as const satisfies BlockMeta
+3
View File
@@ -294,6 +294,7 @@ import { SlackBlock, SlackBlockMeta, SlackV2Block } from '@/blocks/blocks/slack'
import { SmartleadBlock, SmartleadBlockMeta } from '@/blocks/blocks/smartlead'
import { SmtpBlock, SmtpBlockMeta } from '@/blocks/blocks/smtp'
import { SnowflakeBlock, SnowflakeBlockMeta } from '@/blocks/blocks/snowflake'
import { SplunkBlock, SplunkBlockMeta } from '@/blocks/blocks/splunk'
import { SportmonksBlock, SportmonksBlockMeta } from '@/blocks/blocks/sportmonks'
import { SpotifyBlock, SpotifyBlockMeta } from '@/blocks/blocks/spotify'
import { SQSBlock, SQSBlockMeta } from '@/blocks/blocks/sqs'
@@ -625,6 +626,7 @@ export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
smartlead: SmartleadBlock,
smtp: SmtpBlock,
snowflake: SnowflakeBlock,
splunk: SplunkBlock,
sportmonks: SportmonksBlock,
spotify: SpotifyBlock,
sqs: SQSBlock,
@@ -920,6 +922,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
smartlead: SmartleadBlockMeta,
smtp: SmtpBlockMeta,
snowflake: SnowflakeBlockMeta,
splunk: SplunkBlockMeta,
sportmonks: SportmonksBlockMeta,
spotify: SpotifyBlockMeta,
sqs: SQSBlockMeta,
+27
View File
@@ -8726,6 +8726,33 @@ export function BigQueryIcon(props: SVGProps<SVGSVGElement>) {
)
}
/**
* Splunk wordmark. The lettering is drawn with `currentColor` so it stays legible
* bare in both light and dark mode; the chevron keeps its brand green.
*/
export function SplunkIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox='0 0 48 48'
fillRule='evenodd'
clipRule='evenodd'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path
fill='currentColor'
d='M13.432 18L13.5 28 15 28 15 18zM32 18v10.005h1.805c0 0 .015-3.609.035-3.608.019.001 2.69 3.743 2.716 3.733.027-.01 1.599-.576 1.599-.59 0-.014-2.684-3.441-2.684-3.448 0-.006 2.251-2.509 2.231-2.529-.06-.06-1.281-.586-1.314-.566-.018.011-2.582 2.796-2.582 2.796V18H32zM2.099 21.05c-.651.137-1.259.552-1.509 1.031-.23.44-.227 1.043.007 1.491.16.305.625.735 1.348 1.246.782.552 1.078.832 1.153 1.092.111.385-.06.752-.434.931-.187.089-.27.101-.603.086-.433-.02-.729-.132-1.172-.446-.14-.099-.269-.167-.285-.152-.016.016-.155.224-.311.466L.01 27.23l.109.081c.06.045.248.158.422.255 1.187.66 2.944.555 3.835-.229.475-.418.697-1.025.602-1.647-.089-.582-.409-.965-1.417-1.695-.791-.573-1.166-.889-1.253-1.059-.167-.326-.049-.643.307-.827.178-.092.251-.102.608-.087.347.015.453.041.741.179l.335.161.263-.41c.144-.224.252-.426.239-.445-.048-.071-.598-.299-.941-.391C3.402 20.995 2.521 20.961 2.099 21.05zM10.031 21.935c.788.369 1.212 1.528 1.041 2.852-.06.467-.227 1.032-.383 1.296-.159.268-.463.56-.705.675-.444.212-1.107.13-1.523-.187-.577-.442-.893-1.524-.772-2.649.111-1.032.483-1.684 1.133-1.986C9.156 21.781 9.7 21.781 10.031 21.935zM9.313 20.784c-.472.106-.85.319-1.254.705l-.366.35V21H6.814 6v5 5h1.693v-4.08l.324.319c.592.583 1.287.814 2.101.701.532-.074.904-.21 1.3-.475 1.111-.743 1.721-2.221 1.554-3.771-.158-1.467-.912-2.484-2.128-2.871C10.531 20.724 9.679 20.702 9.313 20.784zM16.258 21c0 0 .055 5.284.091 5.438.136.583.525 1.137.964 1.374.403.218.727.287 1.333.284.861-.004 1.386-.211 2.027-.8L21 27v1h1.742l-.023-7H21c0 0 .025 4.347 0 4.5-.1.611-.573 1.109-1.053 1.27-.287.096-.786.174-1.086.127C18.549 26.848 18.033 26.507 18 26v-4.977L16.258 21zM30.742 28c0 0-.055-5.284-.091-5.438-.136-.583-.525-1.137-.964-1.374-.403-.218-.727-.287-1.333-.284-.861.004-1.386.211-2.027.8L26 22v-1h-1.742l.023 7H26c0 0-.025-4.347 0-4.5.1-.611.573-1.109 1.053-1.27.287-.096.661-.17 1.086-.127C28.566 22.147 29.031 22.535 29 23v4.977L30.742 28z'
/>
<path
fill='#77b539'
fillRule='evenodd'
clipRule='evenodd'
d='M40 20.5L40 22 46 24 40 26.601 40 28 47 25 47 23z'
/>
</svg>
)
}
export function SnowflakeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox='0 0 146.36 139.16' xmlns='http://www.w3.org/2000/svg' {...props}>
@@ -214,6 +214,7 @@ import {
SmartleadIcon,
SmtpIcon,
SnowflakeIcon,
SplunkIcon,
SportmonksIcon,
SQSIcon,
SquareIcon,
@@ -487,6 +488,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
smartlead: SmartleadIcon,
smtp: SmtpIcon,
snowflake: SnowflakeIcon,
splunk: SplunkIcon,
sportmonks: SportmonksIcon,
sqs: SQSIcon,
square: SquareIcon,
@@ -19033,6 +19033,73 @@
"integrationType": "databases",
"tags": ["data-warehouse", "data-analytics", "cloud"]
},
{
"type": "splunk",
"slug": "splunk",
"name": "Splunk",
"description": "Run SPL searches and manage saved searches and alerts in Splunk",
"longDescription": "Integrate Splunk Enterprise or Splunk Cloud into workflows. Run SPL searches synchronously or as asynchronous jobs, fetch results, dispatch saved searches, and inspect fired alerts and indexes.",
"bgColor": "#FFFFFF",
"iconName": "SplunkIcon",
"docsUrl": "https://docs.sim.ai/integrations/splunk",
"operations": [
{
"name": "Run Search",
"description": "Run an SPL search synchronously and return its results in a single call (oneshot mode). Use for short searches; use Create Search Job for long-running ones."
},
{
"name": "Create Search Job",
"description": "Start a Splunk search job and return its search ID (sid). The search runs asynchronously — poll its status and fetch results separately."
},
{
"name": "Get Search Job",
"description": "Get the status and progress of a Splunk search job by search ID, including dispatch state, completion progress, and event/result counts."
},
{
"name": "Get Search Results",
"description": "Fetch the transformed results of a completed Splunk search job by search ID, with pagination."
},
{
"name": "Cancel Search Job",
"description": "Cancel a running Splunk search job and delete its result cache."
},
{
"name": "List Saved Searches",
"description": "List saved searches and reports configured in Splunk, including their SPL, schedule, and alert configuration."
},
{
"name": "Get Saved Search",
"description": "Get the configuration of a single Splunk saved search by name, including its SPL, schedule, and alert settings."
},
{
"name": "Dispatch Saved Search",
"description": "Run a Splunk saved search immediately and return the search ID (sid) of the dispatched job."
},
{
"name": "List Fired Alerts",
"description": "List the saved searches with currently triggered (unexpired) Splunk alerts and how many times each has fired."
},
{
"name": "Get Fired Alerts",
"description": "List the unexpired triggered instances of a Splunk alert by saved search name, including severity, trigger time, and the search ID of each firing."
},
{
"name": "List Indexes",
"description": "List the indexes configured on the Splunk instance with their size, event count, and retention settings."
},
{
"name": "List Apps",
"description": "List the apps installed on the Splunk instance with their label, version, author, and enabled state."
}
],
"operationCount": 12,
"triggers": [],
"triggerCount": 0,
"authType": "api-key",
"category": "tools",
"integrationType": "observability",
"tags": ["monitoring", "data-analytics"]
},
{
"type": "sportmonks",
"slug": "sportmonks",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+26
View File
@@ -4002,6 +4002,20 @@ import {
snowflakeUpdateRowsTool,
snowflakeUpsertRowsTool,
} from '@/tools/snowflake'
import {
splunkCancelSearchJobTool,
splunkCreateSearchJobTool,
splunkDispatchSavedSearchTool,
splunkGetFiredAlertsTool,
splunkGetSavedSearchTool,
splunkGetSearchJobTool,
splunkGetSearchResultsTool,
splunkListAppsTool,
splunkListFiredAlertsTool,
splunkListIndexesTool,
splunkListSavedSearchesTool,
splunkRunSearchTool,
} from '@/tools/splunk'
import {
sportmonksCoreGetCitiesTool,
sportmonksCoreGetCityTool,
@@ -5665,6 +5679,18 @@ export const tools: Record<string, ToolConfig> = {
snowflake_unload_data: snowflakeUnloadDataTool,
snowflake_update_rows: snowflakeUpdateRowsTool,
snowflake_upsert_rows: snowflakeUpsertRowsTool,
splunk_cancel_search_job: splunkCancelSearchJobTool,
splunk_create_search_job: splunkCreateSearchJobTool,
splunk_dispatch_saved_search: splunkDispatchSavedSearchTool,
splunk_get_fired_alerts: splunkGetFiredAlertsTool,
splunk_get_saved_search: splunkGetSavedSearchTool,
splunk_get_search_job: splunkGetSearchJobTool,
splunk_get_search_results: splunkGetSearchResultsTool,
splunk_list_apps: splunkListAppsTool,
splunk_list_fired_alerts: splunkListFiredAlertsTool,
splunk_list_indexes: splunkListIndexesTool,
splunk_list_saved_searches: splunkListSavedSearchesTool,
splunk_run_search: splunkRunSearchTool,
sportmonks_football_expected_by_player: sportmonksExpectedByPlayerTool,
sportmonks_football_expected_by_team: sportmonksExpectedByTeamTool,
sportmonks_football_get_all_commentaries: sportmonksGetAllCommentariesTool,
@@ -0,0 +1,60 @@
import type {
SplunkCancelSearchJobParams,
SplunkCancelSearchJobResponse,
} from '@/tools/splunk/types'
import {
buildSplunkFormBody,
buildSplunkFormHeaders,
buildSplunkUrl,
mapSplunkMessages,
readSplunkJson,
SPLUNK_CONNECTION_PARAMS,
SPLUNK_MESSAGES_OUTPUT,
splunkPathSegment,
} from '@/tools/splunk/utils'
import type { ToolConfig } from '@/tools/types'
/** Stops a search job and deletes its result cache via the job control endpoint. */
export const cancelSearchJobTool: ToolConfig<
SplunkCancelSearchJobParams,
SplunkCancelSearchJobResponse
> = {
id: 'splunk_cancel_search_job',
name: 'Splunk Cancel Search Job',
description: 'Cancel a running Splunk search job and delete its result cache.',
version: '1.0.0',
params: {
...SPLUNK_CONNECTION_PARAMS,
sid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Search ID of the job to cancel (e.g. 1457683115.100)',
},
},
request: {
url: (params) =>
buildSplunkUrl(params, `/search/jobs/${splunkPathSegment(params.sid)}/control`),
method: 'POST',
headers: (params) => buildSplunkFormHeaders(params),
body: () => buildSplunkFormBody({ action: 'cancel' }),
},
transformResponse: async (response: Response, params) => {
const data = await readSplunkJson(response)
return {
success: true,
output: {
sid: params?.sid ?? '',
messages: mapSplunkMessages((data as { messages?: unknown })?.messages),
},
}
},
outputs: {
sid: { type: 'string', description: 'Search ID of the cancelled job' },
messages: SPLUNK_MESSAGES_OUTPUT,
},
}
+163
View File
@@ -0,0 +1,163 @@
import type {
SplunkCreateSearchJobParams,
SplunkCreateSearchJobResponse,
} from '@/tools/splunk/types'
import {
buildSplunkFormBody,
buildSplunkFormHeaders,
buildSplunkUrl,
normalizeSearchQuery,
requireSplunkSid,
SPLUNK_CONNECTION_PARAMS,
} from '@/tools/splunk/utils'
import type { ToolConfig } from '@/tools/types'
/**
* `exec_mode` accepts `blocking | oneshot | normal`, but of those only the two that
* return a search ID belong on this tool — the reference says oneshot "returns results
* in the same call" and "Does not return the search ID". Reject it before the search
* runs rather than letting Splunk execute it and returning a null sid.
*/
function resolveExecMode(execMode: string | undefined): string {
const mode = execMode?.trim().toLowerCase()
if (!mode) return 'normal'
if (mode === 'oneshot') {
throw new Error(
'Splunk Create Search Job cannot use exec_mode=oneshot because that mode returns results instead of a search ID. Use the Splunk Run Search operation instead.'
)
}
if (mode !== 'normal' && mode !== 'blocking') {
throw new Error(`Invalid Splunk execution mode "${execMode}". Use normal or blocking.`)
}
return mode
}
/**
* Starts an asynchronous search job and returns its search ID (sid). Poll the job
* with Get Search Job, then read rows with Get Search Results.
*/
export const createSearchJobTool: ToolConfig<
SplunkCreateSearchJobParams,
SplunkCreateSearchJobResponse
> = {
id: 'splunk_create_search_job',
name: 'Splunk Create Search Job',
description:
'Start a Splunk search job and return its search ID (sid). The search runs asynchronously — poll its status and fetch results separately.',
version: '1.0.0',
params: {
...SPLUNK_CONNECTION_PARAMS,
search: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'SPL search string (e.g. index=main sourcetype=access_combined | timechart count). The leading "search" command is added automatically when omitted.',
},
earliestTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Earliest (inclusive) time bound — relative (e.g. -24h) or absolute time',
},
latestTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Latest (exclusive) time bound — relative (e.g. now) or absolute time',
},
execMode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Execution mode: normal (returns the sid immediately) or blocking (returns the sid once the job completes). Defaults to normal. oneshot is rejected here because it returns results instead of a sid — use Splunk Run Search for that.',
},
adhocSearchLevel: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search mode: verbose, fast, or smart. Defaults to fast.',
},
searchId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom search ID to assign to the job. A random ID is generated when omitted.',
},
indexEarliest: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Earliest (inclusive) time bound based on index time rather than event time',
},
indexLatest: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Latest (exclusive) time bound based on index time rather than event time',
},
enableLookups: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether lookups are applied to events. Defaults to true.',
},
allowPartialResults: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'Whether the job may return partial results when a search peer fails. Defaults to true.',
},
autoCancel: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Cancel the job after this many seconds of inactivity (e.g. 300). 0 never auto-cancels.',
},
maxCount: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Number of events accessible in any given status bucket, and in transforming mode the maximum number of results to store. Defaults to 10000.',
},
},
request: {
url: (params) => buildSplunkUrl(params, '/search/jobs'),
method: 'POST',
headers: (params) => buildSplunkFormHeaders(params),
body: (params) =>
buildSplunkFormBody({
search: normalizeSearchQuery(params.search),
output_mode: 'json',
exec_mode: resolveExecMode(params.execMode),
earliest_time: params.earliestTime,
latest_time: params.latestTime,
adhoc_search_level: params.adhocSearchLevel,
id: params.searchId,
index_earliest: params.indexEarliest,
index_latest: params.indexLatest,
enable_lookups: params.enableLookups,
allow_partial_results: params.allowPartialResults,
auto_cancel: params.autoCancel,
max_count: params.maxCount,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return { success: true, output: { sid: requireSplunkSid(data) } }
},
outputs: {
sid: {
type: 'string',
description: 'Search ID of the created job, used to poll status and fetch results',
},
},
}
@@ -0,0 +1,119 @@
import type {
SplunkDispatchSavedSearchParams,
SplunkDispatchSavedSearchResponse,
} from '@/tools/splunk/types'
import {
buildSplunkFormBody,
buildSplunkFormHeaders,
buildSplunkUrl,
requireSplunkSid,
SPLUNK_CONNECTION_PARAMS,
splunkPathSegment,
} from '@/tools/splunk/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Runs a saved search immediately. Like any dispatched search this is asynchronous:
* the endpoint returns the new job's search ID, which Get Search Job and Get Search
* Results then consume. The REST reference documents the response only in its XML form,
* `<response><sid>...</sid></response>`; that `output_mode=json` renders it as a flat
* `{ "sid": "..." }`, matching `POST search/jobs`, is an inference the reference does not
* state outright. `requireSplunkSid` therefore fails loudly rather than reporting success
* with a null sid if the body arrives in some other shape.
*/
export const dispatchSavedSearchTool: ToolConfig<
SplunkDispatchSavedSearchParams,
SplunkDispatchSavedSearchResponse
> = {
id: 'splunk_dispatch_saved_search',
name: 'Splunk Dispatch Saved Search',
description:
'Run a Splunk saved search immediately and return the search ID (sid) of the dispatched job.',
version: '1.0.0',
params: {
...SPLUNK_CONNECTION_PARAMS,
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the saved search to run (e.g. Errors in the last 24 hours)',
},
triggerActions: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to trigger the saved search alert actions on this run',
},
dispatchEarliestTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Override the earliest time bound for this run — relative (e.g. -24h) or absolute time',
},
dispatchLatestTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Override the latest time bound for this run — relative (e.g. now) or absolute time',
},
dispatchMaxCount: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results before the search is finalized (e.g. 10000)',
},
dispatchMaxTime: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of seconds before the search is finalized (e.g. 300)',
},
dispatchTtl: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Time to live in seconds for the search artifacts when no actions are triggered (e.g. 600)',
},
forceDispatch: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'Start a new search even when another instance of this saved search is already running',
},
},
request: {
url: (params) =>
buildSplunkUrl(params, `/saved/searches/${splunkPathSegment(params.name)}/dispatch`),
method: 'POST',
headers: (params) => buildSplunkFormHeaders(params),
body: (params) =>
buildSplunkFormBody({
output_mode: 'json',
trigger_actions: params.triggerActions,
force_dispatch: params.forceDispatch,
'dispatch.earliest_time': params.dispatchEarliestTime,
'dispatch.latest_time': params.dispatchLatestTime,
'dispatch.max_count': params.dispatchMaxCount,
'dispatch.max_time': params.dispatchMaxTime,
'dispatch.ttl': params.dispatchTtl,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return { success: true, output: { sid: requireSplunkSid(data) } }
},
outputs: {
sid: {
type: 'string',
description: 'Search ID of the dispatched job, used to poll status and fetch results',
},
},
}
+121
View File
@@ -0,0 +1,121 @@
import type { SplunkGetFiredAlertsParams, SplunkGetFiredAlertsResponse } from '@/tools/splunk/types'
import {
asNumber,
asString,
buildSplunkHeaders,
buildSplunkUrl,
getEntryContent,
getEntryName,
getSplunkEntries,
SPLUNK_CONNECTION_PARAMS,
splunkPathSegment,
} from '@/tools/splunk/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Lists the unexpired triggered instances of one alert, by saved search name.
*
* The reference for `alerts/fired_alerts/{name}` states "Request parameters: None" and,
* unlike its sibling collection endpoint, does not carry the "Pagination and filtering
* parameters can be used with this method" note. No `count`/`offset` is sent, because a
* Splunk handler may reject an unsupported argument outright rather than ignore it.
*/
export const getFiredAlertsTool: ToolConfig<
SplunkGetFiredAlertsParams,
SplunkGetFiredAlertsResponse
> = {
id: 'splunk_get_fired_alerts',
name: 'Splunk Get Fired Alerts',
description:
'List the unexpired triggered instances of a Splunk alert by saved search name, including severity, trigger time, and the search ID of each firing.',
version: '1.0.0',
params: {
...SPLUNK_CONNECTION_PARAMS,
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Name of the alerting saved search (e.g. Errors in the last 24 hours). Use - to return the fired alerts of every saved search.',
},
},
request: {
url: (params) =>
buildSplunkUrl(params, `/alerts/fired_alerts/${splunkPathSegment(params.name)}`),
method: 'GET',
headers: (params) => buildSplunkHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
firedAlerts: getSplunkEntries(data).map((entry) => {
const content = getEntryContent(entry)
return {
name: getEntryName(entry),
id: asString(entry.id),
updated: asString(entry.updated),
savedSearchName: asString(content.savedsearch_name),
alertType: asString(content.alert_type),
severity: asNumber(content.severity),
sid: asString(content.sid),
triggerTime: asNumber(content.trigger_time),
triggerTimeRendered: asString(content.trigger_time_rendered),
expirationTimeRendered: asString(content.expiration_time_rendered),
triggeredAlerts: asNumber(content.triggered_alerts),
actions: asString(content.actions),
}
}),
},
}
},
outputs: {
firedAlerts: {
type: 'array',
description: 'Unexpired triggered instances of the alert',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Name of the fired alert entry' },
id: { type: 'string', description: 'Fully qualified REST URI of the entry' },
updated: { type: 'string', description: 'Last update timestamp' },
savedSearchName: {
type: 'string',
description: 'Name of the saved search that triggered the alert',
},
alertType: {
type: 'string',
description: 'Whether the alert was historical or real-time',
},
severity: { type: 'number', description: 'Severity level of the alert' },
sid: { type: 'string', description: 'Search ID of the search that triggered the alert' },
triggerTime: { type: 'number', description: 'Time the alert was triggered' },
triggerTimeRendered: {
type: 'string',
description: 'Human-readable time the alert was triggered',
optional: true,
},
expirationTimeRendered: {
type: 'string',
description: 'Human-readable time this triggered alert record expires',
optional: true,
},
triggeredAlerts: {
type: 'number',
description: 'Number of alerts included in this triggered instance',
optional: true,
},
actions: {
type: 'string',
description: 'Additional alert actions triggered by this alert',
},
},
},
},
},
}
+54
View File
@@ -0,0 +1,54 @@
import {
SAVED_SEARCH_OUTPUT_FIELDS,
type SplunkGetSavedSearchParams,
type SplunkGetSavedSearchResponse,
} from '@/tools/splunk/types'
import {
buildSplunkHeaders,
buildSplunkUrl,
getSplunkEntries,
mapSavedSearchEntry,
SPLUNK_CONNECTION_PARAMS,
splunkPathSegment,
} from '@/tools/splunk/utils'
import type { ToolConfig } from '@/tools/types'
export const getSavedSearchTool: ToolConfig<
SplunkGetSavedSearchParams,
SplunkGetSavedSearchResponse
> = {
id: 'splunk_get_saved_search',
name: 'Splunk Get Saved Search',
description:
'Get the configuration of a single Splunk saved search by name, including its SPL, schedule, and alert settings.',
version: '1.0.0',
params: {
...SPLUNK_CONNECTION_PARAMS,
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the saved search (e.g. Errors in the last 24 hours)',
},
},
request: {
url: (params) => buildSplunkUrl(params, `/saved/searches/${splunkPathSegment(params.name)}`),
method: 'GET',
headers: (params) => buildSplunkHeaders(params),
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
const entry = getSplunkEntries(data)[0]
if (!entry) {
throw new Error(
`Splunk returned no saved search named "${params?.name ?? ''}". Check the name and, if it is a private search, the namespace owner and app.`
)
}
return { success: true, output: mapSavedSearchEntry(entry) }
},
outputs: SAVED_SEARCH_OUTPUT_FIELDS,
}
+156
View File
@@ -0,0 +1,156 @@
import type { SplunkGetSearchJobParams, SplunkGetSearchJobResponse } from '@/tools/splunk/types'
import {
asBoolean,
asNumber,
asString,
buildSplunkHeaders,
buildSplunkUrl,
getEntryContent,
getSplunkEntries,
SPLUNK_CONNECTION_PARAMS,
splunkPathSegment,
} from '@/tools/splunk/utils'
import type { ToolConfig } from '@/tools/types'
export const getSearchJobTool: ToolConfig<SplunkGetSearchJobParams, SplunkGetSearchJobResponse> = {
id: 'splunk_get_search_job',
name: 'Splunk Get Search Job',
description:
'Get the status and progress of a Splunk search job by search ID, including dispatch state, completion progress, and event/result counts.',
version: '1.0.0',
params: {
...SPLUNK_CONNECTION_PARAMS,
sid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Search ID of the job to inspect (e.g. 1457683115.100)',
},
},
request: {
url: (params) => buildSplunkUrl(params, `/search/jobs/${splunkPathSegment(params.sid)}`),
method: 'GET',
headers: (params) => buildSplunkHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const entries = getSplunkEntries(data)
const content =
entries.length > 0 ? getEntryContent(entries[0]) : (data as Record<string, unknown>)
return {
success: true,
output: {
sid: asString(content.sid),
label: asString(content.label),
dispatchState: asString(content.dispatchState),
doneProgress: asNumber(content.doneProgress),
isDone: asBoolean(content.isDone),
isFailed: asBoolean(content.isFailed),
isFinalized: asBoolean(content.isFinalized),
isPaused: asBoolean(content.isPaused),
isZombie: asBoolean(content.isZombie),
isSaved: asBoolean(content.isSaved),
isSavedSearch: asBoolean(content.isSavedSearch),
isRealTimeSearch: asBoolean(content.isRealTimeSearch),
eventCount: asNumber(content.eventCount),
eventAvailableCount: asNumber(content.eventAvailableCount),
eventFieldCount: asNumber(content.eventFieldCount),
resultCount: asNumber(content.resultCount),
resultPreviewCount: asNumber(content.resultPreviewCount),
scanCount: asNumber(content.scanCount),
runDuration: asNumber(content.runDuration),
priority: asNumber(content.priority),
earliestTime: asString(content.earliestTime),
latestTime: asString(content.latestTime),
searchEarliestTime: asString(content.searchEarliestTime),
searchLatestTime: asString(content.searchLatestTime),
messages: (content.messages as Record<string, unknown>) ?? null,
},
}
},
outputs: {
sid: { type: 'string', description: 'Search ID of the job' },
label: { type: 'string', description: 'Custom name created for this search', optional: true },
dispatchState: {
type: 'string',
description:
'Job state: QUEUED, PARSING, RUNNING, FINALIZING, PAUSE, INTERNAL_CANCEL, USER_CANCEL, BAD_INPUT_CANCEL, QUIT, FAILED, or DONE',
},
doneProgress: {
type: 'number',
description: 'Approximate progress between 0 and 1.0',
optional: true,
},
isDone: { type: 'boolean', description: 'Whether the search has completed' },
isFailed: { type: 'boolean', description: 'Whether a fatal error occurred running the search' },
isFinalized: {
type: 'boolean',
description: 'Whether the search was finalized (stopped before completion)',
},
isPaused: { type: 'boolean', description: 'Whether the search is paused' },
isZombie: {
type: 'boolean',
description: 'Whether the search process died before the search finished',
},
isSaved: { type: 'boolean', description: 'Whether the search job artifacts are saved to disk' },
isSavedSearch: {
type: 'boolean',
description: 'Whether this is a saved search run by the scheduler',
},
isRealTimeSearch: { type: 'boolean', description: 'Whether this is a real-time search' },
eventCount: { type: 'number', description: 'Number of events returned by the search' },
eventAvailableCount: {
type: 'number',
description: 'Number of events available for export',
optional: true,
},
eventFieldCount: {
type: 'number',
description: 'Number of fields found in the search results',
optional: true,
},
resultCount: { type: 'number', description: 'Total number of results returned by the search' },
resultPreviewCount: {
type: 'number',
description: 'Number of result rows in the latest preview results',
optional: true,
},
scanCount: { type: 'number', description: 'Number of events scanned or read off disk' },
runDuration: {
type: 'number',
description: 'Time in seconds the search took to complete',
optional: true,
},
priority: { type: 'number', description: 'Search priority between 0 and 10', optional: true },
earliestTime: {
type: 'string',
description: 'Earliest (inclusive) time bound for the search',
optional: true,
},
latestTime: {
type: 'string',
description: 'Latest (exclusive) time bound for the search',
optional: true,
},
searchEarliestTime: {
type: 'string',
description: 'Earliest time as specified in the search command itself',
optional: true,
},
searchLatestTime: {
type: 'string',
description: 'Latest time as specified in the search command itself',
optional: true,
},
messages: {
type: 'json',
description: 'Errors and debug messages recorded for the job',
optional: true,
},
},
}
@@ -0,0 +1,96 @@
import type {
SplunkGetSearchResultsParams,
SplunkSearchResultsResponse,
} from '@/tools/splunk/types'
import {
buildSplunkHeaders,
buildSplunkUrl,
mapSearchResultsPayload,
readSplunkJson,
SEARCH_RESULTS_OUTPUTS,
SPLUNK_CONNECTION_PARAMS,
splunkPathSegment,
} from '@/tools/splunk/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Reads transformed results from `search/v2/jobs/{sid}/results`. The v1 instance of
* this endpoint is deprecated and turned off by default from Splunk Enterprise 9.0.1
* and Splunk Cloud 9.0.2208 onward; both versions return the same JSON envelope
* (`init_offset`, `messages`, `preview`, `results`).
*/
export const getSearchResultsTool: ToolConfig<
SplunkGetSearchResultsParams,
SplunkSearchResultsResponse
> = {
id: 'splunk_get_search_results',
name: 'Splunk Get Search Results',
description:
'Fetch the transformed results of a completed Splunk search job by search ID, with pagination.',
version: '1.0.0',
params: {
...SPLUNK_CONNECTION_PARAMS,
sid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Search ID of the job whose results to fetch (e.g. 1457683115.100)',
},
count: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Maximum number of result rows to return. Defaults to 100. Page through larger result sets with offset rather than raising this — a completed job can hold millions of rows.',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'First result row (0-indexed) from which to begin returning data',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated list of fields to return for each row (e.g. _time,host,source). Returns all fields when omitted.',
},
addSummaryToMetadata: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Include field summary statistics in the response',
},
},
request: {
url: (params) => {
const url = buildSplunkUrl(
params,
`/search/v2/jobs/${splunkPathSegment(params.sid)}/results`,
{
count: params.count,
offset: params.offset,
add_summary_to_metadata: params.addSummaryToMetadata,
}
)
const fields = params.fields
?.split(',')
.map((field) => field.trim())
.filter(Boolean)
if (!fields?.length) return url
return `${url}&${fields.map((field) => `f=${encodeURIComponent(field)}`).join('&')}`
},
method: 'GET',
headers: (params) => buildSplunkHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await readSplunkJson(response)
return { success: true, output: mapSearchResultsPayload(data) }
},
outputs: SEARCH_RESULTS_OUTPUTS,
}
+27
View File
@@ -0,0 +1,27 @@
import { cancelSearchJobTool } from '@/tools/splunk/cancel_search_job'
import { createSearchJobTool } from '@/tools/splunk/create_search_job'
import { dispatchSavedSearchTool } from '@/tools/splunk/dispatch_saved_search'
import { getFiredAlertsTool } from '@/tools/splunk/get_fired_alerts'
import { getSavedSearchTool } from '@/tools/splunk/get_saved_search'
import { getSearchJobTool } from '@/tools/splunk/get_search_job'
import { getSearchResultsTool } from '@/tools/splunk/get_search_results'
import { listAppsTool } from '@/tools/splunk/list_apps'
import { listFiredAlertsTool } from '@/tools/splunk/list_fired_alerts'
import { listIndexesTool } from '@/tools/splunk/list_indexes'
import { listSavedSearchesTool } from '@/tools/splunk/list_saved_searches'
import { runSearchTool } from '@/tools/splunk/run_search'
export const splunkRunSearchTool = runSearchTool
export const splunkCreateSearchJobTool = createSearchJobTool
export const splunkGetSearchJobTool = getSearchJobTool
export const splunkGetSearchResultsTool = getSearchResultsTool
export const splunkCancelSearchJobTool = cancelSearchJobTool
export const splunkListSavedSearchesTool = listSavedSearchesTool
export const splunkGetSavedSearchTool = getSavedSearchTool
export const splunkDispatchSavedSearchTool = dispatchSavedSearchTool
export const splunkListFiredAlertsTool = listFiredAlertsTool
export const splunkGetFiredAlertsTool = getFiredAlertsTool
export const splunkListIndexesTool = listIndexesTool
export const splunkListAppsTool = listAppsTool
+119
View File
@@ -0,0 +1,119 @@
import type { SplunkListAppsParams, SplunkListAppsResponse } from '@/tools/splunk/types'
import {
asBoolean,
asString,
buildSplunkHeaders,
buildSplunkUrl,
getEntryContent,
getEntryName,
getSplunkEntries,
SPLUNK_CONNECTION_PARAMS,
} from '@/tools/splunk/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Lists locally installed apps. The app name doubles as the `app` namespace value
* accepted by every other Splunk tool, so this is how a workflow discovers which
* namespaces it can address.
*/
export const listAppsTool: ToolConfig<SplunkListAppsParams, SplunkListAppsResponse> = {
id: 'splunk_list_apps',
name: 'Splunk List Apps',
description:
'List the apps installed on the Splunk instance with their label, version, author, and enabled state.',
version: '1.0.0',
params: {
...SPLUNK_CONNECTION_PARAMS,
count: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of apps to return (e.g. 50). 0 returns all.',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Index of the first app to return, for pagination',
},
},
request: {
url: (params) =>
buildSplunkUrl(params, '/apps/local', { count: params.count, offset: params.offset }),
method: 'GET',
headers: (params) => buildSplunkHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
apps: getSplunkEntries(data).map((entry) => {
const content = getEntryContent(entry)
return {
name: getEntryName(entry),
id: asString(entry.id),
updated: asString(entry.updated),
label: asString(content.label),
version: asString(content.version),
author: asString(content.author),
description: asString(content.description),
details: asString(content.details),
disabled: asBoolean(content.disabled),
visible: asBoolean(content.visible),
configured: asBoolean(content.configured),
checkForUpdates: asBoolean(content.check_for_updates),
stateChangeRequiresRestart: asBoolean(content.state_change_requires_restart),
}
}),
},
}
},
outputs: {
apps: {
type: 'array',
description: 'Apps installed on the Splunk instance',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'App directory name, usable as the app namespace' },
id: { type: 'string', description: 'Fully qualified REST URI of the app' },
updated: { type: 'string', description: 'Last update timestamp' },
label: { type: 'string', description: 'Display name of the app', optional: true },
version: { type: 'string', description: 'App version', optional: true },
author: { type: 'string', description: 'App author', optional: true },
description: { type: 'string', description: 'App description', optional: true },
details: {
type: 'string',
description: 'URL with detailed information about the app',
optional: true,
},
disabled: { type: 'boolean', description: 'Whether the app is disabled' },
visible: {
type: 'boolean',
description: 'Whether the app is visible and navigable from Splunk Web',
},
configured: {
type: 'boolean',
description: 'Whether the custom app setup has been completed',
optional: true,
},
checkForUpdates: {
type: 'boolean',
description: 'Whether Splunkbase is checked for app updates',
optional: true,
},
stateChangeRequiresRestart: {
type: 'boolean',
description: 'Whether changing the app state requires a restart',
optional: true,
},
},
},
},
},
}
@@ -0,0 +1,90 @@
import type {
SplunkListFiredAlertsParams,
SplunkListFiredAlertsResponse,
} from '@/tools/splunk/types'
import {
asNumber,
asString,
buildSplunkHeaders,
buildSplunkUrl,
getEntryContent,
getEntryName,
getSplunkEntries,
SPLUNK_CONNECTION_PARAMS,
} from '@/tools/splunk/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Lists the saved searches that currently have unexpired triggered alerts, with the
* trigger count for each. Use Get Fired Alerts for the individual instances.
*/
export const listFiredAlertsTool: ToolConfig<
SplunkListFiredAlertsParams,
SplunkListFiredAlertsResponse
> = {
id: 'splunk_list_fired_alerts',
name: 'Splunk List Fired Alerts',
description:
'List the saved searches with currently triggered (unexpired) Splunk alerts and how many times each has fired.',
version: '1.0.0',
params: {
...SPLUNK_CONNECTION_PARAMS,
count: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of entries to return (e.g. 50). 0 returns all.',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Index of the first entry to return, for pagination',
},
},
request: {
url: (params) =>
buildSplunkUrl(params, '/alerts/fired_alerts', {
count: params.count,
offset: params.offset,
}),
method: 'GET',
headers: (params) => buildSplunkHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
alerts: getSplunkEntries(data).map((entry) => {
const content = getEntryContent(entry)
return {
name: getEntryName(entry),
id: asString(entry.id),
updated: asString(entry.updated),
triggeredAlertCount: asNumber(content.triggered_alert_count),
}
}),
},
}
},
outputs: {
alerts: {
type: 'array',
description: 'Saved searches with currently triggered alerts',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Name of the alerting saved search' },
id: { type: 'string', description: 'Fully qualified REST URI of the entry' },
updated: { type: 'string', description: 'Last update timestamp' },
triggeredAlertCount: { type: 'number', description: 'Trigger count for this alert' },
},
},
},
},
}
+117
View File
@@ -0,0 +1,117 @@
import type { SplunkListIndexesParams, SplunkListIndexesResponse } from '@/tools/splunk/types'
import {
asBoolean,
asNumber,
asString,
buildSplunkHeaders,
buildSplunkUrl,
getEntryContent,
getEntryName,
getSplunkEntries,
SPLUNK_CONNECTION_PARAMS,
} from '@/tools/splunk/utils'
import type { ToolConfig } from '@/tools/types'
export const listIndexesTool: ToolConfig<SplunkListIndexesParams, SplunkListIndexesResponse> = {
id: 'splunk_list_indexes',
name: 'Splunk List Indexes',
description:
'List the indexes configured on the Splunk instance with their size, event count, and retention settings.',
version: '1.0.0',
params: {
...SPLUNK_CONNECTION_PARAMS,
datatype: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter indexes by type: all, event, or metric. Splunk defaults to event, so pass all to include metric indexes.',
},
count: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of indexes to return (e.g. 50). 0 returns all.',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Index of the first entry to return, for pagination',
},
},
request: {
url: (params) =>
buildSplunkUrl(params, '/data/indexes', {
datatype: params.datatype,
count: params.count,
offset: params.offset,
}),
method: 'GET',
headers: (params) => buildSplunkHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
indexes: getSplunkEntries(data).map((entry) => {
const content = getEntryContent(entry)
return {
name: getEntryName(entry),
id: asString(entry.id),
updated: asString(entry.updated),
datatype: asString(content.datatype),
disabled: asBoolean(content.disabled),
isInternal: asBoolean(content.isInternal),
totalEventCount: asNumber(content.totalEventCount),
currentDBSizeMB: asNumber(content.currentDBSizeMB),
maxTotalDataSizeMB: asNumber(content.maxTotalDataSizeMB),
frozenTimePeriodInSecs: asNumber(content.frozenTimePeriodInSecs),
minTime: asString(content.minTime),
maxTime: asString(content.maxTime),
homePath: asString(content.homePath),
coldPath: asString(content.coldPath),
thawedPath: asString(content.thawedPath),
}
}),
},
}
},
outputs: {
indexes: {
type: 'array',
description: 'Indexes configured on the instance',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Index name' },
id: { type: 'string', description: 'Fully qualified REST URI of the index' },
updated: { type: 'string', description: 'Last update timestamp' },
datatype: { type: 'string', description: 'Index data type (event or metric)' },
disabled: { type: 'boolean', description: 'Whether the index is disabled' },
isInternal: { type: 'boolean', description: 'Whether this is an internal Splunk index' },
totalEventCount: { type: 'number', description: 'Total number of events in the index' },
currentDBSizeMB: { type: 'number', description: 'Current index size in megabytes' },
maxTotalDataSizeMB: {
type: 'number',
description: 'Maximum index size in megabytes before rolling to frozen',
},
frozenTimePeriodInSecs: {
type: 'number',
description: 'Age in seconds at which data rolls to frozen',
},
minTime: { type: 'string', description: 'Timestamp of the earliest event in the index' },
maxTime: { type: 'string', description: 'Timestamp of the latest event in the index' },
homePath: { type: 'string', description: 'Path to the hot and warm buckets' },
coldPath: { type: 'string', description: 'Path to the cold buckets' },
thawedPath: { type: 'string', description: 'Path to the thawed buckets' },
},
},
},
},
}
@@ -0,0 +1,75 @@
import {
SAVED_SEARCH_OUTPUT_FIELDS,
type SplunkListSavedSearchesParams,
type SplunkListSavedSearchesResponse,
} from '@/tools/splunk/types'
import {
buildSplunkHeaders,
buildSplunkUrl,
getSplunkEntries,
mapSavedSearchEntry,
SPLUNK_CONNECTION_PARAMS,
savedSearchFieldQuery,
} from '@/tools/splunk/utils'
import type { ToolConfig } from '@/tools/types'
export const listSavedSearchesTool: ToolConfig<
SplunkListSavedSearchesParams,
SplunkListSavedSearchesResponse
> = {
id: 'splunk_list_saved_searches',
name: 'Splunk List Saved Searches',
description:
'List saved searches and reports configured in Splunk, including their SPL, schedule, and alert configuration.',
version: '1.0.0',
params: {
...SPLUNK_CONNECTION_PARAMS,
search: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter saved searches. A bare term matches as a substring across fields (e.g. Errors); field_name=field_value matches one field (e.g. is_scheduled=1).',
},
count: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of saved searches to return (e.g. 50). 0 returns all.',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Index of the first saved search to return, for pagination',
},
},
request: {
url: (params) =>
`${buildSplunkUrl(params, '/saved/searches', {
search: params.search,
count: params.count,
offset: params.offset,
})}&${savedSearchFieldQuery()}`,
method: 'GET',
headers: (params) => buildSplunkHeaders(params),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: { savedSearches: getSplunkEntries(data).map(mapSavedSearchEntry) },
}
},
outputs: {
savedSearches: {
type: 'array',
description: 'Saved searches configured in Splunk',
items: { type: 'object', properties: SAVED_SEARCH_OUTPUT_FIELDS },
},
},
}
+54
View File
@@ -0,0 +1,54 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { createSearchJobTool } from '@/tools/splunk/create_search_job'
import { getFiredAlertsTool } from '@/tools/splunk/get_fired_alerts'
import { listSavedSearchesTool } from '@/tools/splunk/list_saved_searches'
const BASE = { baseUrl: 'https://splunk.example.com:8089' }
function buildBody(tool: typeof createSearchJobTool, params: Record<string, unknown>): string {
const body = tool.request.body
if (!body) throw new Error('tool has no body builder')
return body(params as Parameters<NonNullable<typeof tool.request.body>>[0]) as unknown as string
}
describe('createSearchJobTool exec_mode', () => {
it('defaults to normal when the caller leaves it empty', () => {
expect(buildBody(createSearchJobTool, { ...BASE, search: 'index=main' })).toContain(
'exec_mode=normal'
)
})
it('rejects oneshot instead of returning a job with no search ID', () => {
expect(() =>
buildBody(createSearchJobTool, { ...BASE, search: 'index=main', execMode: 'oneshot' })
).toThrow(/cannot use exec_mode=oneshot/)
})
it('rejects an execution mode Splunk does not define', () => {
expect(() =>
buildBody(createSearchJobTool, { ...BASE, search: 'index=main', execMode: 'turbo' })
).toThrow(/Invalid Splunk execution mode/)
})
})
describe('listSavedSearchesTool', () => {
it('limits the response with f, which the reference prescribes for this endpoint', () => {
const url = listSavedSearchesTool.request.url({ ...BASE } as never)
expect(url).toContain('f=search')
expect(url).toContain('f=cron_schedule')
})
})
describe('getFiredAlertsTool', () => {
it('sends no pagination to an endpoint documented as taking no request parameters', () => {
const url = getFiredAlertsTool.request.url({ ...BASE, name: 'Errors' } as never)
expect(url).toBe(
'https://splunk.example.com:8089/services/alerts/fired_alerts/Errors?output_mode=json'
)
expect(url).not.toContain('count=')
expect(url).not.toContain('offset=')
})
})
+91
View File
@@ -0,0 +1,91 @@
import type { SplunkRunSearchParams, SplunkSearchResultsResponse } from '@/tools/splunk/types'
import {
buildSplunkFormBody,
buildSplunkFormHeaders,
buildSplunkUrl,
mapSearchResultsPayload,
normalizeSearchQuery,
SEARCH_RESULTS_OUTPUTS,
SPLUNK_CONNECTION_PARAMS,
} from '@/tools/splunk/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Runs SPL with `exec_mode=oneshot`, the documented synchronous mode in which
* `POST search/jobs` returns the results directly instead of a search ID.
*/
export const runSearchTool: ToolConfig<SplunkRunSearchParams, SplunkSearchResultsResponse> = {
id: 'splunk_run_search',
name: 'Splunk Run Search',
description:
'Run an SPL search synchronously and return its results in a single call (oneshot mode). Use for short searches; use Create Search Job for long-running ones.',
version: '1.0.0',
params: {
...SPLUNK_CONNECTION_PARAMS,
search: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'SPL search string (e.g. index=main error | stats count by host). The leading "search" command is added automatically when omitted.',
},
earliestTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Earliest (inclusive) time bound — relative (e.g. -24h, -7d@d) or absolute epoch/formatted time',
},
latestTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Latest (exclusive) time bound — relative (e.g. now) or absolute time',
},
adhocSearchLevel: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search mode: verbose, fast, or smart. Defaults to fast.',
},
autoCancel: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Cancel the search after this many seconds of inactivity (e.g. 60). 0 never auto-cancels.',
},
maxCount: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Maximum number of results the search stores and returns. Defaults to 10000. Lower it to bound large oneshot responses.',
},
},
request: {
url: (params) => buildSplunkUrl(params, '/search/jobs'),
method: 'POST',
headers: (params) => buildSplunkFormHeaders(params),
body: (params) =>
buildSplunkFormBody({
search: normalizeSearchQuery(params.search),
exec_mode: 'oneshot',
output_mode: 'json',
earliest_time: params.earliestTime,
latest_time: params.latestTime,
adhoc_search_level: params.adhocSearchLevel,
auto_cancel: params.autoCancel,
max_count: params.maxCount,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return { success: true, output: mapSearchResultsPayload(data) }
},
outputs: SEARCH_RESULTS_OUTPUTS,
}
+323
View File
@@ -0,0 +1,323 @@
import type { OutputProperty, ToolResponse } from '@/tools/types'
/** A single entry of the `messages` array Splunk attaches to REST responses. */
export interface SplunkMessage {
type: string | null
text: string | null
}
/**
* Connection and credential fields shared by every Splunk tool. `baseUrl` points at
* the splunkd management port (default 8089). Supply either an authentication token
* (bearer) or a username/password pair for basic authentication.
*/
export interface SplunkBaseParams {
baseUrl: string
authToken?: string
username?: string
password?: string
/** Namespace owner for `/servicesNS` requests (defaults to the token's user). */
owner?: string
/** Namespace app context for `/servicesNS` requests (e.g. `search`). */
app?: string
}
export interface SplunkRunSearchParams extends SplunkBaseParams {
search: string
earliestTime?: string
latestTime?: string
adhocSearchLevel?: string
autoCancel?: number
maxCount?: number
}
/**
* Documented envelope for search results: `init_offset`, `messages`, `preview`, and
* `results`. Row objects are the search's own fields, so their shape is dynamic.
*/
export interface SplunkSearchResultsResponse extends ToolResponse {
output: {
results: Record<string, unknown>[]
resultCount: number
preview: boolean | null
initOffset: number | null
messages: SplunkMessage[]
}
}
export interface SplunkCreateSearchJobParams extends SplunkBaseParams {
search: string
earliestTime?: string
latestTime?: string
execMode?: string
adhocSearchLevel?: string
searchId?: string
indexEarliest?: string
indexLatest?: string
enableLookups?: boolean
allowPartialResults?: boolean
autoCancel?: number
maxCount?: number
}
export interface SplunkCreateSearchJobResponse extends ToolResponse {
output: {
sid: string
}
}
export interface SplunkGetSearchJobParams extends SplunkBaseParams {
sid: string
}
export interface SplunkGetSearchJobResponse extends ToolResponse {
output: {
sid: string | null
label: string | null
dispatchState: string | null
doneProgress: number | null
isDone: boolean | null
isFailed: boolean | null
isFinalized: boolean | null
isPaused: boolean | null
isZombie: boolean | null
isSaved: boolean | null
isSavedSearch: boolean | null
isRealTimeSearch: boolean | null
eventCount: number | null
eventAvailableCount: number | null
eventFieldCount: number | null
resultCount: number | null
resultPreviewCount: number | null
scanCount: number | null
runDuration: number | null
priority: number | null
earliestTime: string | null
latestTime: string | null
searchEarliestTime: string | null
searchLatestTime: string | null
messages: Record<string, unknown> | null
}
}
export interface SplunkGetSearchResultsParams extends SplunkBaseParams {
sid: string
count?: number
offset?: number
fields?: string
addSummaryToMetadata?: boolean
}
export interface SplunkCancelSearchJobParams extends SplunkBaseParams {
sid: string
}
export interface SplunkCancelSearchJobResponse extends ToolResponse {
output: {
sid: string
messages: SplunkMessage[]
}
}
export interface SplunkSavedSearch {
name: string | null
id: string | null
author: string | null
updated: string | null
search: string | null
qualifiedSearch: string | null
description: string | null
disabled: boolean | null
isScheduled: boolean | null
isVisible: boolean | null
cronSchedule: string | null
nextScheduledTime: string | null
alertType: string | null
dispatchEarliestTime: string | null
dispatchLatestTime: string | null
}
export interface SplunkListSavedSearchesParams extends SplunkBaseParams {
search?: string
count?: number
offset?: number
}
export interface SplunkListSavedSearchesResponse extends ToolResponse {
output: {
savedSearches: SplunkSavedSearch[]
}
}
export interface SplunkGetSavedSearchParams extends SplunkBaseParams {
name: string
}
export interface SplunkGetSavedSearchResponse extends ToolResponse {
output: SplunkSavedSearch
}
export interface SplunkDispatchSavedSearchParams extends SplunkBaseParams {
name: string
triggerActions?: boolean
dispatchEarliestTime?: string
dispatchLatestTime?: string
dispatchMaxCount?: number
dispatchMaxTime?: number
dispatchTtl?: number
forceDispatch?: boolean
}
export interface SplunkDispatchSavedSearchResponse extends ToolResponse {
output: {
sid: string
}
}
export interface SplunkListFiredAlertsParams extends SplunkBaseParams {
count?: number
offset?: number
}
export interface SplunkListFiredAlertsResponse extends ToolResponse {
output: {
alerts: {
name: string | null
id: string | null
updated: string | null
triggeredAlertCount: number | null
}[]
}
}
export interface SplunkGetFiredAlertsParams extends SplunkBaseParams {
name: string
}
export interface SplunkGetFiredAlertsResponse extends ToolResponse {
output: {
firedAlerts: {
name: string | null
id: string | null
updated: string | null
savedSearchName: string | null
alertType: string | null
severity: number | null
sid: string | null
triggerTime: number | null
triggerTimeRendered: string | null
expirationTimeRendered: string | null
triggeredAlerts: number | null
actions: string | null
}[]
}
}
export interface SplunkListIndexesParams extends SplunkBaseParams {
datatype?: string
count?: number
offset?: number
}
export interface SplunkListIndexesResponse extends ToolResponse {
output: {
indexes: {
name: string | null
id: string | null
updated: string | null
datatype: string | null
disabled: boolean | null
isInternal: boolean | null
totalEventCount: number | null
currentDBSizeMB: number | null
maxTotalDataSizeMB: number | null
frozenTimePeriodInSecs: number | null
minTime: string | null
maxTime: string | null
homePath: string | null
coldPath: string | null
thawedPath: string | null
}[]
}
}
export interface SplunkListAppsParams extends SplunkBaseParams {
count?: number
offset?: number
}
export interface SplunkListAppsResponse extends ToolResponse {
output: {
apps: {
name: string | null
id: string | null
updated: string | null
label: string | null
version: string | null
author: string | null
description: string | null
details: string | null
disabled: boolean | null
visible: boolean | null
configured: boolean | null
checkForUpdates: boolean | null
stateChangeRequiresRestart: boolean | null
}[]
}
}
/** Union of every Splunk tool response, used by the block's generic output typing. */
export type SplunkResponse =
| SplunkSearchResultsResponse
| SplunkCreateSearchJobResponse
| SplunkGetSearchJobResponse
| SplunkCancelSearchJobResponse
| SplunkListSavedSearchesResponse
| SplunkGetSavedSearchResponse
| SplunkDispatchSavedSearchResponse
| SplunkListFiredAlertsResponse
| SplunkGetFiredAlertsResponse
| SplunkListIndexesResponse
| SplunkListAppsResponse
/** Output schema shared by the two tools that project a saved search. */
export const SAVED_SEARCH_OUTPUT_FIELDS: Record<string, OutputProperty> = {
name: { type: 'string', description: 'Saved search name' },
id: { type: 'string', description: 'Fully qualified REST URI of the saved search' },
author: { type: 'string', description: 'Owner of the saved search' },
updated: { type: 'string', description: 'Last update timestamp' },
search: { type: 'string', description: 'The SPL the saved search runs' },
qualifiedSearch: {
type: 'string',
description: 'The exact search string the scheduler runs',
optional: true,
},
description: { type: 'string', description: 'Saved search description', optional: true },
disabled: { type: 'boolean', description: 'Whether the saved search is disabled' },
isScheduled: { type: 'boolean', description: 'Whether the search runs on a schedule' },
isVisible: {
type: 'boolean',
description: 'Whether the search appears in the visible saved search list',
},
cronSchedule: { type: 'string', description: 'Cron schedule for the search', optional: true },
nextScheduledTime: {
type: 'string',
description: 'Time the scheduler runs this search again',
optional: true,
},
alertType: {
type: 'string',
description: 'Alert condition type (e.g. always, custom, number of events)',
optional: true,
},
dispatchEarliestTime: {
type: 'string',
description: 'Earliest time bound used when the search is dispatched',
optional: true,
},
dispatchLatestTime: {
type: 'string',
description: 'Latest time bound used when the search is dispatched',
optional: true,
},
}
+137
View File
@@ -0,0 +1,137 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import {
buildSplunkFormBody,
buildSplunkHeaders,
buildSplunkUrl,
normalizeSearchQuery,
readSplunkJson,
requireSplunkSid,
savedSearchFieldQuery,
} from '@/tools/splunk/utils'
const BASE = { baseUrl: 'https://splunk.example.com:8089' }
describe('buildSplunkUrl', () => {
it('pins output_mode=json and cannot be overridden by a caller', () => {
const url = buildSplunkUrl(BASE, '/search/jobs', { output_mode: 'xml' })
expect(url).toBe('https://splunk.example.com:8089/services/search/jobs?output_mode=json')
})
it('omits query values that an untouched subBlock supplies as null', () => {
const url = buildSplunkUrl(BASE, '/data/indexes', {
count: null,
offset: undefined,
datatype: '',
add_summary_to_metadata: null,
})
expect(url).not.toContain('null')
expect(url).toBe('https://splunk.example.com:8089/services/data/indexes?output_mode=json')
})
it('keeps explicit falsy values that Splunk treats as meaningful', () => {
const url = buildSplunkUrl(BASE, '/data/indexes', { count: 0, offset: 0 })
expect(url).toContain('count=0')
expect(url).toContain('offset=0')
})
it('uses the namespace prefix only when an owner or app is supplied', () => {
expect(buildSplunkUrl(BASE, '/saved/searches')).toContain('/services/saved/searches')
expect(buildSplunkUrl({ ...BASE, owner: 'admin', app: 'search' }, '/saved/searches')).toContain(
'/servicesNS/admin/search/saved/searches'
)
})
it('fills a half-specified namespace with the - wildcard, never nobody or search', () => {
const appOnly = buildSplunkUrl({ ...BASE, app: 'myapp' }, '/saved/searches')
expect(appOnly).toContain('/servicesNS/-/myapp/saved/searches')
expect(appOnly).not.toContain('nobody')
const ownerOnly = buildSplunkUrl({ ...BASE, owner: 'admin' }, '/saved/searches')
expect(ownerOnly).toContain('/servicesNS/admin/-/saved/searches')
})
})
describe('buildSplunkHeaders', () => {
it('prefers the bearer token over basic credentials', () => {
const headers = buildSplunkHeaders({ ...BASE, authToken: 'tok', username: 'u', password: 'p' })
expect(headers.Authorization).toBe('Bearer tok')
})
it('falls back to basic authentication', () => {
const headers = buildSplunkHeaders({ ...BASE, username: 'u', password: 'p' })
expect(headers.Authorization).toBe(`Basic ${Buffer.from('u:p').toString('base64')}`)
})
it('throws when neither credential form is supplied', () => {
expect(() => buildSplunkHeaders(BASE)).toThrow(/authentication token or a username/)
})
})
describe('readSplunkJson', () => {
it('returns an empty envelope for 204 No Content instead of throwing', async () => {
await expect(readSplunkJson(new Response(null, { status: 204 }))).resolves.toEqual({})
})
it('returns an empty envelope for a 200 with an empty body', async () => {
await expect(readSplunkJson(new Response(' ', { status: 200 }))).resolves.toEqual({})
})
it('still parses a real body', async () => {
await expect(readSplunkJson(new Response('{"results":[]}'))).resolves.toEqual({ results: [] })
})
})
describe('requireSplunkSid', () => {
it('reads the flat sid envelope', () => {
expect(requireSplunkSid({ sid: '1457683115.100' })).toBe('1457683115.100')
})
it('throws rather than reporting success with no search ID', () => {
expect(() => requireSplunkSid({})).toThrow(/did not return a search ID/)
})
})
describe('savedSearchFieldQuery', () => {
it('requests only the projected fields, with dotted keys encoded', () => {
const query = savedSearchFieldQuery()
expect(query).toContain('f=qualifiedSearch')
expect(query).toContain(`f=${encodeURIComponent('dispatch.earliest_time')}`)
expect(query).not.toContain('action.email')
})
})
describe('buildSplunkFormBody', () => {
it('drops null fields so Splunk applies its own documented default', () => {
const body = buildSplunkFormBody({
search: 'search index=main',
earliest_time: null,
trigger_actions: null,
force_dispatch: null,
enable_lookups: undefined,
})
expect(body).toBe('search=search+index%3Dmain')
expect(body).not.toContain('null')
})
it('serializes booleans as 1/0 when the user set them explicitly', () => {
expect(buildSplunkFormBody({ enable_lookups: true, allow_partial_results: false })).toBe(
'enable_lookups=1&allow_partial_results=0'
)
})
})
describe('normalizeSearchQuery', () => {
it('prefixes the implicit search command', () => {
expect(normalizeSearchQuery('index=main error')).toBe('search index=main error')
})
it('leaves an explicit search or generating command alone', () => {
expect(normalizeSearchQuery('search index=main')).toBe('search index=main')
expect(normalizeSearchQuery('| tstats count')).toBe('| tstats count')
})
})
+395
View File
@@ -0,0 +1,395 @@
import type { SplunkBaseParams, SplunkMessage, SplunkSavedSearch } from '@/tools/splunk/types'
import type { ToolConfig } from '@/tools/types'
/** Connection + credential params every Splunk tool declares. */
export const SPLUNK_CONNECTION_PARAMS: ToolConfig['params'] = {
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'Splunk management URL including the management port (e.g. https://splunk.example.com:8089)',
},
authToken: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Splunk authentication token, sent as a bearer token. Preferred over a password.',
},
username: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Splunk username, used for basic authentication when no token is supplied',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Splunk password, used for basic authentication when no token is supplied',
},
owner: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Namespace owner for /servicesNS requests (e.g. admin, or nobody for app-shared objects). Leave both this and the app empty to use the authenticated user context; set only one and the other becomes the - wildcard.',
},
app: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Namespace app context for /servicesNS requests (e.g. search). Leave both this and the owner empty to use the authenticated user context; set only one and the other becomes the - wildcard.',
},
}
/**
* Splunk defaults every REST response to XML. Every request Sim makes must pin
* `output_mode=json` or the transform layer receives XML it cannot parse.
*/
export const SPLUNK_JSON_OUTPUT_MODE = 'json'
/** Normalize the user-supplied management URL (e.g. `https://splunk.example.com:8089`). */
function normalizeBaseUrl(baseUrl: string): string {
const trimmed = baseUrl.trim().replace(/\/+$/, '')
if (!trimmed) {
throw new Error('Splunk base URL is required (e.g. https://splunk.example.com:8089)')
}
return trimmed
}
/**
* Build the namespace prefix for a REST path. Splunk exposes every configuration
* endpoint both at `/services/...`, where "the system processes the request using
* the active user user/app context", and at `/servicesNS/{owner}/{app}/...` when an
* explicit context is needed.
*
* When only one node is supplied the other is filled with the documented wildcard
* `-` — "To indicate all users, all apps, or resources shared by all users, use the
* wildcard dash (-) symbol". `nobody` is NOT a neutral filler: it names the
* shared-application owner, so using it would silently hide every user-private
* object in the requested app.
*/
function namespacePrefix(params: SplunkBaseParams): string {
const owner = params.owner?.trim()
const app = params.app?.trim()
if (!owner && !app) return '/services'
return `/servicesNS/${encodeURIComponent(owner || '-')}/${encodeURIComponent(app || '-')}`
}
/**
* Compose a fully qualified Splunk REST URL with `output_mode=json` always applied.
* `path` is namespace-relative and must start with a slash (e.g. `/search/jobs`).
*/
export function buildSplunkUrl(
params: SplunkBaseParams,
path: string,
query?: Record<string, string | number | boolean | null | undefined>
): string {
const search = new URLSearchParams()
for (const [key, value] of Object.entries(query ?? {})) {
if (key === 'output_mode' || value == null || value === '') continue
search.set(key, String(value))
}
search.set('output_mode', SPLUNK_JSON_OUTPUT_MODE)
return `${normalizeBaseUrl(params.baseUrl)}${namespacePrefix(params)}${path}?${search.toString()}`
}
/** Encode a user-supplied value for use as a REST path segment. */
export function splunkPathSegment(value: string): string {
return encodeURIComponent(value.trim())
}
/**
* Build auth headers. Splunk supports a bearer authentication token ("In version 7.3
* and higher of the Splunk platform, you can also use Splunk authentication tokens to
* access REST endpoints") and basic authentication; the token is preferred when both
* are supplied.
*/
export function buildSplunkHeaders(
params: SplunkBaseParams,
extra?: Record<string, string>
): Record<string, string> {
const headers: Record<string, string> = { Accept: 'application/json', ...extra }
const token = params.authToken?.trim()
if (token) {
headers.Authorization = `Bearer ${token}`
return headers
}
const username = params.username?.trim()
const password = params.password
if (username && password) {
headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
return headers
}
throw new Error('Splunk requires either an authentication token or a username and password')
}
/** Headers for the form-encoded POST bodies every Splunk write endpoint expects. */
export function buildSplunkFormHeaders(params: SplunkBaseParams): Record<string, string> {
return buildSplunkHeaders(params, { 'Content-Type': 'application/x-www-form-urlencoded' })
}
/**
* Serialize a form body, skipping empty values. Splunk expects booleans as 1/0.
* An untouched subBlock serializes as `null`, so `null` must be dropped exactly
* like `undefined` — otherwise the field is sent as the literal string `null`
* and overrides the default Splunk would otherwise apply.
*/
export function buildSplunkFormBody(
fields: Record<string, string | number | boolean | null | undefined>
): string {
const body = new URLSearchParams()
for (const [key, value] of Object.entries(fields)) {
if (value == null || value === '') continue
body.set(key, typeof value === 'boolean' ? (value ? '1' : '0') : String(value))
}
return body.toString()
}
/**
* Read a Splunk JSON body, tolerating an empty one. The results endpoint answers
* `204 No Content` with no body while a job has not produced results yet, and the job
* control endpoint documents "Returned values: None". `Response.json()` throws on an
* empty body, so a caller that polled a still-running job would surface
* `Unexpected end of JSON input` instead of an empty result set.
*/
export async function readSplunkJson(response: Response): Promise<unknown> {
if (response.status === 204) return {}
const text = await response.text()
if (!text.trim()) return {}
return JSON.parse(text)
}
/**
* Resolve the search ID a dispatching endpoint returns. The REST reference documents
* the response as `<response><sid>...</sid></response>` with a single returned value,
* `sid`, which `output_mode=json` renders as a flat `{ "sid": "..." }`.
*
* A missing `sid` is never a successful dispatch — it means the request produced
* something other than a job (for example `exec_mode=oneshot`, which the reference
* says "Does not return the search ID"). Fail loudly instead of handing the workflow
* a null sid that only breaks two blocks later.
*/
export function requireSplunkSid(data: unknown): string {
const sid = asString((data as { sid?: unknown })?.sid) ?? getEntryName(getSplunkEntries(data)[0])
if (!sid) {
throw new Error(
'Splunk did not return a search ID for this request. Verify the search dispatched successfully; exec_mode=oneshot returns results instead of a search ID.'
)
}
return sid
}
/**
* Splunk SPL sent to `search/jobs` must begin with a search command. Users routinely
* type a bare `index=main error`, so prefix the implicit `search` command unless the
* string already starts with one (`search ...`, `| tstats ...`, `search ...`).
*/
export function normalizeSearchQuery(query: string): string {
const trimmed = query.trim()
if (trimmed.startsWith('|') || /^search\s/i.test(trimmed) || /^search$/i.test(trimmed)) {
return trimmed
}
return `search ${trimmed}`
}
interface SplunkAtomEntry {
name?: string
title?: string
id?: string
updated?: string
author?: string | { name?: string }
content?: Record<string, unknown>
}
/**
* Splunk renders collection responses in an Atom-derived envelope. With
* `output_mode=json` the entries live at `entry[]`; the XML-to-JSON translation
* used in parts of the reference nests the same array under `feed`. Read both.
*/
export function getSplunkEntries(data: unknown): SplunkAtomEntry[] {
const root = (data ?? {}) as { entry?: unknown; feed?: { entry?: unknown } }
const entries = Array.isArray(root.entry)
? root.entry
: Array.isArray(root.feed?.entry)
? root.feed.entry
: []
return entries as SplunkAtomEntry[]
}
/** The per-entry property dictionary, or an empty object when the entry has none. */
export function getEntryContent(entry: SplunkAtomEntry | undefined): Record<string, unknown> {
return (entry?.content as Record<string, unknown>) ?? {}
}
/** Entry display name — `name` under `output_mode=json`, `title` in the Atom form. */
export function getEntryName(entry: SplunkAtomEntry | undefined): string | null {
return entry?.name ?? entry?.title ?? null
}
/** Entry author — a bare string under `output_mode=json`, an object in the Atom form. */
export function getEntryAuthor(entry: SplunkAtomEntry | undefined): string | null {
const author = entry?.author
if (typeof author === 'string') return author
return author?.name ?? null
}
export function asString(value: unknown): string | null {
return typeof value === 'string' ? value : null
}
export function asNumber(value: unknown): number | null {
if (typeof value === 'number') return Number.isFinite(value) ? value : null
if (typeof value === 'string' && value.trim() !== '') {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
return null
}
/** Splunk serializes booleans as `0`/`1` (and occasionally as real booleans). */
export function asBoolean(value: unknown): boolean | null {
if (typeof value === 'boolean') return value
if (typeof value === 'number') return value !== 0
if (typeof value === 'string') {
if (value === '1' || value.toLowerCase() === 'true') return true
if (value === '0' || value.toLowerCase() === 'false') return false
}
return null
}
/** Normalize the `messages` array Splunk attaches to results and control responses. */
export function mapSplunkMessages(value: unknown): SplunkMessage[] {
if (!Array.isArray(value)) return []
return value.map((message) => {
const record = (message ?? {}) as Record<string, unknown>
return { type: asString(record.type), text: asString(record.text) }
})
}
/**
* The `content` keys `mapSavedSearchEntry` reads, in the `f` filtering-parameter form
* the reference prescribes for the `saved/searches` collection: "This endpoint returns
* an unusually high number of values. To limit the number of returned values, specify
* the f filtering parameter." A saved search otherwise carries well over a hundred
* `content` keys (`action.email.*` alone runs to dozens) of which only these are
* projected, so a `count=0` listing pays for all of them on every entry.
*
* Only the collection endpoint documents this; the single-entity endpoint returns one
* entry, so it is left unfiltered rather than assuming `f` is honored there too.
*/
const SAVED_SEARCH_CONTENT_FIELDS = [
'search',
'qualifiedSearch',
'description',
'disabled',
'is_scheduled',
'is_visible',
'cron_schedule',
'next_scheduled_time',
'alert_type',
'dispatch.earliest_time',
'dispatch.latest_time',
] as const
/** Repeated `f=` arguments selecting only the saved-search fields Sim projects. */
export function savedSearchFieldQuery(): string {
return SAVED_SEARCH_CONTENT_FIELDS.map((field) => `f=${encodeURIComponent(field)}`).join('&')
}
/** Project one `saved/searches` Atom entry into the canonical saved-search shape. */
export function mapSavedSearchEntry(entry: unknown): SplunkSavedSearch {
const atomEntry = entry as SplunkAtomEntry | undefined
const content = getEntryContent(atomEntry)
return {
name: getEntryName(atomEntry),
id: asString(atomEntry?.id),
author: getEntryAuthor(atomEntry),
updated: asString(atomEntry?.updated),
search: asString(content.search),
qualifiedSearch: asString(content.qualifiedSearch),
description: asString(content.description),
disabled: asBoolean(content.disabled),
isScheduled: asBoolean(content.is_scheduled),
isVisible: asBoolean(content.is_visible),
cronSchedule: asString(content.cron_schedule),
nextScheduledTime: asString(content.next_scheduled_time),
alertType: asString(content.alert_type),
dispatchEarliestTime: asString(content['dispatch.earliest_time']),
dispatchLatestTime: asString(content['dispatch.latest_time']),
}
}
/**
* Project the documented search-results envelope (`init_offset`, `messages`,
* `preview`, `results`) into Sim's output shape. Result rows are the search's own
* fields, so they are passed through as dynamic objects.
*/
export function mapSearchResultsPayload(data: unknown) {
const root = (data ?? {}) as {
results?: unknown
messages?: unknown
preview?: unknown
init_offset?: unknown
}
const results = Array.isArray(root.results) ? (root.results as Record<string, unknown>[]) : []
return {
results,
resultCount: results.length,
preview: asBoolean(root.preview),
initOffset: asNumber(root.init_offset),
messages: mapSplunkMessages(root.messages),
}
}
/** Shared output schema for the two tools that return search results. */
export const SEARCH_RESULTS_OUTPUTS = {
results: {
type: 'array' as const,
description: 'Result rows. Each row holds the fields produced by the search.',
items: { type: 'object' as const },
},
resultCount: {
type: 'number' as const,
description: 'Number of result rows returned in this response',
},
preview: {
type: 'boolean' as const,
description: 'Whether these are preview results from a still-running job',
optional: true,
},
initOffset: {
type: 'number' as const,
description: 'Offset of the first returned row within the full result set',
optional: true,
},
messages: {
type: 'array' as const,
description: 'Search messages returned alongside the results',
items: {
type: 'object' as const,
properties: {
type: { type: 'string' as const, description: 'Message severity' },
text: { type: 'string' as const, description: 'Message text' },
},
},
},
}
/** Shared output schema for the `messages` envelope key. */
export const SPLUNK_MESSAGES_OUTPUT = {
type: 'array' as const,
description: 'Informational, warning, and error messages returned with the response',
items: {
type: 'object' as const,
properties: {
type: { type: 'string' as const, description: 'Message severity (INFO, WARN, ERROR, DEBUG)' },
text: { type: 'string' as const, description: 'Message text' },
},
},
}