added package configs and sinks

This commit is contained in:
ukrocks007 2024-02-02 18:29:53 +05:30
parent b67f9f3dbc
commit a05f9b18e7
24 changed files with 583 additions and 55 deletions

View File

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

View File

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

View File

@ -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({

View File

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

View File

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

View File

@ -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] &&

View File

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

9
ee/sinks/.gitignore vendored Normal file
View File

@ -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/**

110
ee/sinks/package-lock.json generated Normal file
View File

@ -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=="
}
}
}

30
ee/sinks/package.json Normal file
View File

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

11
ee/sinks/src/classes.ts Normal file
View File

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

3
ee/sinks/src/helper.ts Normal file
View File

@ -0,0 +1,3 @@
export const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};

20
ee/sinks/src/index.ts Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules"]
}

29
ee/sinks/tsconfig.json Normal file
View File

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

View File

@ -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 = {

View File

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

51
package-lock.json generated
View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from '../../../../../ee/security-logs-config/api/[id]/index';

View File

@ -0,0 +1 @@
export { default } from '../../../../ee/security-logs-config/api/index';

View File

@ -0,0 +1 @@
export { default } from '../../../../ee/security-logs/api/index';