From e9e720f1864f22212443533f1733c1eedaf14199 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Hinse Date: Wed, 15 Jan 2025 13:51:32 -0500 Subject: [PATCH] Rewrote the interfaces of the Splunk object to follow the hierarchy of the Javascript SplunkSDK Will be used to simplify the creation of a wrapper to generate Promises on top of the JS SDK --- .../react-components/src/StatusScreen.tsx | 8 +- .../react-components/src/models/constants.ts | 4 +- .../react-components/src/models/splunk.ts | 115 ++++++------------ .../src/utils/configurationFileHelper.ts | 98 ++++++--------- .../src/utils/setupConfiguration.ts | 52 ++++---- 5 files changed, 105 insertions(+), 172 deletions(-) diff --git a/packages/react-components/src/StatusScreen.tsx b/packages/react-components/src/StatusScreen.tsx index 63b3d62..0b1beb6 100644 --- a/packages/react-components/src/StatusScreen.tsx +++ b/packages/react-components/src/StatusScreen.tsx @@ -1,7 +1,7 @@ import React, { FC, useEffect, useState } from 'react'; import Button from './components/Button'; import './global.css'; -import { SplunkCollectionItem } from './models/splunk'; +import { KVCollectionItem } from './models/splunk'; import './StatusScreen.css'; import { fetchCollectionItems, @@ -37,7 +37,7 @@ const StatusScreen: FC<{ theme: string }> = ({ theme }) => { fetchCollectionItems(), fetchVersionName('unknown'), fetchCurrentIndexName(), - ]).then(([id, splunkCollectionItems, version, indexName]) => { + ]).then(([id, kvCollectionItems, version, indexName]) => { const items: StatusItem[] = [ { key: StatusItemKeys.VERSION, @@ -57,7 +57,7 @@ const StatusScreen: FC<{ theme: string }> = ({ theme }) => { value: `${id}`, }, ]; - splunkCollectionItems.forEach((item) => { + kvCollectionItems.forEach((item) => { if (item.key === StatusItemKeys.LAST_FETCHED) { items.push({ key: item.key.startsWith(CollectionKeys.NEXT_PREFIX) @@ -118,7 +118,7 @@ const StatusScreen: FC<{ theme: string }> = ({ theme }) => { return 'Unknown Status Item'; } - function formatValue(item: SplunkCollectionItem): string { + function formatValue(item: KVCollectionItem): string { if (item.key === StatusItemKeys.START_DATE || item.key === StatusItemKeys.LAST_FETCHED) { const date = new Date(item.value); return date.toLocaleString(); diff --git a/packages/react-components/src/models/constants.ts b/packages/react-components/src/models/constants.ts index 2dbf291..b93d2e2 100644 --- a/packages/react-components/src/models/constants.ts +++ b/packages/react-components/src/models/constants.ts @@ -1,8 +1,8 @@ -import { SplunkApplicationNamespace } from './splunk'; +import { ApplicationNamespace } from './splunk'; export const APP_NAME = 'flare'; export const STORAGE_REALM = 'flare_integration_realm'; -export const APPLICATION_NAMESPACE: SplunkApplicationNamespace = { +export const APPLICATION_NAMESPACE: ApplicationNamespace = { owner: 'nobody', app: APP_NAME, sharing: 'app', diff --git a/packages/react-components/src/models/splunk.ts b/packages/react-components/src/models/splunk.ts index 96267f8..84dbaa3 100644 --- a/packages/react-components/src/models/splunk.ts +++ b/packages/react-components/src/models/splunk.ts @@ -1,103 +1,66 @@ -export interface SplunkApplicationNamespace { - app?: string; - owner?: string; - sharing?: string; -} - -export interface SplunkPassword { - name: string; - _properties: { - clear_password: string; - }; -} - -export interface SplunkCollectionItem { +export interface KVCollectionItem { key: string; value: string; user: string; } -export interface SplunkIndex { - name: string; -} - -export interface SplunkSavedSearch { - name: string; - qualifiedPath: string; - update: (properties: Record) => SplunkRequestResponse; -} - -export interface SplunkAppAccessor { - reload: () => void; -} - -export interface Stanza { - name: string; +export interface ApplicationNamespace { + app?: string; + owner?: string; + sharing?: string; } -export interface SplunkRequestResponse { +export interface HTTPResponse { status: number; data: any; } -export interface SplunkAppsAccessor { - fetch: () => SplunkAppsAccessor; - item: (applicationName: string) => SplunkAppAccessor; +export interface Endpoint { + fetch: () => this; + del: (relativePath: string) => HTTPResponse; + qualifiedPath: string; } -export interface ConfigurationStanzaAccessor { - fetch: () => ConfigurationStanzaAccessor; - update: (properties: Record) => SplunkRequestResponse; - list: () => Array<{ name: string }>; +export interface Resource extends Endpoint { + fetch: () => this; properties: () => Record; - _properties: Record; } -export interface ConfigurationFileAccessor { - create: (stanzaName: string) => SplunkRequestResponse; - fetch: () => ConfigurationFileAccessor; - item: ( - stanzaName: string, - properties: SplunkApplicationNamespace - ) => ConfigurationStanzaAccessor; - list: () => Array<{ name: string }>; +export interface Entity extends Resource { + reload: () => void; + update: (properties: Record) => HTTPResponse; + name: string; } -export interface ConfigurationsAccessor { - fetch: () => ConfigurationsAccessor; - create: (configurationFilename: string) => SplunkRequestResponse; - item: (stanzaName: string, properties: SplunkApplicationNamespace) => ConfigurationFileAccessor; - list: () => Array<{ name: string }>; +export interface Collection> extends Resource { + item: (itemName: string, namespace: ApplicationNamespace) => T; + list: () => Array; +} + +export interface ConfigurationFile extends Collection { + create: (stanzaName: string) => HTTPResponse; + name: string; } -export interface SplunkIndexesAccessor { - fetch: () => SplunkIndexesAccessor; - create: (indexName: string, data: any) => SplunkRequestResponse; - item: (indexName: string) => SplunkIndex; - list: () => Array; +export interface Configurations extends Collection { + create: (configurationFilename: string) => HTTPResponse; } -export interface SplunkSavedSearchAccessor { - fetch: () => SplunkSavedSearchAccessor; - create: (indexName: string, data: any) => SplunkRequestResponse; - item: (indexName: string) => SplunkSavedSearch; - list: () => Array; +export interface Indexes extends Collection { + create: (indexName: string, data: any) => HTTPResponse; } -export interface SplunkStoragePasswordAccessors { - fetch: () => SplunkStoragePasswordAccessors; - item: (applicationName: string) => SplunkAppAccessor; - list: () => Array; - del: (passwordId: string) => SplunkRequestResponse; - create: (params: { name: string; realm: string; password: string }) => SplunkRequestResponse; +export interface StoragePasswords extends Collection { + create: (params: { name: string; realm: string; password: string }) => HTTPResponse; } -export interface SplunkService { - configurations: (params: SplunkApplicationNamespace) => ConfigurationsAccessor; - apps: () => SplunkAppsAccessor; - storagePasswords: () => SplunkStoragePasswordAccessors; - indexes: () => SplunkIndexesAccessor; - savedSearches: () => SplunkSavedSearchAccessor; - get: (splunkUrlPath: string, data: any) => SplunkRequestResponse; - post: (splunkUrlPath: string, data: any) => SplunkRequestResponse; +export interface Service { + configurations: (params: ApplicationNamespace) => Configurations; + apps: () => Collection; + storagePasswords: () => StoragePasswords; + indexes: () => Indexes; + savedSearches: () => Collection; + serverInfo: () => any; + get: (splunkUrlPath: string, data: any) => HTTPResponse; + post: (splunkUrlPath: string, data: any) => HTTPResponse; } diff --git a/packages/react-components/src/utils/configurationFileHelper.ts b/packages/react-components/src/utils/configurationFileHelper.ts index 09ef5fb..4d36530 100644 --- a/packages/react-components/src/utils/configurationFileHelper.ts +++ b/packages/react-components/src/utils/configurationFileHelper.ts @@ -1,9 +1,5 @@ -import { - ConfigurationFileAccessor, - ConfigurationsAccessor, - ConfigurationStanzaAccessor, - SplunkService, -} from '../models/splunk'; +import { APPLICATION_NAMESPACE } from '../models/constants'; +import { ConfigurationFile, Configurations, Entity, HTTPResponse, Service } from '../models/splunk'; import { promisify } from './util'; // ---------------------------------- @@ -13,10 +9,10 @@ import { promisify } from './util'; // Existence Functions // --------------------- function doesConfigurationExist( - configurationsAccessor: ConfigurationsAccessor, + configurations: Configurations, configurationFilename: string ): boolean { - for (const stanza of configurationsAccessor.list()) { + for (const stanza of configurations.list()) { if (stanza.name === configurationFilename) { return true; } @@ -26,7 +22,7 @@ function doesConfigurationExist( } function doesStanzaExist( - configurationFileAccessor: ConfigurationFileAccessor, + configurationFileAccessor: ConfigurationFile, stanzaName: string ): boolean { for (const stanza of configurationFileAccessor.list()) { @@ -42,39 +38,33 @@ function doesStanzaExist( // Retrieval Functions // --------------------- function getConfigurationFile( - configurationsAccessor: ConfigurationsAccessor, + configurations: Configurations, configurationFilename: string -): ConfigurationFileAccessor { - const configurationFileAccessor = configurationsAccessor.item(configurationFilename, { - // Name space information not provided - }); - - return configurationFileAccessor; +): Promise { + return promisify(configurations.item(configurationFilename, APPLICATION_NAMESPACE).fetch)(); } function getConfigurationFileStanza( - configurationFileAccessor: ConfigurationFileAccessor, + configurationFile: ConfigurationFile, configurationStanzaName: string -): ConfigurationStanzaAccessor { - const configurationStanzaAccessor = configurationFileAccessor.item(configurationStanzaName, { - // Name space information not provided - }); - - return configurationStanzaAccessor; +): Promise { + return promisify( + configurationFile.item(configurationStanzaName, APPLICATION_NAMESPACE).fetch + )(); } function createStanza( - configurationFileAccessor: ConfigurationFileAccessor, + configurationFile: ConfigurationFile, newStanzaName: string -): Promise { - return promisify(configurationFileAccessor.create)(newStanzaName); +): Promise { + return promisify(configurationFile.create)(newStanzaName); } function updateStanzaProperties( - configurationStanzaAccessor: ConfigurationStanzaAccessor, + configurationStanza: Entity, newStanzaProperties: Record -): Promise { - return promisify(configurationStanzaAccessor.update)(newStanzaProperties); +): Promise { + return promisify(configurationStanza.update)(newStanzaProperties); } // --------------------- @@ -82,22 +72,20 @@ function updateStanzaProperties( // --------------------- function createConfigurationFile( - configurationsAccessor: ConfigurationsAccessor, + configurations: Configurations, configurationFilename: string -): Promise { - return promisify(configurationsAccessor.create)(configurationFilename); +): Promise { + return promisify(configurations.create)(configurationFilename); } export async function updateConfigurationFile( - service: SplunkService, + service: Service, configurationFilename: string, stanzaName: string, properties: Record ): Promise { // Fetch the accessor used to get a configuration file - let configurations = service.configurations({ - // Name space information not provided - }); + let configurations = service.configurations(APPLICATION_NAMESPACE); configurations = await promisify(configurations.fetch)(); // Check for the existence of the configuration file @@ -111,61 +99,53 @@ export async function updateConfigurationFile( } // Fetchs the configuration file accessor - let configurationFileAccessor = getConfigurationFile(configurations, configurationFilename); - configurationFileAccessor = await promisify(configurationFileAccessor.fetch)(); + let configurationFile = await getConfigurationFile(configurations, configurationFilename); // Checks to see if the stanza where the inputs will be // stored exist - const stanzaExist = doesStanzaExist(configurationFileAccessor, stanzaName); + const stanzaExist = doesStanzaExist(configurationFile, stanzaName); // If the configuration stanza doesn't exist, create it if (!stanzaExist) { - await createStanza(configurationFileAccessor, stanzaName); + await createStanza(configurationFile, stanzaName); + + // Need to update the information after the creation of the stanza + configurationFile = await promisify(configurationFile.fetch)(); } - // Need to update the information after the creation of the stanza - configurationFileAccessor = await promisify(configurationFileAccessor.fetch)(); // Fetchs the configuration stanza accessor - let configurationStanzaAccessor = getConfigurationFileStanza( - configurationFileAccessor, - stanzaName - ); - configurationStanzaAccessor = await promisify(configurationStanzaAccessor.fetch)(); + const configurationStanza = await getConfigurationFileStanza(configurationFile, stanzaName); // We don't care if the stanza property does or doesn't exist // This is because we can use the // configurationStanza.update() function to create and // change the information of a property - await updateStanzaProperties(configurationStanzaAccessor, properties); + await updateStanzaProperties(configurationStanza, properties); } export async function getConfigurationStanzaValue( - service: SplunkService, + service: Service, configurationFilename: string, stanzaName: string, propertyName: string, defaultValue: string ): Promise { // Fetch the accessor used to get a configuration file - let configurations = service.configurations({ - // Name space information not provided - }); + let configurations = service.configurations(APPLICATION_NAMESPACE); configurations = await promisify(configurations.fetch)(); // Fetchs the configuration file accessor - let configurationFileAccessor = getConfigurationFile(configurations, configurationFilename); - configurationFileAccessor = await promisify(configurationFileAccessor.fetch)(); + const configurationFile = await getConfigurationFile(configurations, configurationFilename); // Fetchs the configuration stanza accessor - let configurationStanzaAccessor = getConfigurationFileStanza( - configurationFileAccessor, + const configurationStanzaAccessor = await getConfigurationFileStanza( + configurationFile, stanzaName ); - configurationStanzaAccessor = await promisify(configurationStanzaAccessor.fetch)(); let propertyValue = defaultValue; - if (propertyName in configurationStanzaAccessor._properties) { - propertyValue = configurationStanzaAccessor._properties[propertyName]; + if (propertyName in configurationStanzaAccessor.properties()) { + propertyValue = configurationStanzaAccessor.properties()[propertyName]; } return propertyValue; diff --git a/packages/react-components/src/utils/setupConfiguration.ts b/packages/react-components/src/utils/setupConfiguration.ts index 52f4050..0bf9d99 100644 --- a/packages/react-components/src/utils/setupConfiguration.ts +++ b/packages/react-components/src/utils/setupConfiguration.ts @@ -10,26 +10,21 @@ import { STORAGE_REALM, } from '../models/constants'; import { Severity, SourceType, SourceTypeCategory, Tenant } from '../models/flare'; -import { - SplunkCollectionItem, - SplunkRequestResponse, - SplunkService, - SplunkStoragePasswordAccessors, -} from '../models/splunk'; +import { KVCollectionItem, HTTPResponse, Service, StoragePasswords } from '../models/splunk'; import { getConfigurationStanzaValue, updateConfigurationFile } from './configurationFileHelper'; import { promisify } from './util'; -async function completeSetup(splunkService: SplunkService): Promise { +async function completeSetup(splunkService: Service): Promise { await updateConfigurationFile(splunkService, 'app', 'install', { is_configured: 'true', }); } -async function reloadApp(splunkService: SplunkService): Promise { +async function reloadApp(splunkService: Service): Promise { const splunkApps = splunkService.apps(); await promisify(splunkApps.fetch)(); - const currentApp = splunkApps.item(APP_NAME); + const currentApp = splunkApps.item(APP_NAME, APPLICATION_NAMESPACE); await promisify(currentApp.reload)(); } @@ -40,7 +35,7 @@ function getRedirectUrl(): string { async function getFlareSearchDataUrl(): Promise { const service = createService(); const savedSearches = await promisify(service.savedSearches().fetch)(); - const savedSearch = savedSearches.item(FLARE_SAVED_SEARCH_NAME); + const savedSearch = savedSearches.item(FLARE_SAVED_SEARCH_NAME, APPLICATION_NAMESPACE); return `/app/${APP_NAME}/@go?s=${savedSearch.qualifiedPath}`; } @@ -48,13 +43,12 @@ function redirectToHomepage(): void { window.location.href = getRedirectUrl(); } -function createService(): SplunkService { +function createService(): Service { // The splunkjs is injected by Splunk // eslint-disable-next-line no-undef const http = new splunkjs.SplunkWebHttp(); // eslint-disable-next-line no-undef - const service = new splunkjs.Service(http, APPLICATION_NAMESPACE); - + const service: Service = new splunkjs.Service(http, APPLICATION_NAMESPACE); return service; } @@ -62,7 +56,7 @@ function fetchApiKeyValidation(apiKey: string): Promise { const service = createService(); const data = { apiKey }; return promisify(service.post)('/services/fetch_api_key_validation', data).then( - (response: SplunkRequestResponse) => { + (response: HTTPResponse) => { return response.status === 200; } ); @@ -72,7 +66,7 @@ function fetchUserTenants(apiKey: string): Promise> { const service = createService(); const data = { apiKey }; return promisify(service.post)('/services/fetch_user_tenants', data).then( - (response: SplunkRequestResponse) => { + (response: HTTPResponse) => { return response.data.tenants; } ); @@ -82,7 +76,7 @@ function fetchSeverityFilters(apiKey: string): Promise> { const service = createService(); const data = { apiKey }; return promisify(service.post)('/services/fetch_severity_filters', data).then( - (response: SplunkRequestResponse) => { + (response: HTTPResponse) => { return response.data.severities; } ); @@ -92,13 +86,13 @@ function fetchSourceTypeFilters(apiKey: string): Promise { + (response: HTTPResponse) => { return response.data.categories; } ); } -function doesPasswordExist(storage: SplunkStoragePasswordAccessors, key: string): boolean { +function doesPasswordExist(storage: StoragePasswords, key: string): boolean { const passwordId = `${STORAGE_REALM}:${key}:`; for (const password of storage.list()) { @@ -109,11 +103,7 @@ function doesPasswordExist(storage: SplunkStoragePasswordAccessors, key: string) return false; } -async function savePassword( - storage: SplunkStoragePasswordAccessors, - key: string, - value: string -): Promise { +async function savePassword(storage: StoragePasswords, key: string, value: string): Promise { const passwordExists = doesPasswordExist(storage, key); if (passwordExists) { const passwordId = `${STORAGE_REALM}:${key}:`; @@ -171,7 +161,7 @@ async function saveConfiguration( } async function updateEventIngestionCronJobInterval( - service: SplunkService, + service: Service, interval: string ): Promise { await updateConfigurationFile( @@ -185,12 +175,12 @@ async function updateEventIngestionCronJobInterval( } async function updateSavedSearchQuery( - service: SplunkService, + service: Service, savedSearchName: string, query: string ): Promise { const savedSearches = await promisify(service.savedSearches().fetch)(); - const savedSearch = savedSearches.item(savedSearchName); + const savedSearch = savedSearches.item(savedSearchName, APPLICATION_NAMESPACE); if (savedSearch) { await savedSearch.update({ search: query, @@ -198,14 +188,14 @@ async function updateSavedSearchQuery( } } -async function fetchCollectionItems(): Promise { +async function fetchCollectionItems(): Promise { const service = createService(); return promisify(service.get)( `storage/collections/data/event_ingestion_collection/${KV_COLLECTION_NAME}`, {} ) - .then((response: SplunkRequestResponse) => { - const items: SplunkCollectionItem[] = []; + .then((response: HTTPResponse) => { + const items: KVCollectionItem[] = []; if (response.data) { response.data.forEach((element) => { items.push({ @@ -229,7 +219,7 @@ async function fetchPassword(passwordKey: string): Promise { for (const password of storagePasswords.list()) { if (password.name === passwordId) { - return password._properties.clear_password; + return password.properties().clear_password; } } return undefined; @@ -284,7 +274,7 @@ async function createFlareIndex(): Promise { } } -async function saveIndexForIngestion(service: SplunkService, indexName: string): Promise { +async function saveIndexForIngestion(service: Service, indexName: string): Promise { await updateConfigurationFile( service, 'inputs',