mirror of
https://github.com/valitydev/wazuh-kibana-app.git
synced 2024-11-06 09:55:18 +00:00
New cron jobs handler (#2412)
* Added logic to run the tests * Added a new class for making API requests The old monitoring task has a specific logic to obtain data from the agents of the API. * Created the structure files of the SchedulerHandler * Remove a console.log and fixed the return type * Fixed typo errors in the tests titles * Finished the first iteration The logic execute all jobs when the Kibana up. * Inserted the timestamp field * Fixed the tests of schedulerJob class * Refactor getApiObject function * Adds a setting to change the prefix of the job indexes * Some request has parameters in the path To solved it, we modified the code to get the list of the parameters from another request or an array of strings * Refactor code * Create the index pattern if not exists * Add cron.prefix configuration example to wazuh.yml * Changes to adapt to Kibana 7.6 * Revert changes in predefined-jobs.js * Added new log line * Add logic to add the creation interval subfix * Change the jobs array to an object * Refactor code * Add the Mapping Object to save the data in the document * Refactor * Added new error handler * Added logic to change settings * Create statistic index-pattern in tenants * Adapt to api 4.0 * Repaired the broken ApiRequest class tests * Repaired the broken SchedulerJob class tests * Repaired the broken SchedulerJob class tests * Reverse the plugin-helpers version
This commit is contained in:
parent
6654524eec
commit
62940c80fd
4
init.js
4
init.js
@ -19,13 +19,14 @@ import { WazuhHostsRoutes } from './server/routes/wazuh-hosts';
|
||||
import { WazuhReportingRoutes } from './server/routes/wazuh-reporting';
|
||||
import { WazuhUtilsRoutes } from './server/routes/wazuh-utils';
|
||||
import { IndexPatternCronJob } from './server/index-pattern-cron-job';
|
||||
import { SchedulerHandler } from './server/lib/cron-scheduler'
|
||||
import { log } from './server/logger';
|
||||
import { Queue } from './server/jobs/queue';
|
||||
|
||||
export function initApp(server) {
|
||||
const monitoringInstance = new Monitoring(server);
|
||||
const indexPatternCronJobInstance = new IndexPatternCronJob(server);
|
||||
|
||||
const schedulerHandler = new SchedulerHandler(server);
|
||||
log('init:initApp', `Waiting for Kibana migration jobs`, 'debug');
|
||||
server.kibanaMigrator
|
||||
.runMigrations()
|
||||
@ -39,6 +40,7 @@ export function initApp(server) {
|
||||
WazuhElasticRouter(server);
|
||||
monitoringInstance.run();
|
||||
indexPatternCronJobInstance.run();
|
||||
schedulerHandler.run();
|
||||
Queue.launchCronJob();
|
||||
WazuhApiRoutes(server);
|
||||
WazuhHostsRoutes(server);
|
||||
|
@ -37,7 +37,8 @@
|
||||
"test": "_mocha test/**/*",
|
||||
"test:ui:runner": "node ../../scripts/functional_test_runner.js",
|
||||
"test:server": "plugin-helpers test:server",
|
||||
"test:browser": "plugin-helpers test:browser"
|
||||
"test:browser": "plugin-helpers test:browser",
|
||||
"test:jest": "node scripts/jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"angular-animate": "1.7.8",
|
||||
@ -64,6 +65,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elastic/plugin-helpers": "^7.1.8",
|
||||
"@types/node-cron": "^2.0.3",
|
||||
"babel-eslint": "^8.2.6",
|
||||
"chai": "^4.1.2",
|
||||
"eslint": "^5.10.0",
|
||||
|
2
public/react-services/saved-objects.js
vendored
2
public/react-services/saved-objects.js
vendored
@ -78,7 +78,7 @@ export class SavedObject {
|
||||
}
|
||||
}
|
||||
|
||||
static async existsMonitoringIndexPattern(patternID) {
|
||||
static async existsOrCreateIndexPattern(patternID) {
|
||||
try {
|
||||
await GenericRequest.request(
|
||||
'GET',
|
||||
|
@ -43,6 +43,7 @@ export async function getWzConfig($q, genericReq, wazuhConfig) {
|
||||
'wazuh.monitoring.replicas': 0,
|
||||
'wazuh.monitoring.creation': 'd',
|
||||
'wazuh.monitoring.pattern': 'wazuh-monitoring-3.x-*',
|
||||
'cron.prefix': 'wazuh',
|
||||
admin: true,
|
||||
hideManagerAlerts: false,
|
||||
'logs.level': 'info'
|
||||
|
@ -52,7 +52,9 @@ export const configEquivalences = {
|
||||
hideManagerAlerts:
|
||||
'Hide the alerts of the manager in all dashboards and discover',
|
||||
'logs.level':
|
||||
'Set the app logging level, allowed values are info and debug. Default is info.'
|
||||
'Set the app logging level, allowed values are info and debug. Default is info.',
|
||||
'cron.prefix':
|
||||
'Define the index prefix of predefined jobs'
|
||||
};
|
||||
|
||||
export const nameEquivalence = {
|
||||
|
20
scripts/jest.js
Normal file
20
scripts/jest.js
Normal file
@ -0,0 +1,20 @@
|
||||
// # Run Jest tests
|
||||
//
|
||||
// All args will be forwarded directly to Jest, e.g. to watch tests run:
|
||||
//
|
||||
// node scripts/jest --watch
|
||||
//
|
||||
// or to build code coverage:
|
||||
//
|
||||
// node scripts/jest --coverage
|
||||
//
|
||||
// See all cli options in https://facebook.github.io/jest/docs/cli.html
|
||||
|
||||
|
||||
const path = require('path');
|
||||
process.argv.push('--config', path.resolve(__dirname, '../test/jest/config.js'));
|
||||
|
||||
require('../../../src/setup_node_env');
|
||||
const jest = require('../../../node_modules/jest');
|
||||
|
||||
jest.run(process.argv.slice(2));
|
115
server/lib/cron-scheduler/apiRequest.test.ts
Normal file
115
server/lib/cron-scheduler/apiRequest.test.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import axios from 'axios';
|
||||
import { ApiRequest, IApi } from './index'
|
||||
jest.mock('axios');
|
||||
|
||||
describe('ApiRequest', () => {
|
||||
const apiExample1: IApi = {
|
||||
id: 'default',
|
||||
user: 'foo',
|
||||
password: 'bar',
|
||||
url: 'http://localhost',
|
||||
port: 55000,
|
||||
cluster_info: {
|
||||
manager: 'master',
|
||||
cluster: 'Disabled',
|
||||
status: 'disabled',
|
||||
},
|
||||
}
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should return the object with the data of the request ', async () => {
|
||||
const mockResponse = {
|
||||
data: { "enabled": "yes", "running": "yes" },
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {},
|
||||
config: {},
|
||||
}
|
||||
axios.mockResolvedValue(mockResponse);
|
||||
|
||||
const apiRequest = new ApiRequest('/cluster/status', apiExample1);
|
||||
const response = await apiRequest.getData();
|
||||
|
||||
expect(response).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
test('should return the object with the error when the path is invalid', async () => {
|
||||
const mockResponse = {
|
||||
response: {
|
||||
data: {
|
||||
"type": "about:blank",
|
||||
"title": "Not Found",
|
||||
"detail": "Nothing matches the given URI",
|
||||
"status": 404
|
||||
},
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
headers: {},
|
||||
config: {},
|
||||
},
|
||||
status: 404
|
||||
};
|
||||
axios.mockRejectedValue(mockResponse);
|
||||
|
||||
const apiRequest = new ApiRequest('/cluster/statu', apiExample1);
|
||||
try {
|
||||
await apiRequest.getData();
|
||||
} catch (error) {
|
||||
expect(error).toEqual({error: 404, message: "Nothing matches the given URI"});
|
||||
}
|
||||
})
|
||||
|
||||
test('should throw an error when the api user are unauthorized', async () => {
|
||||
const mockResponse = {
|
||||
response: {
|
||||
response: {
|
||||
data: {
|
||||
"type": "about:blank",
|
||||
"title": "Unauthorized",
|
||||
"detail": "The server could not verify that you are authorized to access the URL requested. You either supplied the wrong credentials (e.g. a bad password),or your browser doesn't understand how to supply the credentials required.",
|
||||
"status": 401
|
||||
},
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
headers: {},
|
||||
config: {},
|
||||
},
|
||||
"status": 401
|
||||
}
|
||||
};
|
||||
axios.mockRejectedValue(mockResponse);
|
||||
|
||||
const apiRequest = new ApiRequest('/cluster/status', apiExample1);
|
||||
try {
|
||||
await apiRequest.getData();
|
||||
} catch (error) {
|
||||
expect(error).toEqual({error: 401, message: 'Wrong Wazuh API credentials used'});
|
||||
}
|
||||
})
|
||||
|
||||
test('should throw an error when the port or url api are invalid', async () => {
|
||||
const mockResponse = {response: { data: { detail: 'ECONNREFUSED' }, status: 500} }
|
||||
axios.mockRejectedValue(mockResponse);
|
||||
|
||||
const apiRequest = new ApiRequest('/cluster/status', apiExample1);
|
||||
try {
|
||||
await apiRequest.getData();
|
||||
} catch (error) {
|
||||
expect(error).toStrictEqual({error: 3005, message: 'Wazuh API is not reachable. Please check your url and port.'});
|
||||
}
|
||||
})
|
||||
|
||||
test('should throw an error when the url api are invalid', async () => {
|
||||
const mockResponse = {response: { data: { detail: 'ECONNRESET' }, status: 500} }
|
||||
axios.mockRejectedValue(mockResponse);
|
||||
const apiRequest = new ApiRequest('/cluster/status', apiExample1);
|
||||
try {
|
||||
await apiRequest.getData();
|
||||
} catch (error) {
|
||||
expect(error).toStrictEqual({error: 3005, message: 'Wrong protocol being used to connect to the Wazuh API'});
|
||||
}
|
||||
})
|
||||
|
||||
})
|
63
server/lib/cron-scheduler/apiRequest.ts
Normal file
63
server/lib/cron-scheduler/apiRequest.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { AxiosResponse }from 'axios';
|
||||
import { ApiInterceptor } from '../api-interceptor.js';
|
||||
|
||||
export interface IApi {
|
||||
id: string
|
||||
user: string
|
||||
password: string
|
||||
url: string
|
||||
port: number
|
||||
cluster_info: {
|
||||
manager: string
|
||||
cluster: 'Disabled' | 'Enabled'
|
||||
status: 'disabled' | 'enabled'
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiRequest {
|
||||
private api: IApi;
|
||||
private request: string;
|
||||
private params: {};
|
||||
private apiInterceptor: ApiInterceptor;
|
||||
|
||||
constructor(request:string, api:IApi, params:{}={}, ) {
|
||||
this.request = request;
|
||||
this.api = api;
|
||||
this.params = params;
|
||||
this.apiInterceptor = new ApiInterceptor()
|
||||
}
|
||||
|
||||
private async makeRequest():Promise<AxiosResponse> {
|
||||
const {id, url, port} = this.api;
|
||||
|
||||
const response: AxiosResponse = await this.apiInterceptor.request(
|
||||
'GET',
|
||||
`${url}:${port}/v4/${this.request}`,
|
||||
this.params,
|
||||
{idHost: id }
|
||||
)
|
||||
return response;
|
||||
}
|
||||
|
||||
public async getData():Promise<object> {
|
||||
try {
|
||||
const response = await this.makeRequest();
|
||||
if (response.status !== 200) throw response;
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
throw {error: 404, message: error.data.detail};
|
||||
}
|
||||
if (error.response && error.response.status === 401){
|
||||
throw {error: 401, message: 'Wrong Wazuh API credentials used'};
|
||||
}
|
||||
if (error.data.detail && error.data.detail === 'ECONNRESET') {
|
||||
throw {error: 3005, message: 'Wrong protocol being used to connect to the Wazuh API'};
|
||||
}
|
||||
if (error.data.detail && ['ENOTFOUND','EHOSTUNREACH','EINVAL','EAI_AGAIN','ECONNREFUSED'].includes(error.data.detail)) {
|
||||
throw {error: 3005, message: 'Wazuh API is not reachable. Please check your url and port.'};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
29
server/lib/cron-scheduler/configured-jobs.ts
Normal file
29
server/lib/cron-scheduler/configured-jobs.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { jobs } from './index';
|
||||
import { IApi } from './apiRequest';
|
||||
import { IJob } from './predefined-jobs';
|
||||
|
||||
export const configuredJobs = (params:{jobName?:string, host?: IApi}) => {
|
||||
const { host, jobName } = params;
|
||||
return checkCluster(getJobs({jobName, host}))
|
||||
}
|
||||
|
||||
const getJobs = (params:{jobName?:string, host?: IApi}) => {
|
||||
const { host, jobName } = params;
|
||||
if (!jobName) return {jobObj:jobs, host};
|
||||
return {jobObj:{[jobName]:jobs[jobName]}, host}
|
||||
}
|
||||
|
||||
const checkCluster = (params: {jobObj:{[key:string]: IJob}, host?: IApi}) => {
|
||||
const {host} = params;
|
||||
const newJobObj = JSON.parse(JSON.stringify(params.jobObj));
|
||||
if(host && host.cluster_info.status === 'enabled'){
|
||||
['manager-stats-remoted', 'manager-stats-analysisd'].forEach(item => {
|
||||
newJobObj[item] && (newJobObj[item].status = false);
|
||||
});
|
||||
} else if (host && host.cluster_info.status === 'disabled') {
|
||||
['cluster-stats-remoted', 'cluster-stats-analysisd'].forEach(item => {
|
||||
newJobObj[item] && (newJobObj[item].status = false);
|
||||
})
|
||||
}
|
||||
return newJobObj;
|
||||
}
|
34
server/lib/cron-scheduler/error-handler.ts
Normal file
34
server/lib/cron-scheduler/error-handler.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { log } from '../../logger';
|
||||
import { getConfiguration } from '../../lib/get-configuration';
|
||||
|
||||
|
||||
const DEBUG = 'debug';
|
||||
const INFO = 'info';
|
||||
const ERROR = 'error';
|
||||
const COLOR = '\u001b[34mwazuh\u001b[39m';
|
||||
const ERROR_COLOR = (errorLevel) => [COLOR, 'Cron-scheduler', errorLevel === DEBUG ? INFO : errorLevel]
|
||||
export function ErrorHandler(error, server) {
|
||||
const { ['logs.level']: logLevel } = getConfiguration();
|
||||
const errorLevel = ErrorLevels[error.error] || ERROR;
|
||||
log('Cron-scheduler', error, errorLevel === ERROR ? INFO : errorLevel);
|
||||
try {
|
||||
if (errorLevel === DEBUG && logLevel !== DEBUG) return;
|
||||
server.log(ERROR_COLOR(errorLevel), `${JSON.stringify(error)}`);
|
||||
} catch (error) {
|
||||
server.log(ERROR_COLOR(ERROR), `Message to long to show in console output, check the log file`)
|
||||
}
|
||||
}
|
||||
|
||||
const ErrorLevels = {
|
||||
401: INFO,
|
||||
403: ERROR,
|
||||
409: DEBUG,
|
||||
3005: INFO,
|
||||
3013: DEBUG,
|
||||
10001: INFO,
|
||||
10002: DEBUG,
|
||||
10003: DEBUG,
|
||||
10004: DEBUG,
|
||||
10005: DEBUG,
|
||||
10006: DEBUG,
|
||||
}
|
9
server/lib/cron-scheduler/index.ts
Normal file
9
server/lib/cron-scheduler/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export { SchedulerHandler } from './scheduler-handler';
|
||||
|
||||
export { jobs, IJob, IRequest } from './predefined-jobs';
|
||||
|
||||
export { SchedulerJob } from './scheduler-job';
|
||||
|
||||
export { ApiRequest, IApi } from './apiRequest';
|
||||
|
||||
export { SaveDocument, IIndexConfiguration } from './save-document';
|
86
server/lib/cron-scheduler/predefined-jobs.ts
Normal file
86
server/lib/cron-scheduler/predefined-jobs.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { IIndexConfiguration } from './index';
|
||||
|
||||
export interface IJob {
|
||||
status: boolean
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
request: string | IRequest
|
||||
params: {}
|
||||
interval: string
|
||||
index: IIndexConfiguration
|
||||
apis?: string[]
|
||||
}
|
||||
|
||||
export interface IRequest {
|
||||
request: string
|
||||
params: {
|
||||
[key:string]: {
|
||||
request?: string
|
||||
list?: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const jobs: {[key:string]: IJob} = {
|
||||
'manager-stats-remoted': {
|
||||
status: true,
|
||||
method: "GET",
|
||||
request: '/manager/stats/remoted?pretty',
|
||||
params: {},
|
||||
interval: '*/5 * * * * *',
|
||||
index: {
|
||||
name: 'statistic',
|
||||
creation: 'w',
|
||||
mapping: '{"remoted": ${data}, "apiName": ${apiName}, "cluster": "false"}',
|
||||
}
|
||||
},
|
||||
'manager-stats-analysisd': {
|
||||
status: true,
|
||||
method: "GET",
|
||||
request: '/manager/stats/analysisd?pretty',
|
||||
params: {},
|
||||
interval: '*/5 * * * * *',
|
||||
index: {
|
||||
name: 'statistic',
|
||||
creation: 'w',
|
||||
mapping: '{"analysisd": ${data}, "apiName": ${apiName}, "cluster": "false"}',
|
||||
}
|
||||
},
|
||||
'cluster-stats-remoted': {
|
||||
status: true,
|
||||
method: "GET",
|
||||
request: {
|
||||
request: '/cluster/{nodeName}/stats/remoted?pretty',
|
||||
params: {
|
||||
nodeName: {
|
||||
request: '/cluster/nodes?select=name'
|
||||
}
|
||||
}
|
||||
},
|
||||
params: {},
|
||||
interval: '*/5 * * * * *',
|
||||
index: {
|
||||
name:'statistic',
|
||||
creation: 'w',
|
||||
mapping: '{"remoted": ${data}, "apiName": ${apiName}, "nodeName": ${nodeName}, "cluster": "true"}',
|
||||
}
|
||||
},
|
||||
'cluster-stats-analysisd': {
|
||||
status: true,
|
||||
method: "GET",
|
||||
request: {
|
||||
request: '/cluster/{nodeName}/stats/analysisd?pretty',
|
||||
params: {
|
||||
nodeName: {
|
||||
request: '/cluster/nodes?select=name'
|
||||
}
|
||||
}
|
||||
},
|
||||
params: {},
|
||||
interval: '*/5 * * * * *',
|
||||
index: {
|
||||
name: 'statistic',
|
||||
creation: 'w',
|
||||
mapping: '{"analysisd": ${data}, "apiName": ${apiName}, "nodeName": ${nodeName}, "cluster": "true"}',
|
||||
}
|
||||
},
|
||||
}
|
28
server/lib/cron-scheduler/save-document.test.ts
Normal file
28
server/lib/cron-scheduler/save-document.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { SaveDocument } from './index';
|
||||
import elasticsearch from 'elasticsearch';
|
||||
jest.mock('elasticsearch');
|
||||
|
||||
describe('SaveDocument', () => {
|
||||
const fakeServer = {
|
||||
plugins:{
|
||||
elasticsearch:{
|
||||
getCluster: data => {
|
||||
return {
|
||||
clusterClient:{client: new elasticsearch.Client({})},
|
||||
callWithRequest: Function,
|
||||
callWithInternalUser: Function,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let savedDocument: SaveDocument;
|
||||
beforeEach(() => {
|
||||
savedDocument = new SaveDocument(fakeServer)
|
||||
});
|
||||
|
||||
test('should be create the object SavedDocument', () => {
|
||||
expect(savedDocument).toBeInstanceOf(SaveDocument);
|
||||
});
|
||||
|
||||
});
|
156
server/lib/cron-scheduler/save-document.ts
Normal file
156
server/lib/cron-scheduler/save-document.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { BulkIndexDocumentsParams } from 'elasticsearch';
|
||||
import { getConfiguration } from '../get-configuration';
|
||||
import { log } from '../../logger.js';
|
||||
import { indexDate } from '../index-date.js';
|
||||
|
||||
export interface IIndexConfiguration {
|
||||
name: string
|
||||
creation: 'h' | 'd' | 'w' | 'm'
|
||||
mapping?: string
|
||||
}
|
||||
|
||||
export class SaveDocument {
|
||||
server: object;
|
||||
callWithRequest: Function
|
||||
callWithInternalUser: Function
|
||||
logPath = 'cron-scheduler|SaveDocument';
|
||||
|
||||
constructor(server) {
|
||||
this.server = server;
|
||||
this.callWithRequest = server.plugins.elasticsearch.getCluster('data').callWithRequest;
|
||||
this.callWithInternalUser = server.plugins.elasticsearch.getCluster('data').callWithInternalUser;
|
||||
}
|
||||
|
||||
async save(doc:object[], indexConfig:IIndexConfiguration) {
|
||||
const { name, creation, mapping } = indexConfig;
|
||||
const index = this.addIndexPrefix(name);
|
||||
const indexCreation = `${index}-${indexDate(creation)}`;
|
||||
try {
|
||||
await this.checkIndexAndCreateIfNotExists(indexCreation);
|
||||
const createDocumentObject = this.createDocument(doc, indexCreation, mapping);
|
||||
const response = await this.callWithInternalUser('bulk', createDocumentObject);
|
||||
log(this.logPath, `Response of create new document ${JSON.stringify(response)}`, 'debug');
|
||||
await this.checkIndexPatternAndCreateIfNotExists(index);
|
||||
} catch(error) {
|
||||
if (error.status === 403)
|
||||
throw {error: 403, message: `Authorization Exception in the index "${index}"`}
|
||||
if (error.status === 409)
|
||||
throw {error: 409, message: `Duplicate index-pattern: ${index}`}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async checkIndexAndCreateIfNotExists(index) {
|
||||
const exists = await this.callWithInternalUser('indices.exists',{index});
|
||||
log(this.logPath, `Index '${index}' exists? ${exists}`, 'debug');
|
||||
if(!exists) {
|
||||
const response = await this.callWithInternalUser('indices.create',
|
||||
{
|
||||
index,
|
||||
body: {
|
||||
settings: {
|
||||
index: {
|
||||
number_of_shards: 2,
|
||||
number_of_replicas: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
log(this.logPath, `Status of create a new index: ${JSON.stringify(response)}`, 'debug');
|
||||
}
|
||||
}
|
||||
|
||||
private async checkIndexPatternAndCreateIfNotExists(index) {
|
||||
const KIBANA_INDEX = this.getKibanaIndex();
|
||||
log(this.logPath, `Internal index of kibana: ${KIBANA_INDEX}`, 'debug');
|
||||
const result = await this.callWithInternalUser('search', {
|
||||
index: KIBANA_INDEX,
|
||||
type: '_doc',
|
||||
body: {
|
||||
query: {
|
||||
match: {
|
||||
_id: `index-pattern:${index}-*`
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (result.hits.total.value === 0) {
|
||||
await this.createIndexPattern(KIBANA_INDEX, index);
|
||||
}
|
||||
}
|
||||
|
||||
private async createIndexPattern(KIBANA_INDEX: any, index: any) {
|
||||
const response = await this.callWithInternalUser('create', {
|
||||
index: KIBANA_INDEX,
|
||||
type: '_doc',
|
||||
'id': `index-pattern:${index}-*`,
|
||||
body: {
|
||||
type: 'index-pattern',
|
||||
'index-pattern': {
|
||||
title: `${index}-*`,
|
||||
timeFieldName: 'timestamp',
|
||||
}
|
||||
}
|
||||
});
|
||||
log(
|
||||
this.logPath,
|
||||
`The indexPattern no exist, response of createIndexPattern: ${JSON.stringify(response)}`,
|
||||
'debug'
|
||||
);
|
||||
}
|
||||
|
||||
private getKibanaIndex() {
|
||||
return ((((this.server || {})
|
||||
// @ts-ignore
|
||||
.registrations || {})
|
||||
.kibana || {})
|
||||
.options || {})
|
||||
.index || '.kibana';
|
||||
}
|
||||
|
||||
private createDocument (doc, index, mapping:string): BulkIndexDocumentsParams {
|
||||
const createDocumentObject: BulkIndexDocumentsParams = {
|
||||
index,
|
||||
type: '_doc',
|
||||
body: doc.flatMap(item => [{
|
||||
index: { _index: index } },
|
||||
{
|
||||
...this.buildData(item, mapping),
|
||||
timestamp: new Date(Date.now()).toISOString()
|
||||
}
|
||||
])
|
||||
};
|
||||
log(this.logPath, `Document object: ${JSON.stringify(createDocumentObject)}`, 'debug');
|
||||
return createDocumentObject;
|
||||
}
|
||||
|
||||
buildData(item, mapping)
|
||||
{
|
||||
const getValue = (key: string, item) => {
|
||||
const keys = key.split('.');
|
||||
if (keys.length === 1) {
|
||||
return JSON.stringify(item[key]);
|
||||
}
|
||||
return getValue(keys.slice(1).join('.'), item[keys[0]])
|
||||
}
|
||||
if (mapping) {
|
||||
const data = mapping.replace(
|
||||
/\${([a-z|A-Z|0-9|\.\-\_]+)}/gi,
|
||||
(...key) => getValue(key[1], item)
|
||||
)
|
||||
return JSON.parse(data);
|
||||
}
|
||||
if (typeof item.data === 'object'){
|
||||
return item.data;
|
||||
}
|
||||
return {data: item.data};
|
||||
}
|
||||
|
||||
private addIndexPrefix(index): string {
|
||||
const configFile = getConfiguration();
|
||||
const prefix = configFile['cron.prefix'] || 'wazuh';
|
||||
return `${prefix}-${index}`;
|
||||
}
|
||||
|
||||
}
|
23
server/lib/cron-scheduler/scheduler-handler.ts
Normal file
23
server/lib/cron-scheduler/scheduler-handler.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { jobs, SchedulerJob } from './index';
|
||||
import { configuredJobs } from './configured-jobs';
|
||||
import { schedule } from 'node-cron';
|
||||
|
||||
export class SchedulerHandler {
|
||||
server: object;
|
||||
schedulerJobs: SchedulerJob[];
|
||||
constructor(server) {
|
||||
this.server = server;
|
||||
this.schedulerJobs = [];
|
||||
}
|
||||
|
||||
run() {
|
||||
for (const job in configuredJobs({})) {
|
||||
const schedulerJob:SchedulerJob = new SchedulerJob(job, this.server);
|
||||
this.schedulerJobs.push(schedulerJob);
|
||||
const task = schedule(
|
||||
jobs[job].interval,
|
||||
() => schedulerJob.run(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
187
server/lib/cron-scheduler/scheduler-job.test.ts
Normal file
187
server/lib/cron-scheduler/scheduler-job.test.ts
Normal file
@ -0,0 +1,187 @@
|
||||
//@ts-nocheck
|
||||
import {
|
||||
SchedulerJob,
|
||||
IApi,
|
||||
jobs
|
||||
} from './index';
|
||||
import { WazuhHostsCtrl } from '../../controllers/wazuh-hosts';
|
||||
jest.mock('../../controllers/wazuh-hosts');
|
||||
jest.mock('./save-document');
|
||||
jest.mock('./predefined-jobs', () => ({
|
||||
jobs: {
|
||||
testJob1: {
|
||||
status: true,
|
||||
method: 'GET',
|
||||
request: '/manager/status',
|
||||
params: {},
|
||||
interval: '* */2 * * *',
|
||||
index: 'manager-status',
|
||||
},
|
||||
testJob2: {
|
||||
status: true,
|
||||
method: 'GET',
|
||||
request: '/manager/status',
|
||||
params: {},
|
||||
interval: '* */2 * * *',
|
||||
index: 'manager-status',
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
describe('SchedulerJob', () => {
|
||||
const oneApi = [{
|
||||
url: 'https://localhost',
|
||||
port: 55000,
|
||||
username: 'foo',
|
||||
password: 'bar',
|
||||
id: 'default',
|
||||
cluster_info: {
|
||||
status: 'disabled',
|
||||
manager: 'master',
|
||||
node: 'node01',
|
||||
cluster: 'Disabled'
|
||||
}
|
||||
}];
|
||||
const twoApi = [
|
||||
{
|
||||
url: 'https://localhost',
|
||||
port: 55000,
|
||||
username: 'foo',
|
||||
password: 'bar',
|
||||
id: 'internal',
|
||||
cluster_info: {
|
||||
status: 'disabled',
|
||||
manager: 'master',
|
||||
node: 'node01',
|
||||
cluster: 'Disabled'
|
||||
}
|
||||
},
|
||||
{
|
||||
url: 'https://externalhost',
|
||||
port: 55000,
|
||||
username: 'foo',
|
||||
password: 'bar',
|
||||
id: 'external',
|
||||
cluster_info: {
|
||||
status: 'disabled',
|
||||
manager: 'master',
|
||||
node: 'node01',
|
||||
cluster: 'Disabled'
|
||||
}
|
||||
},
|
||||
];
|
||||
const threeApi = [
|
||||
{
|
||||
url: 'https://localhost',
|
||||
port: 55000,
|
||||
username: 'foo',
|
||||
password: 'bar',
|
||||
id: 'internal',
|
||||
cluster_info: {
|
||||
status: 'disabled',
|
||||
manager: 'master',
|
||||
node: 'node01',
|
||||
cluster: 'Disabled'
|
||||
}
|
||||
},
|
||||
{
|
||||
url: 'https://externalhost',
|
||||
port: 55000,
|
||||
username: 'foo',
|
||||
password: 'bar',
|
||||
id: 'external',
|
||||
cluster_info: {
|
||||
status: 'disabled',
|
||||
manager: 'master',
|
||||
node: 'node01',
|
||||
cluster: 'Disabled'
|
||||
}
|
||||
},
|
||||
{
|
||||
url: 'https://externalhost',
|
||||
port: 55000,
|
||||
username: 'foo',
|
||||
password: 'bar',
|
||||
id: 'experimental',
|
||||
cluster_info: {
|
||||
status: 'disabled',
|
||||
manager: 'master',
|
||||
node: 'node01',
|
||||
cluster: 'Disabled'
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
let schedulerJob: SchedulerJob;
|
||||
|
||||
beforeEach(() => {
|
||||
schedulerJob = new SchedulerJob('testJob1', {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
})
|
||||
|
||||
it('should job is assigned ', () => {
|
||||
expect(schedulerJob).toBeInstanceOf(SchedulerJob);
|
||||
expect(schedulerJob.jobName).toEqual('testJob1');
|
||||
});
|
||||
|
||||
it('should get API object when no specified the `apis` parameter on the job object', async () => {
|
||||
WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(oneApi);
|
||||
|
||||
|
||||
const apis: IApi[] = await schedulerJob.getApiObjects();
|
||||
expect(apis).not.toBeUndefined();
|
||||
expect(apis).not.toBeFalsy();
|
||||
expect(apis).toEqual(oneApi);
|
||||
});
|
||||
|
||||
it('should get all API objects when no specified the `apis` parameter on the job object', async () => {
|
||||
WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(twoApi)
|
||||
const apis: IApi[] = await schedulerJob.getApiObjects();
|
||||
|
||||
expect(apis).not.toBeUndefined();
|
||||
expect(apis).not.toBeFalsy();
|
||||
expect(apis).toEqual(twoApi);
|
||||
});
|
||||
|
||||
it('should get one of two API object when specified the id in `apis` parameter on the job object', async () => {
|
||||
WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(twoApi)
|
||||
jobs[schedulerJob.jobName] = { ...jobs[schedulerJob.jobName], apis: ['internal'] };
|
||||
const apis: IApi[] = await schedulerJob.getApiObjects();
|
||||
const filteredTwoApi = twoApi.filter(item => item.id === 'internal')
|
||||
|
||||
expect(apis).not.toBeUndefined();
|
||||
expect(apis).not.toBeFalsy();
|
||||
expect(apis).toEqual(filteredTwoApi);
|
||||
});
|
||||
|
||||
it('should get two of three API object when specified the id in `apis` parameter on the job object', async () => {
|
||||
WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(threeApi)
|
||||
const selectedApis = ['internal', 'external'];
|
||||
jobs[schedulerJob.jobName] = { ...jobs[schedulerJob.jobName], apis: selectedApis };
|
||||
const apis: IApi[] = await schedulerJob.getApiObjects();
|
||||
const filteredThreeApi = threeApi.filter(item => selectedApis.includes(item.id))
|
||||
|
||||
expect(apis).not.toBeUndefined();
|
||||
expect(apis).not.toBeFalsy();
|
||||
expect(apis).toEqual(filteredThreeApi);
|
||||
});
|
||||
|
||||
it('should throw an exception when no get APIs', async () => {
|
||||
WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue([])
|
||||
await expect(schedulerJob.getApiObjects()).rejects.toEqual(
|
||||
{ error: 10001, message: 'No Wazuh host configured in wazuh.yml' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an exception when no match API', async () => {
|
||||
WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(threeApi)
|
||||
jobs[schedulerJob.jobName] = { ...jobs[schedulerJob.jobName], apis: ['unkown'] };
|
||||
await expect(schedulerJob.getApiObjects()).rejects.toEqual(
|
||||
{ error: 10002, message: 'No host was found with the indicated ID' }
|
||||
);
|
||||
});
|
||||
|
||||
})
|
118
server/lib/cron-scheduler/scheduler-job.ts
Normal file
118
server/lib/cron-scheduler/scheduler-job.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { jobs } from './predefined-jobs';
|
||||
import { WazuhHostsCtrl } from '../../controllers/wazuh-hosts';
|
||||
import { IApi, ApiRequest, SaveDocument } from './index';
|
||||
import { ErrorHandler } from './error-handler';
|
||||
import { configuredJobs } from './configured-jobs';
|
||||
|
||||
export class SchedulerJob {
|
||||
jobName: string;
|
||||
wazuhHosts: WazuhHostsCtrl;
|
||||
saveDocument: SaveDocument;
|
||||
server: any;
|
||||
|
||||
constructor(jobName: string, server) {
|
||||
this.jobName = jobName;
|
||||
this.server = server;
|
||||
this.wazuhHosts = new WazuhHostsCtrl();
|
||||
this.saveDocument = new SaveDocument(server);
|
||||
}
|
||||
|
||||
public async run() {
|
||||
const { index, status } = configuredJobs({})[this.jobName];
|
||||
if ( !status ) { return; }
|
||||
try {
|
||||
const hosts = await this.getApiObjects();
|
||||
const data = await hosts.reduce(async (acc:Promise<object[]>, host) => {
|
||||
const {status} = configuredJobs({host, jobName: this.jobName})[this.jobName];
|
||||
if (!status) return acc;
|
||||
const response = await this.getResponses(host);
|
||||
const accResolve = await Promise.resolve(acc)
|
||||
return [
|
||||
...accResolve,
|
||||
...response,
|
||||
];
|
||||
}, Promise.resolve([]));
|
||||
!!data.length && await this.saveDocument.save(data, index);
|
||||
} catch (error) {
|
||||
ErrorHandler(error, this.server);
|
||||
}
|
||||
}
|
||||
|
||||
private async getApiObjects() {
|
||||
const { apis } = jobs[this.jobName];
|
||||
const hosts:IApi[] = await this.wazuhHosts.getHostsEntries(false, false, false);
|
||||
if (!hosts.length) throw {error: 10001, message: 'No Wazuh host configured in wazuh.yml' }
|
||||
if(apis){
|
||||
return this.filterHosts(hosts, apis);
|
||||
}
|
||||
return hosts;
|
||||
}
|
||||
|
||||
private filterHosts(hosts: IApi[], apis: string[]) {
|
||||
const filteredHosts = hosts.filter(host => apis.includes(host.id));
|
||||
if (filteredHosts.length <= 0) {
|
||||
throw {error: 10002, message: 'No host was found with the indicated ID'};
|
||||
}
|
||||
return filteredHosts;
|
||||
}
|
||||
|
||||
private async getResponses(host): Promise<object[]> {
|
||||
const { request, params } = jobs[this.jobName];
|
||||
const data:object[] = [];
|
||||
|
||||
if (typeof request === 'string') {
|
||||
const apiRequest = new ApiRequest(request, host, params);
|
||||
const response = await apiRequest.getData()
|
||||
data.push({...response, apiName:host.id});
|
||||
}else {
|
||||
await this.getResponsesForIRequest(host, data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private async getResponsesForIRequest(host: any, data: object[]) {
|
||||
const { request, params } = jobs[this.jobName];
|
||||
const fieldName = this.getParamName(typeof request !== 'string' && request.request);
|
||||
const paramList = await this.getParamList(fieldName, host);
|
||||
for (const param of paramList) {
|
||||
const paramRequest = typeof request !== 'string' && request.request.replace(/\{.+\}/, param);
|
||||
const apiRequest = !!paramRequest && new ApiRequest(paramRequest, host, params);
|
||||
const response = apiRequest && await apiRequest.getData() || {};
|
||||
data.push({
|
||||
...response,
|
||||
apiName: host.id,
|
||||
[fieldName]: param,
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private getParamName(request): string {
|
||||
const regexResult = /\{(?<fieldName>.+)\}/.exec(request);
|
||||
if (regexResult === null) throw {error: 10003, message: `The parameter is not found in the Request: ${request}`};
|
||||
// @ts-ignore
|
||||
const { fieldName } = regexResult.groups;
|
||||
if (fieldName === undefined || fieldName === '') throw {error: 10004, message: `Invalid field in the request: {request: ${request}, field: ${fieldName}}`}
|
||||
return fieldName
|
||||
}
|
||||
|
||||
private async getParamList(fieldName, host) {
|
||||
const { request } = jobs[this.jobName];
|
||||
// @ts-ignore
|
||||
const apiRequest = new ApiRequest(request.params[fieldName].request, host)
|
||||
const response = await apiRequest.getData();
|
||||
const { affected_items } = response['data'];
|
||||
if (affected_items === undefined || affected_items.lenght === 0 ) throw {error: 10005, message: `Empty response when tried to get the parameters list: ${JSON.stringify(response)}`}
|
||||
const values = affected_items.map(this.mapParamList)
|
||||
return values
|
||||
}
|
||||
|
||||
private mapParamList(item) {
|
||||
if (typeof item !== 'object') {
|
||||
return item
|
||||
}
|
||||
const keys = Object.keys(item)
|
||||
if(keys.length > 1 || keys.length < 0) throw { error: 10006, message: `More than one key or none were obtained: ${keys}`}
|
||||
return item[keys[0]];
|
||||
}
|
||||
}
|
@ -119,6 +119,11 @@ export const initialWazuhConfig = `---
|
||||
# Default index pattern to use for Wazuh monitoring
|
||||
#wazuh.monitoring.pattern: wazuh-monitoring-3.x-*
|
||||
#
|
||||
# --------------------------------- wazuh-cron ----------------------------------
|
||||
#
|
||||
# Customize the index prefix of predefined jobs
|
||||
# This change is not retroactive, if you change it new indexes will be created
|
||||
# cron.prefix: test
|
||||
#
|
||||
# ------------------------------- App privileges --------------------------------
|
||||
#admin: true
|
||||
|
67
test/jest/config.js
Normal file
67
test/jest/config.js
Normal file
@ -0,0 +1,67 @@
|
||||
import path from 'path';
|
||||
|
||||
const kbnDir = path.resolve(__dirname, '../../../../');
|
||||
|
||||
export default {
|
||||
rootDir: path.resolve(__dirname, '../..'),
|
||||
roots: [
|
||||
'<rootDir>/public',
|
||||
'<rootDir>/server'
|
||||
],
|
||||
modulePaths: [
|
||||
`${kbnDir}/node_modules`
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
`${kbnDir}/packages/kbn-ui-framework/src/components/**/*.js`,
|
||||
`${kbnDir}/!packages/kbn-ui-framework/src/components/index.js`,
|
||||
`${kbnDir}/!packages/kbn-ui-framework/src/components/**/*/index.js`,
|
||||
`${kbnDir}/packages/kbn-ui-framework/src/services/**/*.js`,
|
||||
`${kbnDir}/!packages/kbn-ui-framework/src/services/index.js`,
|
||||
`${kbnDir}/!packages/kbn-ui-framework/src/services/**/*/index.js`,
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^ui/(.*)': `${kbnDir}/src/ui/public/$1`,
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `${kbnDir}/src/dev/jest/mocks/file_mock.js`,
|
||||
'\\.(css|less|scss)$': `${kbnDir}/src/dev/jest/mocks/style_mock.js`,
|
||||
},
|
||||
setupFiles: [
|
||||
`${kbnDir}/src/dev/jest/setup/babel_polyfill.js`,
|
||||
`${kbnDir}/src/dev/jest/setup/enzyme.js`,
|
||||
],
|
||||
coverageDirectory: `${kbnDir}/target/jest-coverage`,
|
||||
coverageReporters: [
|
||||
'html',
|
||||
],
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
skipBabel: true,
|
||||
},
|
||||
},
|
||||
moduleFileExtensions: [
|
||||
'js',
|
||||
'json',
|
||||
'ts',
|
||||
'tsx',
|
||||
],
|
||||
modulePathIgnorePatterns: [
|
||||
'__fixtures__/',
|
||||
'target/',
|
||||
],
|
||||
testMatch: [
|
||||
'**/*.test.{js,ts,tsx}'
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.js$': `${kbnDir}/src/dev/jest/babel_transform.js`,
|
||||
'^.+\\.tsx?$': `${kbnDir}/src/dev/jest/babel_transform.js`,
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'[/\\\\]node_modules[/\\\\].+\\.js$',
|
||||
],
|
||||
snapshotSerializers: [
|
||||
`${kbnDir}/node_modules/enzyme-to-json/serializer`,
|
||||
],
|
||||
reporters: [
|
||||
'default',
|
||||
`${kbnDir}/src/dev/jest/junit_reporter.js`,
|
||||
],
|
||||
};
|
Loading…
Reference in New Issue
Block a user