diff --git a/components/splunk/actions/create-event/create-event.mjs b/components/splunk/actions/create-event/create-event.mjs new file mode 100644 index 0000000000000..41871e1410cfd --- /dev/null +++ b/components/splunk/actions/create-event/create-event.mjs @@ -0,0 +1,48 @@ +import splunk from "../../splunk.app.mjs"; + +export default { + key: "splunk-create-event", + name: "Create Event", + description: "Sends a new event to a specified Splunk index. [See the documentation](https://docs.splunk.com/Documentation/Splunk/9.4.1/RESTREF/RESTinput#receivers.2Fsimple)", + version: "0.0.1", + type: "action", + props: { + splunk, + indexName: { + propDefinition: [ + splunk, + "indexName", + ], + }, + eventData: { + type: "string", + label: "Event Data", + description: "The data of the event to send to the Splunk index. Raw event text. This is the entirety of the HTTP request body", + }, + source: { + type: "string", + label: "Source", + description: "The source value to fill in the metadata for this input's events", + optional: true, + }, + sourcetype: { + type: "string", + label: "Sourcetype", + description: "The sourcetype to apply to events from this input", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.splunk.sendEvent({ + $, + params: { + index: this.indexName, + source: this.source, + sourcetype: this.sourcetype, + }, + data: this.eventData, + }); + $.export("$summary", `Event sent to index ${this.indexName} successfully`); + return response; + }, +}; diff --git a/components/splunk/actions/get-search-job-status/get-search-job-status.mjs b/components/splunk/actions/get-search-job-status/get-search-job-status.mjs new file mode 100644 index 0000000000000..ca089ae46a7de --- /dev/null +++ b/components/splunk/actions/get-search-job-status/get-search-job-status.mjs @@ -0,0 +1,26 @@ +import splunk from "../../splunk.app.mjs"; + +export default { + key: "splunk-get-search-job-status", + name: "Get Search Job Status", + description: "Retrieve the status of a previously executed Splunk search job. [See the documentation](https://docs.splunk.com/Documentation/Splunk/9.4.1/RESTREF/RESTsearch#search.2Fjobs)", + version: "0.0.1", + type: "action", + props: { + splunk, + jobId: { + propDefinition: [ + splunk, + "jobId", + ], + }, + }, + async run({ $ }) { + const response = await this.splunk.getSearchJobStatus({ + $, + jobId: this.jobId, + }); + $.export("$summary", `Successfully retrieved status for job ID ${this.jobId}`); + return response; + }, +}; diff --git a/components/splunk/actions/run-search/run-search.mjs b/components/splunk/actions/run-search/run-search.mjs new file mode 100644 index 0000000000000..52ad935fa0dfb --- /dev/null +++ b/components/splunk/actions/run-search/run-search.mjs @@ -0,0 +1,26 @@ +import splunk from "../../splunk.app.mjs"; + +export default { + key: "splunk-run-search", + name: "Run Search", + description: "Executes a Splunk search query and returns the results. [See the documentation](https://docs.splunk.com/Documentation/Splunk/9.4.1/RESTREF/RESTsearch#search.2Fjobs)", + version: "0.0.1", + type: "action", + props: { + splunk, + name: { + propDefinition: [ + splunk, + "savedSearchName", + ], + }, + }, + async run({ $ }) { + const response = await this.splunk.executeSearchQuery({ + $, + name: this.name, + }); + $.export("$summary", `Executed Splunk search: ${this.name}`); + return response; + }, +}; diff --git a/components/splunk/package.json b/components/splunk/package.json index 8fee8316f5794..bc2b77303da01 100644 --- a/components/splunk/package.json +++ b/components/splunk/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/splunk", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Splunk Components", "main": "splunk.app.mjs", "keywords": [ @@ -11,5 +11,10 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "https": "^1.0.0", + "md5": "^2.3.0" } -} \ No newline at end of file +} diff --git a/components/splunk/sources/common/base.mjs b/components/splunk/sources/common/base.mjs new file mode 100644 index 0000000000000..a0088f7cdf538 --- /dev/null +++ b/components/splunk/sources/common/base.mjs @@ -0,0 +1,27 @@ +import splunk from "../../splunk.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + splunk, + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + db: "$.service.db", + }, + methods: { + async getRecentJobIds() { + const results = this.splunk.paginate({ + resourceFn: this.splunk.listJobs, + }); + const jobIds = []; + for await (const job of results) { + jobIds.push(job.content.sid); + } + return jobIds; + }, + }, +}; diff --git a/components/splunk/sources/new-alert-fired/new-alert-fired.mjs b/components/splunk/sources/new-alert-fired/new-alert-fired.mjs new file mode 100644 index 0000000000000..9551057e47be0 --- /dev/null +++ b/components/splunk/sources/new-alert-fired/new-alert-fired.mjs @@ -0,0 +1,68 @@ +import splunk from "../../splunk.app.mjs"; +import { exec } from "child_process"; +import util from "util"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "splunk-new-alert-fired", + name: "New Alert Fired (Instant)", + description: "Emit new event when a new alert is triggered in Splunk. [See the documentation](https://docs.splunk.com/Documentation/Splunk/9.4.1/RESTREF/RESTsearch#alerts.2Ffired_alerts)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + splunk, + http: "$.interface.http", + db: "$.service.db", + savedSearchName: { + propDefinition: [ + splunk, + "savedSearchName", + ], + }, + }, + hooks: { + async activate() { + const response = await this.updateSavedSearch(`-d action.webhook=1 -d action.webhook.param.url="${this.http.endpoint}"`); + if (!response) { + throw new Error("Error creating webhook"); + } + }, + async deactivate() { + const response = await this.updateSavedSearch("-d action.webhook=0"); + if (!response) { + throw new Error("Error disabling webhook"); + } + }, + }, + methods: { + async updateSavedSearch(data) { + const cmd = `curl -X POST ${this.splunk._baseUrl()}/saved/searches/${encodeURIComponent(this.savedSearchName)}?output_mode=json -k -H "Authorization: Bearer ${this.splunk.$auth.api_token}" ${data}`; + const execPromise = util.promisify(exec); + try { + const { stdout } = await execPromise(cmd); + return stdout; + } catch (error) { + console.error("Error:", error.message); + } + }, + generateMeta(alert) { + const ts = +alert.result._time; + return { + id: ts, + summary: `New Alert Fired for Source: ${alert.result.source}`, + ts, + }; + }, + }, + async run(event) { + const { body } = event; + if (!body) { + return; + } + + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, + sampleEmit, +}; diff --git a/components/splunk/sources/new-alert-fired/test-event.mjs b/components/splunk/sources/new-alert-fired/test-event.mjs new file mode 100644 index 0000000000000..d7c006b369832 --- /dev/null +++ b/components/splunk/sources/new-alert-fired/test-event.mjs @@ -0,0 +1,30 @@ +export default { + "sid": "", + "search_name": "", + "app": "search", + "owner": "", + "results_link": "https://splunk:8000/app/search/search?q=", + "result": { + "_confstr": "source::Source|host::44.210.81.125|webhook", + "_eventtype_color": "", + "_indextime": "1742843623", + "_raw": "{ \"name\": \"test\", \"value\": \"test\" }", + "_serial": "3", + "_si": [ + "main" + ], + "_sourcetype": "webhook", + "_time": "1742843623", + "eventtype": "", + "host": "44.210.81.125", + "index": "main", + "linecount": "", + "name": "test", + "punct": "{_\"\":_\"_\",_\"\":_\"\"_}", + "source": "Source", + "sourcetype": "webhook", + "splunk_server": "", + "timestamp": "none", + "value": "test" + } +} \ No newline at end of file diff --git a/components/splunk/sources/new-search-event/new-search-event.mjs b/components/splunk/sources/new-search-event/new-search-event.mjs new file mode 100644 index 0000000000000..491e6ff9729fb --- /dev/null +++ b/components/splunk/sources/new-search-event/new-search-event.mjs @@ -0,0 +1,42 @@ +import common from "../common/base.mjs"; +import md5 from "md5"; + +export default { + ...common, + key: "splunk-new-search-event", + name: "New Search Event", + description: "Emit new event when a new search event is created. [See the documentation](https://docs.splunk.com/Documentation/Splunk/9.4.1/RESTREF/RESTsearch#search.2Fv2.2Fjobs.2F.7Bsearch_id.7D.2Fevents)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + generateMeta(event) { + return { + id: md5(JSON.stringify(event)), + summary: "New Search Event", + ts: Date.now(), + }; + }, + }, + async run() { + const jobIds = await this.getRecentJobIds(); + const events = []; + for (const jobId of jobIds) { + try { + const response = await this.splunk.getSearchEvents({ + jobId, + }); + if (response?.results?.length) { + events.push(...response.results); + } + } catch { + console.log(`No events found for sid: ${jobId}`); + } + } + events.forEach((event) => { + const meta = this.generateMeta(event); + this.$emit(event, meta); + }); + }, +}; diff --git a/components/splunk/sources/new-search-result/new-search-result.mjs b/components/splunk/sources/new-search-result/new-search-result.mjs new file mode 100644 index 0000000000000..b4fe08d6083c5 --- /dev/null +++ b/components/splunk/sources/new-search-result/new-search-result.mjs @@ -0,0 +1,48 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "splunk-new-search-result", + name: "New Search Result", + description: "Emit new events when a search returns results in Splunk. [See the documentation](https://docs.splunk.com/Documentation/Splunk/9.4.1/RESTREF/RESTsearch#saved.2Fsearches)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + async getRecentJobs() { + const jobs = []; + const results = this.splunk.paginate({ + resourceFn: this.splunk.listJobs, + }); + for await (const job of results) { + jobs.push(job); + } + return jobs; + }, + generateMeta(result) { + return { + id: result.id, + summary: `New Search with ID: ${result.id}`, + ts: Date.now(), + }; + }, + }, + async run() { + const jobs = await this.getRecentJobs(); + for (const job of jobs) { + if (job.content?.resultCount > 0) { + const { results } = await this.splunk.getSearchResults({ + jobId: job.content.sid, + }); + if (results) { + job.results = results; + } + } + } + jobs.forEach((result) => { + const meta = this.generateMeta(result); + this.$emit(result, meta); + }); + }, +}; diff --git a/components/splunk/splunk.app.mjs b/components/splunk/splunk.app.mjs index 13ba053e30d09..730c4acc70cb3 100644 --- a/components/splunk/splunk.app.mjs +++ b/components/splunk/splunk.app.mjs @@ -1,11 +1,200 @@ +import { axios } from "@pipedream/platform"; +import https from "https"; +const DEFAULT_LIMIT = 50; + export default { type: "app", app: "splunk", - propDefinitions: {}, + propDefinitions: { + jobId: { + type: "string", + label: "Search ID", + description: "The ID of the Splunk search job to retrieve status for", + async options({ page }) { + const { entry } = await this.listJobs({ + params: { + count: DEFAULT_LIMIT, + offset: DEFAULT_LIMIT * page, + }, + }); + return entry?.map(({ + name: label, content, + }) => ({ + label, + value: content.sid, + })) || []; + }, + }, + indexName: { + type: "string", + label: "Index Name", + description: "The name of the Splunk index", + async options({ page }) { + const { entry } = await this.listIndexes({ + params: { + count: DEFAULT_LIMIT, + offset: DEFAULT_LIMIT * page, + }, + }); + return entry?.map(({ name }) => name) || []; + }, + }, + savedSearchName: { + type: "string", + label: "Saved Search Name", + description: "The name of a saved search", + async options({ page }) { + const { entry } = await this.listSavedSearches({ + params: { + count: DEFAULT_LIMIT, + offset: DEFAULT_LIMIT * page, + }, + }); + return entry?.map(({ name }) => name) || []; + }, + }, + query: { + type: "string", + label: "Search Query", + description: "The Splunk search query. Example: `search *`. [See the documentation](https://docs.splunk.com/Documentation/Splunk/9.4.1/SearchReference/Search) for more information about search command sytax.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `${this.$auth.api_url}:${this.$auth.api_port}/services`; + }, + _makeRequest({ + $ = this, + path, + params, + ...otherOpts + }) { + const config = { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.api_token}`, + }, + params: { + ...params, + output_mode: "json", + }, + }; + if (this.$auth.self_signed) { + config.httpsAgent = new https.Agent({ + rejectUnauthorized: false, + }); + } + return axios($, config); + }, + listJobs(opts = {}) { + return this._makeRequest({ + path: "/search/jobs", + ...opts, + }); + }, + listIndexes(opts = {}) { + return this._makeRequest({ + path: "/data/indexes", + ...opts, + }); + }, + listSavedSearches(opts = {}) { + return this._makeRequest({ + path: "/saved/searches", + ...opts, + }); + }, + listFiredAlerts(opts = {}) { + return this._makeRequest({ + path: "/alerts/fired_alerts", + ...opts, + }); + }, + updateSavedSearch({ + name, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/saved/searches/${name}`, + ...opts, + }); + }, + getSavedSearch({ + name, ...opts + }) { + return this._makeRequest({ + path: `/saved/searches/${name}`, + ...opts, + }); + }, + executeSearchQuery({ + name, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/saved/searches/${name}/dispatch`, + ...opts, + }); + }, + getSearchJobStatus({ + jobId, ...opts + }) { + return this._makeRequest({ + path: `/search/jobs/${jobId}`, + ...opts, + }); + }, + getSearchResults({ + jobId, ...opts + }) { + return this._makeRequest({ + path: `/search/v2/jobs/${jobId}/results`, + ...opts, + }); + }, + getSearchEvents({ + jobId, ...opts + }) { + return this._makeRequest({ + path: `/search/v2/jobs/${jobId}/events`, + ...opts, + }); + }, + sendEvent(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/receivers/simple", + ...opts, + }); + }, + async *paginate({ + resourceFn, + args, + max, + }) { + args = { + ...args, + params: { + ...args?.params, + count: DEFAULT_LIMIT, + }, + }; + let hasMore, count = 0; + do { + const { + entry, paging, + } = await resourceFn(args); + for (const item of entry) { + yield item; + count++; + if (max && count >= max) { + return; + } + } + hasMore = paging.total > count; + args.params.offset += args.params.count; + } while (hasMore); }, }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a17809d5bc33..69e623534703b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7146,8 +7146,7 @@ importers: specifier: ^6.11.1 version: 6.13.1 - components/line_messaging_api: - specifiers: {} + components/line_messaging_api: {} components/linear: dependencies: @@ -12065,7 +12064,17 @@ importers: specifier: ^1.2.0 version: 1.6.6 - components/splunk: {} + components/splunk: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 + https: + specifier: ^1.0.0 + version: 1.0.0 + md5: + specifier: ^2.3.0 + version: 2.3.0 components/splunk_http_event_collector: {} @@ -23414,6 +23423,9 @@ packages: resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} engines: {node: '>= 14'} + https@1.0.0: + resolution: {integrity: sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -41015,6 +41027,8 @@ snapshots: transitivePeerDependencies: - supports-color + https@1.0.0: {} + human-signals@2.1.0: {} human-signals@5.0.0: {}