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:
Jose Sanchez Robles 2020-08-04 09:40:10 +02:00 committed by GitHub
parent 6654524eec
commit 62940c80fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 951 additions and 4 deletions

View File

@ -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);

View File

@ -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",

View File

@ -78,7 +78,7 @@ export class SavedObject {
}
}
static async existsMonitoringIndexPattern(patternID) {
static async existsOrCreateIndexPattern(patternID) {
try {
await GenericRequest.request(
'GET',

View File

@ -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'

View File

@ -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
View 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));

View 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'});
}
})
})

View 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;
}
}
}

View 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;
}

View 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,
}

View 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';

View 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"}',
}
},
}

View 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);
});
});

View 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}`;
}
}

View 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(),
);
}
}
}

View 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' }
);
});
})

View 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]];
}
}

View File

@ -119,7 +119,12 @@ 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
View 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`,
],
};