mirror of https://github.com/boxyhq/jackson.git
added package configs and sinks
This commit is contained in:
parent
b67f9f3dbc
commit
a05f9b18e7
|
@ -7,6 +7,7 @@ import requestIp from 'request-ip';
|
|||
import jackson from '@lib/jackson';
|
||||
import { auditLogEnabledGroup, retracedOptions } from '@lib/env';
|
||||
import { sessionName } from '@lib/constants';
|
||||
import { sendSecurityLogs } from '@ee/security-logs-config';
|
||||
|
||||
type AuditEventType =
|
||||
| 'sso.user.login'
|
||||
|
@ -45,14 +46,35 @@ type AuditEventType =
|
|||
// Security Logs Config
|
||||
| 'security.logs.config.create'
|
||||
| 'security.logs.config.update'
|
||||
| 'security.logs.config.delete';
|
||||
| 'security.logs.config.delete'
|
||||
|
||||
// SaaS app
|
||||
| 'member.invitation.create'
|
||||
| 'member.invitation.delete'
|
||||
| 'member.remove'
|
||||
| 'member.update'
|
||||
| 'sso.connection.create'
|
||||
| 'sso.connection.patch'
|
||||
| 'sso.connection.delete'
|
||||
| 'dsync.connection.create'
|
||||
| 'dsync.connection.delete'
|
||||
| 'webhook.create'
|
||||
| 'webhook.delete'
|
||||
| 'webhook.update'
|
||||
| 'team.create'
|
||||
| 'team.update'
|
||||
| 'team.delete'
|
||||
| 'audit_log.splunk_connection.create'
|
||||
| 'audit_log.splunk_connection.delete'
|
||||
| 'audit_log.splunk_connection.update';
|
||||
|
||||
interface ReportAdminEventParams {
|
||||
action: AuditEventType;
|
||||
crud: Retraced.CRUD;
|
||||
target?: Retraced.Target;
|
||||
req?: NextApiRequest;
|
||||
group?: Retraced.Group;
|
||||
actor?: Retraced.Actor;
|
||||
req?: NextApiRequest;
|
||||
}
|
||||
|
||||
interface ReportEventParams {
|
||||
|
@ -62,6 +84,7 @@ interface ReportEventParams {
|
|||
req: NextApiRequest;
|
||||
group?: Retraced.Group;
|
||||
target?: Retraced.Target;
|
||||
sourceIp?: string;
|
||||
productId?: string;
|
||||
}
|
||||
|
||||
|
@ -104,21 +127,16 @@ const getClient = async () => {
|
|||
|
||||
// Report events to Retraced
|
||||
const reportEvent = async (params: ReportEventParams) => {
|
||||
const { action, crud, actor, req } = params;
|
||||
|
||||
const { action, crud, actor, sourceIp, req } = params;
|
||||
try {
|
||||
const retracedClient = await getClient();
|
||||
|
||||
if (!retracedClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const retracedEvent: Event = {
|
||||
action,
|
||||
crud,
|
||||
actor,
|
||||
created: new Date(),
|
||||
source_ip: getClientIp(req),
|
||||
source_ip: sourceIp || getClientIp(req),
|
||||
};
|
||||
|
||||
if ('group' in params && params.group) {
|
||||
|
@ -163,7 +181,11 @@ const reportEvent = async (params: ReportEventParams) => {
|
|||
return;
|
||||
}
|
||||
|
||||
await retracedClient.reportEvent(retracedEvent);
|
||||
if (retracedClient) {
|
||||
await retracedClient.reportEvent(retracedEvent);
|
||||
}
|
||||
|
||||
await sendSecurityLogs(retracedEvent, retracedEvent.group?.id);
|
||||
} catch (error: any) {
|
||||
console.error('Error reporting event to Retraced', error);
|
||||
}
|
||||
|
@ -171,31 +193,28 @@ const reportEvent = async (params: ReportEventParams) => {
|
|||
|
||||
// Report Admin portal events to Retraced
|
||||
export const reportAdminPortalEvent = async (params: ReportAdminEventParams) => {
|
||||
const { action, crud, target, actor, req } = params;
|
||||
const { action, crud, target, actor, group, req } = params;
|
||||
|
||||
try {
|
||||
const retracedClient = await getClient();
|
||||
|
||||
if (!retracedClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const retracedEvent: Event = {
|
||||
action,
|
||||
crud,
|
||||
target,
|
||||
actor: actor ?? (await getAdminUser(req)),
|
||||
group: adminPortalGroup,
|
||||
group: group || adminPortalGroup,
|
||||
created: new Date(),
|
||||
};
|
||||
|
||||
const ip = getClientIp(req);
|
||||
|
||||
if (ip) {
|
||||
retracedEvent['source_ip'] = ip;
|
||||
}
|
||||
|
||||
await retracedClient.reportEvent(retracedEvent);
|
||||
if (retracedClient) {
|
||||
await retracedClient.reportEvent(retracedEvent);
|
||||
}
|
||||
await sendSecurityLogs(retracedEvent);
|
||||
} catch (error: any) {
|
||||
console.error('Error reporting event to Retraced', error);
|
||||
}
|
||||
|
|
|
@ -48,9 +48,9 @@ const handlePUT = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
const { id } = req.query as { id: string };
|
||||
|
||||
const { config } = req.body as { config: any };
|
||||
const { config, name } = req.body as { config: any; name?: string };
|
||||
|
||||
const updatedApp = await securityLogsConfigController.update(id, config);
|
||||
const updatedApp = await securityLogsConfigController.update(id, config, name);
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'security.logs.config.update',
|
||||
|
|
|
@ -28,16 +28,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { securityLogsConfigController } = await jackson();
|
||||
|
||||
const { tenant, type, config } = req.body as {
|
||||
const { tenant, type, config, name } = req.body as {
|
||||
tenant: string;
|
||||
type: string;
|
||||
config: any;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
const id = await securityLogsConfigController.createSecurityLogsConfig({
|
||||
tenant: boxyhqHosted ? tenant : adminPortalSSODefaults.tenant,
|
||||
tenant: boxyhqHosted && tenant ? tenant : adminPortalSSODefaults.tenant,
|
||||
type,
|
||||
config,
|
||||
name,
|
||||
});
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { adminPortalSSODefaults, boxyhqHosted } from '@lib/env';
|
||||
import jackson from '@lib/jackson';
|
||||
import getSinkInstance from '@boxyhq/security-logs-sink';
|
||||
|
||||
export const sendSecurityLogs = async (event: any, tenant?: string) => {
|
||||
const { securityLogsConfigController } = await jackson();
|
||||
const tenantToUse = boxyhqHosted && tenant ? tenant : adminPortalSSODefaults.tenant;
|
||||
|
||||
const configs = tenantToUse ? await securityLogsConfigController.getAll(tenantToUse) : { data: [] };
|
||||
for (const config of configs.data) {
|
||||
const sink = getSinkInstance({
|
||||
type: config.type,
|
||||
...config.config,
|
||||
});
|
||||
await sink.sendEvent(event);
|
||||
}
|
||||
};
|
|
@ -76,6 +76,9 @@ const ConfigList = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
|||
<table className='w-full text-left text-sm text-gray-500 dark:text-gray-400'>
|
||||
<thead className='bg-gray-50 text-xs uppercase text-gray-700 dark:bg-gray-700 dark:text-gray-400'>
|
||||
<tr className='hover:bg-gray-50'>
|
||||
<th scope='col' className='px-6 py-3'>
|
||||
{t('name')}
|
||||
</th>
|
||||
<th scope='col' className='px-6 py-3'>
|
||||
{t('type')}
|
||||
</th>
|
||||
|
@ -94,6 +97,7 @@ const ConfigList = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
|||
<tr
|
||||
key={config.id}
|
||||
className='border-b bg-white last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800'>
|
||||
<td className='px-6'>{config.name}</td>
|
||||
<td className='px-6'>{getDisplayTypeFromSinkType(config.type)}</td>
|
||||
<td className='px-6 py-3'>{config.tenant}</td>
|
||||
<td className='px-6'>
|
||||
|
|
|
@ -12,6 +12,7 @@ const NewConfiguration = ({ hasValidLicense }: { hasValidLicense: boolean }) =>
|
|||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [config, setConfig] = useState({});
|
||||
const [name, setName] = useState('');
|
||||
const [type, setType] = useState(t('select_type'));
|
||||
|
||||
if (!hasValidLicense) {
|
||||
|
@ -28,7 +29,7 @@ const NewConfiguration = ({ hasValidLicense }: { hasValidLicense: boolean }) =>
|
|||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ type: configMap[type].type, config }),
|
||||
body: JSON.stringify({ name, type: configMap[type].type, config }),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
@ -83,6 +84,20 @@ const NewConfiguration = ({ hasValidLicense }: { hasValidLicense: boolean }) =>
|
|||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className='form-control w-full md:w-1/2'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('name')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='name'
|
||||
className='input-bordered input'
|
||||
value={name}
|
||||
required={false}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t('name')}
|
||||
/>
|
||||
</div>
|
||||
{type && (
|
||||
<>
|
||||
{configMap[type] &&
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import retraced from '@ee/retraced';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
try {
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
return await handlePOST(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', 'POST');
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
} catch (error: any) {
|
||||
const { message, statusCode = 500 } = error;
|
||||
|
||||
return res.status(statusCode).json({
|
||||
error: { message },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get Security Logs config by id
|
||||
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { action, crud, actor, sourceIp, group, target, productId } = req.body;
|
||||
await retraced.reportEvent({
|
||||
action: action,
|
||||
crud: crud,
|
||||
actor: actor,
|
||||
req,
|
||||
group: group,
|
||||
target: target,
|
||||
sourceIp: sourceIp,
|
||||
productId: productId,
|
||||
});
|
||||
res.json({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -0,0 +1,9 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
.vscode
|
||||
.nyc_output
|
||||
_config
|
||||
dist
|
||||
.DS_Store
|
||||
/node_modules
|
||||
**/node_modules/**
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"name": "sinks",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sinks",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "1.6.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
|
||||
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.4",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "@boxyhq/security-logs-sink",
|
||||
"version": "0.0.0",
|
||||
"description": "Package to deliver security logs to different SIEMs and other destination",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/boxyhq/jackson.git"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"keywords": [
|
||||
"SIEM",
|
||||
"Sinks",
|
||||
"Splunk"
|
||||
],
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "1.6.7"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export class WithExponentialBackoff {
|
||||
/**
|
||||
* Calculates the next timeout value for exponential backoff.
|
||||
* @param waitFor The current timeout value.
|
||||
* @returns The next timeout value.
|
||||
*/
|
||||
public getNextExponentialBackoff(waitFor: number): number {
|
||||
// Double the wait time until it reaches 60 seconds
|
||||
return waitFor * 2 > 60000 ? 60000 : waitFor * 2;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export const sleep = (ms: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { Logger, Sink } from './interfaces';
|
||||
import { SplunkHecLogs } from './splunk_hec_logs';
|
||||
|
||||
const getSinkInstance = (sinkConfig: any, customLogger?: Logger): Sink => {
|
||||
switch (sinkConfig.type) {
|
||||
case 'splunk_hec_logs':
|
||||
return new SplunkHecLogs(
|
||||
{
|
||||
defaultToken: sinkConfig.default_token,
|
||||
endpoint: sinkConfig.endpoint,
|
||||
indexingAckEnabled: sinkConfig?.acknowledgements?.indexer_acknowledgements_enabled,
|
||||
},
|
||||
customLogger
|
||||
);
|
||||
default:
|
||||
throw new Error(`unknown sink type: ${sinkConfig.type}`);
|
||||
}
|
||||
};
|
||||
|
||||
export default getSinkInstance;
|
|
@ -0,0 +1,16 @@
|
|||
// Interface for a Sink
|
||||
export interface Sink {
|
||||
// HealthCheck returns true if the sink is healthy
|
||||
healthCheck(): Promise<boolean>;
|
||||
// TransformEvent transforms an event before sending it to the sink
|
||||
transformEvent(event: any): any;
|
||||
// SendEvent sends an event to the sink
|
||||
sendEvent(event: any): Promise<any>;
|
||||
// SendEvents sends events to the sink
|
||||
sendEvents(events: any[], batchSize: number): Promise<any>;
|
||||
}
|
||||
|
||||
export interface Logger {
|
||||
info: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
import axios from 'axios';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { WithExponentialBackoff } from '../classes';
|
||||
import { Logger, Sink } from '../interfaces';
|
||||
import { sleep } from '../helper';
|
||||
|
||||
export class SplunkHecLogs extends WithExponentialBackoff implements Sink {
|
||||
private endpoint: string;
|
||||
private defaultToken: string;
|
||||
private indexingAckEnabled: boolean;
|
||||
private logger: Logger = console;
|
||||
|
||||
constructor(
|
||||
opts: { defaultToken: string; endpoint: string; indexingAckEnabled?: boolean },
|
||||
customLogger?: Logger
|
||||
) {
|
||||
super();
|
||||
if (customLogger) {
|
||||
this.logger = customLogger;
|
||||
}
|
||||
if (!opts.endpoint) {
|
||||
throw new Error('endpoint is required');
|
||||
}
|
||||
if (!opts.defaultToken) {
|
||||
throw new Error('defaultToken is required');
|
||||
}
|
||||
this.indexingAckEnabled = opts.indexingAckEnabled ? opts.indexingAckEnabled : false;
|
||||
this.endpoint = opts.endpoint;
|
||||
this.defaultToken = opts.defaultToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the health of the Splunk HEC endpoint.
|
||||
* @returns A promise that resolves to true if the health check is successful, or rejects with an error if it fails.
|
||||
*/
|
||||
public async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
const config = {
|
||||
method: 'get',
|
||||
maxBodyLength: Infinity,
|
||||
url: `${this.endpoint}/services/collector/health/1.0`,
|
||||
};
|
||||
const response = await axios.request(config);
|
||||
|
||||
if (response.status === 200) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.info(error ? error.message : 'Something went wrong with the health check');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a single event into the expected format.
|
||||
* @param event The event to transform.
|
||||
* @param batched Indicates whether the event is part of a batch.
|
||||
* @returns The transformed event.
|
||||
*/
|
||||
public transformEvent(event: any, batched = false): any {
|
||||
if (batched) {
|
||||
return event.map((e) => JSON.stringify({ event: e })).join('\n');
|
||||
} else {
|
||||
return {
|
||||
event,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a single event to the Splunk HEC endpoint.
|
||||
* @param event The event to send.
|
||||
* @returns A promise that resolves to true if the event is successfully sent and indexed, or false otherwise.
|
||||
*/
|
||||
public async sendEvent(event: any): Promise<boolean> {
|
||||
if (!event) {
|
||||
throw new Error('event is required');
|
||||
}
|
||||
let backoff = 100;
|
||||
do {
|
||||
try {
|
||||
const transformedEvent = this.transformEvent(event);
|
||||
const channel = this.indexingAckEnabled ? randomUUID() : undefined;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Splunk ${this.defaultToken}`,
|
||||
'X-Splunk-Request-Channel': channel,
|
||||
};
|
||||
const response = await axios.post(`${this.endpoint}/services/collector/event`, transformedEvent, {
|
||||
headers,
|
||||
});
|
||||
if (response.status === 200) {
|
||||
return true;
|
||||
} else {
|
||||
await sleep(backoff);
|
||||
backoff = this.getNextExponentialBackoff(backoff);
|
||||
}
|
||||
} catch (ex) {
|
||||
await sleep(backoff);
|
||||
backoff = this.getNextExponentialBackoff(backoff);
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends multiple events to the Splunk HEC endpoint.
|
||||
* @param events The events to send.
|
||||
* @returns A promise that resolves to true if all events are successfully sent and indexed, or false otherwise.
|
||||
*/
|
||||
public async sendEvents(events: any[], batchSize = 100): Promise<boolean> {
|
||||
if (!events || !Array.isArray(events)) {
|
||||
throw new Error('events must be an array');
|
||||
}
|
||||
if (events.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const batchedEvents = this.getBatchedEvents(events, batchSize);
|
||||
let index = 0;
|
||||
const channel = this.indexingAckEnabled ? randomUUID() : undefined;
|
||||
const headers = {
|
||||
Authorization: `Splunk ${this.defaultToken}`,
|
||||
'X-Splunk-Request-Channel': channel,
|
||||
};
|
||||
let backoff = 100;
|
||||
this.logger.info(
|
||||
`Sending batch${this.indexingAckEnabled ? `(${channel})` : ''} of ${batchedEvents.length} batches and ${
|
||||
events.length
|
||||
} events to Splunk`
|
||||
);
|
||||
do {
|
||||
try {
|
||||
const transformedEvent = this.transformEvent(batchedEvents[index], true);
|
||||
const response = await axios.post(`${this.endpoint}/services/collector/event`, transformedEvent, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
this.logger.info(`Batch ${index} of ${batchedEvents[index].length} events sent to Splunk`);
|
||||
backoff = 100;
|
||||
await sleep(backoff);
|
||||
index++;
|
||||
} else {
|
||||
this.logger.info(`Splunk HEC returned status code ${response.status}. Retrying in ${backoff}ms...`);
|
||||
await sleep(backoff);
|
||||
backoff = this.getNextExponentialBackoff(backoff);
|
||||
}
|
||||
} catch (ex: any) {
|
||||
this.logger.info(
|
||||
`Splunk HEC returned status code ${ex.response.status}. Retrying in ${backoff}ms...`
|
||||
);
|
||||
await sleep(backoff);
|
||||
backoff = this.getNextExponentialBackoff(backoff);
|
||||
}
|
||||
} while (index < batchedEvents.length);
|
||||
this.logger.info(
|
||||
`Batch${this.indexingAckEnabled ? `(${channel})` : ''} of ${events.length} events sent to Splunk`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an array of events into batches of a specified size.
|
||||
* @param {any[]} events - The array of events to be batched.
|
||||
* @param {number} batchSize - The size of each batch.
|
||||
* @returns {any[]} - An array of batches.
|
||||
*/
|
||||
private getBatchedEvents(events: any[], batchSize: number): any[] {
|
||||
const batchedEvents: any[] = [];
|
||||
for (let i = 0; i < events.length; i += batchSize) {
|
||||
batchedEvents.push(events.slice(i, i + batchSize));
|
||||
}
|
||||
return batchedEvents;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules"]
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "CommonJS",
|
||||
"target": "es6", //same as es2015
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": false,
|
||||
"strict": true,
|
||||
"noImplicitThis": false,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"noEmitOnError": false,
|
||||
"noUnusedParameters": true,
|
||||
"removeComments": false,
|
||||
"strictNullChecks": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["node_modules"],
|
||||
"ts-node": {
|
||||
"files": true
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ export class SecurityLogsConfigController {
|
|||
const id = randomUUID();
|
||||
const record = {
|
||||
id,
|
||||
name: params.name,
|
||||
tenant: params.tenant,
|
||||
type: params.type,
|
||||
config: params.config,
|
||||
|
@ -35,15 +36,17 @@ export class SecurityLogsConfigController {
|
|||
public async getAll(tenant: string, pageOffset?: number, pageLimit?: number, pageToken?: string) {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
||||
return await this.store.getByIndex(
|
||||
{
|
||||
name: IndexNames.Tenant,
|
||||
value: tenant,
|
||||
},
|
||||
pageOffset,
|
||||
pageLimit,
|
||||
pageToken
|
||||
);
|
||||
return tenant
|
||||
? await this.store.getByIndex(
|
||||
{
|
||||
name: IndexNames.Tenant,
|
||||
value: tenant,
|
||||
},
|
||||
pageOffset,
|
||||
pageLimit,
|
||||
pageToken
|
||||
)
|
||||
: await this.store.getAll(pageOffset, pageLimit, pageToken);
|
||||
}
|
||||
|
||||
public async get(id: string): Promise<SecurityLogsConfig | undefined> {
|
||||
|
@ -52,7 +55,7 @@ export class SecurityLogsConfigController {
|
|||
return await this.store.get(id);
|
||||
}
|
||||
|
||||
public async update(id: string, config: any): Promise<SecurityLogsConfig> {
|
||||
public async update(id: string, config: any, name?: string): Promise<SecurityLogsConfig> {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
||||
const currentConfig = await this.get(id);
|
||||
|
@ -65,6 +68,7 @@ export class SecurityLogsConfigController {
|
|||
type: currentConfig.type,
|
||||
tenant: currentConfig.tenant,
|
||||
config: config ?? currentConfig.config,
|
||||
name: name ?? currentConfig.name,
|
||||
};
|
||||
|
||||
const updatedConfig = {
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
export type SecurityLogsConfigCreate = {
|
||||
tenant: string;
|
||||
name?: string;
|
||||
config: any;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type SecurityLogsConfig = {
|
||||
id: string;
|
||||
name?: string;
|
||||
tenant: string;
|
||||
config: any;
|
||||
type: string;
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"@boxyhq/metrics": "0.2.6",
|
||||
"@boxyhq/react-ui": "3.3.25",
|
||||
"@boxyhq/saml-jackson": "file:npm",
|
||||
"@boxyhq/security-logs-sink": "file:./ee/sinks",
|
||||
"@heroicons/react": "2.1.1",
|
||||
"@retracedhq/logs-viewer": "2.7.0",
|
||||
"@retracedhq/retraced": "0.7.4",
|
||||
|
@ -73,6 +74,28 @@
|
|||
"npm": ">=8"
|
||||
}
|
||||
},
|
||||
"ee/sinks": {
|
||||
"name": "@boxyhq/security-logs-sink",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "1.6.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=8"
|
||||
}
|
||||
},
|
||||
"ee/sinks/node_modules/axios": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
|
||||
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.4",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
"version": "1.2.6",
|
||||
"dev": true,
|
||||
|
@ -348,6 +371,10 @@
|
|||
"resolved": "npm",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@boxyhq/security-logs-sink": {
|
||||
"resolved": "ee/sinks",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"devOptional": true,
|
||||
|
@ -4690,9 +4717,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.3",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
|
||||
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
|
@ -18452,24 +18479,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"npm/node_modules/follow-redirects": {
|
||||
"version": "1.15.3",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"npm/node_modules/for-each": {
|
||||
"version": "0.3.3",
|
||||
"license": "MIT",
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
"@boxyhq/metrics": "0.2.6",
|
||||
"@boxyhq/react-ui": "3.3.25",
|
||||
"@boxyhq/saml-jackson": "file:npm",
|
||||
"@boxyhq/security-logs-sink": "file:./ee/sinks",
|
||||
"@heroicons/react": "2.1.1",
|
||||
"@retracedhq/logs-viewer": "2.7.0",
|
||||
"@retracedhq/retraced": "0.7.4",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { default } from '../../../../../ee/security-logs-config/api/[id]/index';
|
|
@ -0,0 +1 @@
|
|||
export { default } from '../../../../ee/security-logs-config/api/index';
|
|
@ -0,0 +1 @@
|
|||
export { default } from '../../../../ee/security-logs/api/index';
|
Loading…
Reference in New Issue