mirror of https://github.com/boxyhq/jackson.git
Compare commits
93 Commits
3e062e6b49
...
aa361f6f22
Author | SHA1 | Date |
---|---|---|
Utkarsh Mehta | aa361f6f22 | |
Deepak Prabhakara | 5000983a36 | |
ukrocks007 | 6665967f20 | |
ukrocks007 | e56c6ea43a | |
ukrocks007 | 0ec7b654db | |
ukrocks007 | 7964460084 | |
ukrocks007 | 60e7429f73 | |
ukrocks007 | da43d4781f | |
ukrocks007 | 08bbf80397 | |
ukrocks007 | 511bc19ce2 | |
ukrocks007 | e8b00acc64 | |
ukrocks007 | 418e41017e | |
ukrocks007 | 0e87280e09 | |
ukrocks007 | e2eaff0daa | |
ukrocks007 | 8c0c79478e | |
ukrocks007 | 4bc8d3d7b9 | |
ukrocks007 | 5882bb8309 | |
Deepak Prabhakara | 508eec557b | |
ukrocks007 | 43a0d5534d | |
ukrocks007 | 1f51c3e561 | |
ukrocks007 | aeb2b441e7 | |
ukrocks007 | babfcbbf27 | |
ukrocks007 | d499887f7a | |
ukrocks007 | ffa506c41f | |
ukrocks007 | bf25eada7a | |
ukrocks007 | 7746cd47aa | |
ukrocks007 | cb23941914 | |
ukrocks007 | 184851c1de | |
ukrocks007 | 4b872939e4 | |
ukrocks007 | 62f9fcf78d | |
ukrocks007 | 884995be71 | |
Deepak Prabhakara | a1951065a0 | |
Deepak Prabhakara | 30e500329e | |
Deepak Prabhakara | 4688dfd87b | |
Deepak Prabhakara | 828e5b2ea3 | |
Deepak Prabhakara | be0db6334b | |
ukrocks007 | 7826824b2d | |
ukrocks007 | b66da67a34 | |
ukrocks007 | 9f16beec05 | |
ukrocks007 | 84b9b568df | |
ukrocks007 | 18aab06524 | |
ukrocks007 | 5b9d91b5ea | |
ukrocks007 | ef2aebf3af | |
ukrocks007 | 7b946146f2 | |
ukrocks007 | 39ad0e44af | |
ukrocks007 | 09879bb5c4 | |
ukrocks007 | f8427d001d | |
ukrocks007 | 0dbacae55a | |
ukrocks007 | eb723cb39e | |
ukrocks007 | 4c23cf8513 | |
ukrocks007 | a05f9b18e7 | |
ukrocks007 | b67f9f3dbc | |
ukrocks007 | 4b11c48e96 | |
ukrocks007 | 8b2ce8cee9 | |
Kiran K | a79cf7c117 | |
Kiran K | e0c7592f33 | |
Kiran K | 93e1199d87 | |
Kiran K | de01aed345 | |
Kiran K | bb119bbbb3 | |
Kiran K | 48e731416f | |
Kiran K | ddb51c1b35 | |
Kiran K | 71c66626a7 | |
Kiran K | 726b7e79dc | |
Kiran K | 0b0b23e4a2 | |
Kiran K | 116ad0da19 | |
Kiran K | 5d1c5cdf51 | |
Kiran K | e498c9aa4c | |
Kiran K | b88ca2eb04 | |
Kiran K | 6228d71c7f | |
Kiran K | 1a623647cb | |
Kiran K | 0097cfe9b0 | |
Kiran K | 70d7a21a0f | |
Kiran K | f3bdf88d5b | |
Kiran K | d2cd190ce1 | |
Kiran K | 681cf9537e | |
Kiran K | e08671161d | |
Kiran K | 303dc81727 | |
Kiran K | ae1ee8343f | |
Kiran K | a06c76a2e5 | |
Kiran K | 141d91d620 | |
Kiran K | 85050d5adb | |
Kiran K | aa9562a473 | |
Kiran K | 1867c6e7f7 | |
Kiran K | 533c7449a8 | |
Kiran K | add539869f | |
Kiran K | eda43bb740 | |
Kiran K | 837be177c7 | |
Kiran K | f23eb97161 | |
Kiran K | 8be2e20f90 | |
Kiran K | fc0a32da90 | |
Kiran K | 7cae8439eb | |
Kiran K | cf9a051085 | |
Kiran K | 60cd162672 |
|
@ -45,6 +45,9 @@ NEXTAUTH_ADMIN_CREDENTIALS=
|
|||
RETRACED_HOST_URL=
|
||||
RETRACED_EXTERNAL_URL=
|
||||
RETRACED_ADMIN_ROOT_TOKEN=
|
||||
RETRACED_API_KEY=
|
||||
RETRACED_PROJECT_ID=
|
||||
AUDIT_LOG_TEAMS=
|
||||
|
||||
# Admin Portal for Terminus (Privacy Vault)
|
||||
TERMINUS_PROXY_HOST_URL=
|
||||
|
|
|
@ -458,3 +458,20 @@ jobs:
|
|||
working-directory: ./${{ matrix.package }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- run: npm install
|
||||
working-directory: ./ee/security-sinks
|
||||
|
||||
- name: Publish Security_Sink
|
||||
if: github.ref == 'refs/heads/release' || contains(github.ref, 'refs/tags/beta-v')
|
||||
run: |
|
||||
npm install -g json
|
||||
JACKSON_VERSION=${{ needs.ci.outputs.NPM_VERSION }}
|
||||
json -I -f package.json -e "this.main=\"dist/index.js\""
|
||||
json -I -f package.json -e "this.types=\"dist/index.d.ts\""
|
||||
json -I -f package.json -e "this.version=\"${JACKSON_VERSION}\""
|
||||
|
||||
npm publish --tag ${{ needs.ci.outputs.PUBLISH_TAG }} --access public
|
||||
working-directory: ./ee/security-sinks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
|
@ -11,6 +11,7 @@ WORKDIR /app
|
|||
COPY package.json package-lock.json ./
|
||||
COPY npm npm
|
||||
COPY internal-ui internal-ui
|
||||
COPY ee/security-sinks ee/security-sinks
|
||||
COPY migrate.sh prebuild.ts ./
|
||||
RUN npm install
|
||||
RUN npm rebuild --arch=x64 --platform=linux --libc=musl sharp
|
||||
|
@ -22,6 +23,7 @@ WORKDIR /app
|
|||
|
||||
COPY --from=deps /app/npm ./npm
|
||||
COPY --from=deps /app/internal-ui ./internal-ui
|
||||
COPY --from=deps /app/ee/security-sinks ./ee/security-sinks
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
|
|
|
@ -2,6 +2,12 @@ const fs = require('fs');
|
|||
const path = require('path');
|
||||
const regExp = /\bt\('(.*?)'/gm;
|
||||
const altRegExp = /\bi18nKey='(.*?)'/gm;
|
||||
const exceptionList = [
|
||||
'bui-default-token',
|
||||
'bui-default-token-placeholder',
|
||||
'bui-splunk-collector-url',
|
||||
'bui-splunk-hec-endpoint-placeholder',
|
||||
];
|
||||
|
||||
const allStrings = {};
|
||||
|
||||
|
@ -43,7 +49,7 @@ files.forEach((file) => {
|
|||
});
|
||||
|
||||
Object.keys(localeFile).forEach((key) => {
|
||||
if (!allStrings[key]) {
|
||||
if (!allStrings[key] && !exceptionList.includes(key)) {
|
||||
error = true;
|
||||
console.error(`Unused key: ${key}`);
|
||||
}
|
||||
|
|
|
@ -128,6 +128,11 @@ export const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => {
|
|||
text: 'Branding',
|
||||
active: asPath.includes('/admin/settings/branding'),
|
||||
},
|
||||
{
|
||||
href: '/admin/settings/security-logs',
|
||||
text: 'Security Logs',
|
||||
active: asPath.includes('/admin/settings/security-logs'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import jackson from '@lib/jackson';
|
||||
import retraced from '@ee/retraced';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
@ -15,6 +16,12 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
const { logoUrl, faviconUrl, companyName, primaryColor } = req.body;
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'portal.branding.update',
|
||||
crud: 'u',
|
||||
req,
|
||||
});
|
||||
|
||||
res.json({
|
||||
data: await brandingController.update({ logoUrl, faviconUrl, companyName, primaryColor }),
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import jackson from '@lib/jackson';
|
||||
import retraced from '@ee/retraced';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
@ -34,6 +35,15 @@ const handlePATCH = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
const updatedApp = await samlFederatedController.app.update(req.body);
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'federation.app.update',
|
||||
crud: 'u',
|
||||
req,
|
||||
target: {
|
||||
id: updatedApp.id,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ data: updatedApp });
|
||||
};
|
||||
|
||||
|
@ -45,6 +55,15 @@ const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
await samlFederatedController.app.delete({ id });
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'federation.app.delete',
|
||||
crud: 'd',
|
||||
req,
|
||||
target: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ data: null });
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import jackson from '@lib/jackson';
|
||||
import retraced from '@ee/retraced';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
import { parsePaginateApiParams } from '@lib/utils';
|
||||
import { validateDevelopmentModeLimits } from '@lib/development-mode';
|
||||
|
@ -24,6 +25,15 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
const app = await samlFederatedController.app.create(req.body);
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'federation.app.create',
|
||||
crud: 'c',
|
||||
req,
|
||||
target: {
|
||||
id: app.id,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({ data: app });
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,273 @@
|
|||
import * as Retraced from '@retracedhq/retraced';
|
||||
import type { Event } from '@retracedhq/retraced';
|
||||
import type { NextApiRequest } from 'next';
|
||||
import { getToken as getNextAuthToken } from 'next-auth/jwt';
|
||||
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';
|
||||
import { extractAuthToken, validateApiKey } from '@lib/auth';
|
||||
|
||||
type AuditEventType =
|
||||
| 'sso.user.login'
|
||||
|
||||
// Single Sign On
|
||||
| 'sso.connection.create'
|
||||
| 'sso.connection.update'
|
||||
| 'sso.connection.delete'
|
||||
|
||||
// Directory Sync
|
||||
| 'dsync.connection.create'
|
||||
| 'dsync.connection.update'
|
||||
| 'dsync.connection.delete'
|
||||
| 'dsync.webhook_event.delete'
|
||||
|
||||
// Setup Link
|
||||
| 'sso.setuplink.create'
|
||||
| 'sso.setuplink.update'
|
||||
| 'sso.setuplink.delete'
|
||||
| 'dsync.setuplink.create'
|
||||
| 'dsync.setuplink.update'
|
||||
| 'dsync.setuplink.delete'
|
||||
|
||||
// Federated SAML
|
||||
| 'federation.app.create'
|
||||
| 'federation.app.update'
|
||||
| 'federation.app.delete'
|
||||
|
||||
// Retraced
|
||||
| 'retraced.project.create'
|
||||
|
||||
// Admin settings
|
||||
| 'portal.branding.update'
|
||||
| 'portal.user.login'
|
||||
|
||||
// Security Logs Config
|
||||
| 'security.logs.config.create'
|
||||
| 'security.logs.config.update'
|
||||
| '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'
|
||||
| 'api_key.create'
|
||||
| 'api_key.delete';
|
||||
|
||||
interface ReportAdminEventParams {
|
||||
action: AuditEventType;
|
||||
crud: Retraced.CRUD;
|
||||
target?: Retraced.Target;
|
||||
group?: Retraced.Group;
|
||||
actor?: Retraced.Actor;
|
||||
req?: NextApiRequest;
|
||||
}
|
||||
|
||||
interface ReportEventParams {
|
||||
action: AuditEventType;
|
||||
crud: Retraced.CRUD;
|
||||
actor: Retraced.Actor;
|
||||
req: NextApiRequest;
|
||||
group?: Retraced.Group;
|
||||
target?: Retraced.Target;
|
||||
sourceIp?: string;
|
||||
productId?: string;
|
||||
}
|
||||
|
||||
const adminPortalGroup = {
|
||||
id: 'boxyhq-admin-portal',
|
||||
name: 'BoxyHQ Admin Portal',
|
||||
};
|
||||
|
||||
let client: Retraced.Client | null = null;
|
||||
|
||||
// Check if audit log is enabled for a given group
|
||||
// const auditLogEnabledFor = (groupId: string) => {
|
||||
// return auditLogEnabledGroup.includes(groupId);
|
||||
// };
|
||||
|
||||
// Create a Retraced client
|
||||
const getClient = async () => {
|
||||
const { checkLicense } = await jackson();
|
||||
|
||||
if (!(await checkLicense())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!retracedOptions.hostUrl || !retracedOptions.apiKey || !retracedOptions.projectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client) {
|
||||
return client;
|
||||
}
|
||||
|
||||
client = new Retraced.Client({
|
||||
endpoint: retracedOptions.hostUrl,
|
||||
apiKey: retracedOptions.apiKey,
|
||||
projectId: retracedOptions.projectId,
|
||||
});
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
// Report events to Retraced
|
||||
const reportEvent = async (params: ReportEventParams) => {
|
||||
const { action, crud, actor, sourceIp, req } = params;
|
||||
try {
|
||||
const retracedClient = await getClient();
|
||||
|
||||
const retracedEvent: Event = {
|
||||
action,
|
||||
crud,
|
||||
actor,
|
||||
created: new Date(),
|
||||
source_ip: sourceIp || getClientIp(req),
|
||||
};
|
||||
|
||||
if ('group' in params && params.group) {
|
||||
retracedEvent.group = params.group;
|
||||
}
|
||||
|
||||
if ('target' in params && params.target) {
|
||||
retracedEvent.target = params.target;
|
||||
}
|
||||
|
||||
// Find team info if productId is provided
|
||||
if ('productId' in params && params.productId) {
|
||||
const { productController } = await jackson();
|
||||
|
||||
const product = await productController.get(params.productId);
|
||||
|
||||
if (!product) {
|
||||
console.error(`Can't find product info for productId ${params.productId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (product.teamId && product.teamName) {
|
||||
retracedEvent.group = {
|
||||
id: product.teamId,
|
||||
name: product.teamName,
|
||||
};
|
||||
}
|
||||
|
||||
if (product.id && product.name) {
|
||||
retracedEvent.target = {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!retracedEvent.group?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auditLogEnabledGroup.length && !auditLogEnabledGroup.includes(retracedEvent.group?.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (retracedClient) {
|
||||
await retracedClient.reportEvent(retracedEvent);
|
||||
}
|
||||
|
||||
await sendSecurityLogs(retracedEvent, retracedEvent.group?.id);
|
||||
} catch (error: any) {
|
||||
console.error('Error reporting event to Retraced', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Report Admin portal events to Retraced
|
||||
export const reportAdminPortalEvent = async (params: ReportAdminEventParams) => {
|
||||
const { action, crud, target, actor, group, req } = params;
|
||||
|
||||
try {
|
||||
const retracedClient = await getClient();
|
||||
|
||||
const retracedEvent: Event = {
|
||||
action,
|
||||
crud,
|
||||
target,
|
||||
actor: actor ?? (await getAdminUser(req)),
|
||||
group: group || adminPortalGroup,
|
||||
created: new Date(),
|
||||
};
|
||||
const ip = getClientIp(req);
|
||||
|
||||
if (ip) {
|
||||
retracedEvent['source_ip'] = ip;
|
||||
}
|
||||
if (retracedClient) {
|
||||
await retracedClient.reportEvent(retracedEvent);
|
||||
}
|
||||
await sendSecurityLogs(retracedEvent);
|
||||
} catch (error: any) {
|
||||
console.error('Error reporting event to Retraced', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Find admin actor info from NextAuth token
|
||||
const getAdminUser = async (req: NextApiRequest | undefined) => {
|
||||
if (!req) {
|
||||
throw new Error(`NextApiRequest is required to get actor info for Retraced event.`);
|
||||
}
|
||||
|
||||
// API keys used for admin portal routes
|
||||
if (validateApiKey(extractAuthToken(req))) {
|
||||
return {
|
||||
id: 'API',
|
||||
name: 'API',
|
||||
};
|
||||
} else {
|
||||
const user = await getNextAuthToken({
|
||||
req,
|
||||
cookieName: sessionName,
|
||||
});
|
||||
|
||||
if (!user || !user.email || !user.name) {
|
||||
throw new Error(`Can't find actor info from the NextAuth token.`);
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.email,
|
||||
name: user.name,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Find Ip from request
|
||||
const getClientIp = (req: NextApiRequest | undefined) => {
|
||||
if (!req) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceIp = requestIp.getClientIp(req);
|
||||
|
||||
if (!sourceIp.startsWith('::')) {
|
||||
return sourceIp as string;
|
||||
}
|
||||
};
|
||||
|
||||
const retraced = {
|
||||
reportEvent,
|
||||
reportAdminPortalEvent,
|
||||
};
|
||||
|
||||
export default retraced;
|
|
@ -0,0 +1,92 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import jackson from '@lib/jackson';
|
||||
import retraced from '@ee/retraced';
|
||||
import { ApiError } from '@lib/error';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
try {
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return await handleGET(req, res);
|
||||
case 'PUT':
|
||||
return await handlePUT(req, res);
|
||||
case 'DELETE':
|
||||
return await handleDELETE(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET, PUT, DELETE');
|
||||
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 handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { securityLogsConfigController } = await jackson();
|
||||
|
||||
const { id } = req.query as { id: string };
|
||||
|
||||
const config = await securityLogsConfigController.get(id);
|
||||
|
||||
if (!config) {
|
||||
throw new ApiError(`Security Logs Config with id ${id} not found`, 404);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
...config,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Update Security Logs config
|
||||
const handlePUT = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { securityLogsConfigController } = await jackson();
|
||||
|
||||
const { id } = req.query as { id: string };
|
||||
|
||||
const { config, name } = req.body as { config: any; name?: string };
|
||||
|
||||
const updatedApp = await securityLogsConfigController.update(id, config, name);
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'security.logs.config.update',
|
||||
crud: 'u',
|
||||
req,
|
||||
target: {
|
||||
id: updatedApp.id,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({ data: updatedApp });
|
||||
};
|
||||
|
||||
// Delete the Security Logs config
|
||||
const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { securityLogsConfigController } = await jackson();
|
||||
|
||||
const { id } = req.query as { id: string };
|
||||
|
||||
await securityLogsConfigController.delete(id);
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'security.logs.config.delete',
|
||||
crud: 'd',
|
||||
req,
|
||||
target: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({ data: {} });
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -0,0 +1,84 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import jackson from '@lib/jackson';
|
||||
import retraced from '@ee/retraced';
|
||||
import { adminPortalSSODefaults, boxyhqHosted } from '@lib/env';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
|
||||
try {
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
return await handlePOST(req, res);
|
||||
case 'GET':
|
||||
return await handleGET(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', 'POST, GET');
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
} catch (error: any) {
|
||||
const { message, statusCode = 500 } = error;
|
||||
|
||||
return res.status(statusCode).json({ error: { message } });
|
||||
}
|
||||
};
|
||||
|
||||
// Create new Security Logs Config
|
||||
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { securityLogsConfigController } = await jackson();
|
||||
|
||||
const { tenant, type, config, name } = req.body as {
|
||||
tenant: string;
|
||||
type: string;
|
||||
config: any;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
const id = await securityLogsConfigController.createSecurityLogsConfig({
|
||||
tenant: boxyhqHosted && tenant ? tenant : adminPortalSSODefaults.tenant,
|
||||
type,
|
||||
config,
|
||||
name,
|
||||
});
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'security.logs.config.create',
|
||||
crud: 'c',
|
||||
req,
|
||||
target: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(201).json({ data: id });
|
||||
};
|
||||
|
||||
// Get Security Logs Configs
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { securityLogsConfigController } = await jackson();
|
||||
|
||||
const { pageOffset, pageLimit, pageToken, tenant } = req.query as {
|
||||
pageOffset: string;
|
||||
pageLimit: string;
|
||||
pageToken?: string;
|
||||
tenant: string;
|
||||
};
|
||||
|
||||
const offset = parseInt(pageOffset);
|
||||
const limit = parseInt(pageLimit);
|
||||
|
||||
const configs = await securityLogsConfigController.getAll(
|
||||
boxyhqHosted ? tenant : adminPortalSSODefaults.tenant,
|
||||
offset,
|
||||
limit,
|
||||
pageToken
|
||||
);
|
||||
if (configs.pageToken) {
|
||||
res.setHeader('jackson-pagetoken', configs.pageToken);
|
||||
}
|
||||
|
||||
return res.json({ data: configs.data });
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { errorToast, successToast } from '@components/Toaster';
|
||||
import { useRouter } from 'next/router';
|
||||
import LicenseRequired from '@components/LicenseRequired';
|
||||
import { SecurityLogsConfigEdit } from '@boxyhq/internal-ui';
|
||||
|
||||
const UpdateConfig = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
||||
const router = useRouter();
|
||||
const { id } = router.query as { id: string };
|
||||
|
||||
if (!hasValidLicense) {
|
||||
return <LicenseRequired />;
|
||||
}
|
||||
|
||||
const urls = {
|
||||
getById: (id: string) => `/api/admin/security-logs-config/${id}`,
|
||||
updateById: (id: string) => `/api/admin/security-logs-config/${id}`,
|
||||
deleteById: (id: string) => `/api/admin/security-logs-config/${id}`,
|
||||
listConfigs: '/admin/settings/security-logs',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecurityLogsConfigEdit id={id} urls={urls} onSuccess={successToast} onError={errorToast} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateConfig;
|
|
@ -0,0 +1,26 @@
|
|||
import LicenseRequired from '@components/LicenseRequired';
|
||||
import { errorToast, successToast } from '@components/Toaster';
|
||||
import { SecurityLogsConfigs } from '@boxyhq/internal-ui';
|
||||
|
||||
const ConfigList = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
||||
if (!hasValidLicense) {
|
||||
return <LicenseRequired />;
|
||||
}
|
||||
|
||||
const urls = {
|
||||
listConfigs: '/api/admin/security-logs-config',
|
||||
createConfig: '/admin/settings/security-logs/new',
|
||||
editById: (id) => `/admin/settings/security-logs/${id}`,
|
||||
deleteById: (id: string) => `/api/admin/security-logs-config/${id}`,
|
||||
};
|
||||
return (
|
||||
<SecurityLogsConfigs
|
||||
urls={urls}
|
||||
skipColumns={['endpoint']}
|
||||
onSuccess={successToast}
|
||||
onError={errorToast}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigList;
|
|
@ -0,0 +1,18 @@
|
|||
import { successToast, errorToast } from '@components/Toaster';
|
||||
import LicenseRequired from '@components/LicenseRequired';
|
||||
import { SecurityLogsConfigCreate } from '@boxyhq/internal-ui';
|
||||
|
||||
const NewConfiguration = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
||||
if (!hasValidLicense) {
|
||||
return <LicenseRequired />;
|
||||
}
|
||||
|
||||
const urls = {
|
||||
createConfig: '/api/admin/security-logs-config',
|
||||
listConfigs: '/admin/settings/security-logs',
|
||||
};
|
||||
|
||||
return <SecurityLogsConfigCreate urls={urls} onSuccess={successToast} onError={errorToast} />;
|
||||
};
|
||||
|
||||
export default NewConfiguration;
|
|
@ -0,0 +1,45 @@
|
|||
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,7 @@
|
|||
# Enterprise Edition
|
||||
|
||||
Welcome to the Enterprise Edition ("/ee") of BoxyHQ.
|
||||
|
||||
The [/ee](https://github.com/boxyhq/jackson/tree/main/ee) subfolder is the place for all the **Enterprise** features for this repository.
|
||||
|
||||
> _❗ NOTE: This section is copyrighted (unlike the rest of our [repository](https://github.com/boxyhq/jackson)). You are not allowed to use this code without obtaining a proper [license](https://boxyhq.com/pricing) first.❗_
|
|
@ -0,0 +1 @@
|
|||
The BoxyHQ Enterprise Edition (EE) license (the “EE License”)
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/zsh -ex
|
||||
|
||||
# TODO: Make this generic so everyone can run it
|
||||
|
||||
VERSION=0.0.0
|
||||
|
||||
# Unpublish the current version
|
||||
npm unpublish --registry http://localhost:4873/ @boxyhq/security-sinks@$VERSION --force
|
||||
|
||||
# Build the package
|
||||
rm -rf dist
|
||||
npm run build
|
||||
|
||||
# Publish
|
||||
npm publish --registry http://localhost:4873/
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"name": "@boxyhq/security-sinks",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@boxyhq/security-sinks",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "1.6.8"
|
||||
},
|
||||
"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.8",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
|
||||
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"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.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"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,34 @@
|
|||
{
|
||||
"name": "@boxyhq/security-sinks",
|
||||
"version": "0.0.0",
|
||||
"description": "Package to deliver security logs to different SIEMs and other destination",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"prepublishOnly": "npm run build",
|
||||
"sort": "npx sort-package-json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/boxyhq/jackson.git"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"typings": "src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"ENTERPRISE.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"keywords": [
|
||||
"SIEM",
|
||||
"Sinks",
|
||||
"Splunk"
|
||||
],
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "1.6.8"
|
||||
}
|
||||
}
|
|
@ -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,178 @@
|
|||
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;
|
||||
const retry = true;
|
||||
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 (retry);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
}
|
|
@ -9,6 +9,10 @@ npm unpublish --registry http://localhost:4873/ @boxyhq/internal-ui@$VERSION
|
|||
|
||||
# Build the package
|
||||
rm -rf dist
|
||||
|
||||
# Update package.json main and types
|
||||
jq '.main = "./dist/index.js" | .types = "./dist/index.d.ts"' package.json > tmp.json && mv tmp.json package.json
|
||||
|
||||
npm run build
|
||||
|
||||
# Publish
|
||||
|
@ -20,6 +24,9 @@ npm publish --registry http://localhost:4873/
|
|||
# npm i --save-exact --registry http://localhost:4873/ @boxyhq/internal-ui@$VERSION
|
||||
# rm -rf .next
|
||||
|
||||
# revert package.json main and types
|
||||
jq '.main = "./src/index.ts" | .types = "./src/index.d.ts"' package.json > tmp.json && mv tmp.json package.json
|
||||
|
||||
# Install the published version in `boxyhq/saas-app`
|
||||
cd ../../saas-app
|
||||
npm uninstall @boxyhq/internal-ui
|
||||
|
|
|
@ -5,3 +5,4 @@ export * from './dsync';
|
|||
export * from './provider';
|
||||
export * from './sso-tracer';
|
||||
export * from './setup-link';
|
||||
export * from './security-logs-config';
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { useRouter } from '../hooks';
|
||||
import { configMap } from './lib';
|
||||
import { ButtonPrimary, LinkBack } from '../shared';
|
||||
|
||||
export const SecurityLogsConfigCreate = ({
|
||||
urls,
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
urls: {
|
||||
createConfig: string;
|
||||
listConfigs: string;
|
||||
};
|
||||
onSuccess: (message: string) => void;
|
||||
onError: (message: string) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { router } = useRouter();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [config, setConfig] = useState({});
|
||||
const [name, setName] = useState('');
|
||||
const [type, setType] = useState(t('bui-shared-select-type'));
|
||||
|
||||
const onSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const rawResponse = await fetch(urls.createConfig, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ name, type: configMap[type].type, config }),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
const response = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
if (response?.error?.message) {
|
||||
onError(response.error.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ('data' in response) {
|
||||
onSuccess(t('bui-slc-new-success'));
|
||||
router?.replace(urls.listConfigs);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
setConfig({
|
||||
...config,
|
||||
[target.id]: target.value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinkBack href={urls.listConfigs} />
|
||||
<h2 className='mb-5 mt-5 font-bold text-gray-700 md:text-xl'>{t('bui-slc-add')}</h2>
|
||||
<div className='rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800'>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className='flex flex-col space-y-3'>
|
||||
<div className='form-control w-full md:w-1/2'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('bui-shared-type')}</span>
|
||||
</label>
|
||||
<select
|
||||
className='select-bordered select w-full'
|
||||
id='type'
|
||||
value={type}
|
||||
onChange={(e) => {
|
||||
setType(e.target.value);
|
||||
}}
|
||||
required>
|
||||
{[t('bui-shared-select-type'), ...Object.keys(configMap)].map((key) => {
|
||||
return (
|
||||
<option key={key} value={key}>
|
||||
{key}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className='form-control w-full md:w-1/2'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t('bui-shared-name')}</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='name'
|
||||
className='input-bordered input'
|
||||
value={name}
|
||||
required={false}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t('bui-shared-name')}
|
||||
/>
|
||||
</div>
|
||||
{type && (
|
||||
<>
|
||||
{configMap[type] &&
|
||||
configMap[type].fields.map((field) => {
|
||||
return (
|
||||
<div key={field.index} className='form-control w-full md:w-1/2'>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t(field.label)}</span>
|
||||
</label>
|
||||
<input
|
||||
type={field.type}
|
||||
id={field.name}
|
||||
className='input-bordered input'
|
||||
required
|
||||
onChange={onChange}
|
||||
placeholder={t(field.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<ButtonPrimary loading={loading}>{t('bui-slc-create-config')}</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,69 @@
|
|||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { ButtonDanger, ConfirmationModal } from '../shared';
|
||||
import { useRouter } from '../hooks';
|
||||
import { ApiResponse } from '../types';
|
||||
|
||||
export const SecurityLogsConfigDelete = ({
|
||||
id,
|
||||
urls,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: {
|
||||
id: string;
|
||||
urls: { deleteById: (id: string) => string; listConfigs: string };
|
||||
onError: (string) => void;
|
||||
onSuccess: (string) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { router } = useRouter();
|
||||
|
||||
const [delModalVisible, setDelModalVisible] = useState(false);
|
||||
|
||||
const deleteApp = async () => {
|
||||
const rawResponse = await fetch(urls.deleteById(id), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
const response: ApiResponse<unknown> = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
onError(response.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if ('data' in response) {
|
||||
onSuccess(t('bui-slc-delete-success'));
|
||||
router?.replace(urls.listConfigs);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='mt-5 flex items-center rounded bg-red-100 p-6 text-red-900'>
|
||||
<div className='flex-1'>
|
||||
<h6 className='mb-1 font-medium'>{t('bui-slc-delete-confirmation')}</h6>
|
||||
<p className='font-light'>{t('bui-slc-logs-noop')}</p>
|
||||
</div>
|
||||
<ButtonDanger
|
||||
type='button'
|
||||
data-modal-toggle='popup-modal'
|
||||
onClick={() => {
|
||||
setDelModalVisible(true);
|
||||
}}>
|
||||
{t('bui-shared-delete')}
|
||||
</ButtonDanger>
|
||||
</section>
|
||||
<ConfirmationModal
|
||||
title={t('bui-slc-delete')}
|
||||
description={t('bui-slc-delete-modal-confirmation')}
|
||||
visible={delModalVisible}
|
||||
onConfirm={deleteApp}
|
||||
onCancel={() => {
|
||||
setDelModalVisible(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,140 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SecurityLogsConfig } from '@boxyhq/saml-jackson';
|
||||
|
||||
import { fetcher } from '../utils';
|
||||
import { ButtonPrimary, LinkBack, Loading } from '../shared';
|
||||
import { SinkConfigMapField, getFieldsFromSinkType } from './lib';
|
||||
import { Error } from '../shared';
|
||||
import { ApiError, ApiResponse, ApiSuccess } from '../types';
|
||||
import { useRouter } from '../hooks';
|
||||
import { SecurityLogsConfigDelete } from './SecurityLogsConfigDelete';
|
||||
|
||||
export const SecurityLogsConfigEdit = ({
|
||||
id,
|
||||
urls,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: {
|
||||
id: string;
|
||||
urls: {
|
||||
getById: (id: string) => string;
|
||||
updateById: (id: string) => string;
|
||||
deleteById: (id: string) => string;
|
||||
listConfigs: string;
|
||||
};
|
||||
onError: (error: string) => void;
|
||||
onSuccess: (message: string) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { router } = useRouter();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [config, setConfig] = useState<any>({});
|
||||
const [fields, setFields] = useState<SinkConfigMapField[]>([]);
|
||||
|
||||
const { data, error, isLoading } = useSWR<ApiSuccess<SecurityLogsConfig>, ApiError>(
|
||||
urls.getById(id),
|
||||
fetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setConfig(data.data?.config);
|
||||
setFields(getFieldsFromSinkType(data.data?.type) || []);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (error) {
|
||||
<Error message={error.message} />;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const onSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const rawResponse = await fetch(urls.updateById(id), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
config,
|
||||
type: data?.data?.type,
|
||||
}),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
const response: ApiResponse<SecurityLogsConfig> = await rawResponse.json();
|
||||
|
||||
if ('error' in response) {
|
||||
if (response?.error?.message) {
|
||||
onError(response.error.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ('data' in response) {
|
||||
onSuccess(t('bui-slc-update-success'));
|
||||
router?.replace(urls.listConfigs);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
setConfig({
|
||||
...config,
|
||||
[target.id]: target.value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<LinkBack href={urls.listConfigs} />
|
||||
<div className='mb-5 flex items-center justify-between'>
|
||||
<h2 className='mt-5 font-bold text-gray-700 md:text-xl'>{t('bui-slc-update')}</h2>
|
||||
</div>
|
||||
<div className='rounded border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-800'>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className='space-y-3'>
|
||||
{fields.map((field) => {
|
||||
return (
|
||||
<div className='form-control w-full md:w-1/2' key={field.index}>
|
||||
<label className='label'>
|
||||
<span className='label-text'>{t(field.label)}</span>
|
||||
</label>
|
||||
<input
|
||||
type={field.type}
|
||||
className='input-bordered input'
|
||||
id={field.name}
|
||||
placeholder={t(field.placeholder)}
|
||||
value={config[field.name]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div>
|
||||
<ButtonPrimary type='submit' loading={loading}>
|
||||
{t('bui-shared-save-changes')}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<SecurityLogsConfigDelete id={id} urls={urls} onSuccess={onSuccess} onError={onError} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,233 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import type { SecurityLogsConfig } from '@boxyhq/saml-jackson';
|
||||
import useSWR from 'swr';
|
||||
import { ConfirmationModal, EmptyState, LinkPrimary, Loading, pageLimit, Pagination, Table } from '../shared';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import PencilIcon from '@heroicons/react/24/outline/PencilIcon';
|
||||
import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
|
||||
|
||||
import { addQueryParamsToPath, fetcher } from '../utils';
|
||||
import { usePaginate, useRouter } from '../hooks';
|
||||
import { TableBodyType } from '../shared/Table';
|
||||
import { ApiError, ApiSuccess } from '../types';
|
||||
import { getDisplayTypeFromSinkType } from './lib';
|
||||
import { Error } from '../shared';
|
||||
import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
|
||||
|
||||
export const SecurityLogsConfigs = ({
|
||||
urls,
|
||||
skipColumns,
|
||||
onError,
|
||||
onSuccess,
|
||||
}: {
|
||||
urls: {
|
||||
listConfigs: string;
|
||||
createConfig: string;
|
||||
editById: (id: string) => string;
|
||||
deleteById?: (id: string) => string;
|
||||
};
|
||||
onSuccess?: (id: string) => void;
|
||||
onError?: (message: string) => void;
|
||||
skipColumns?: string[];
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { router } = useRouter();
|
||||
|
||||
const [connection, setConnection] = useState<string | null>(null);
|
||||
const [delModalVisible, setDelModalVisible] = useState(false);
|
||||
const { paginate, setPaginate, pageTokenMap, setPageTokenMap } = usePaginate(router!);
|
||||
|
||||
const params = {
|
||||
pageOffset: paginate.offset,
|
||||
pageLimit: pageLimit,
|
||||
};
|
||||
|
||||
// For DynamoDB
|
||||
if (paginate.offset > 0 && pageTokenMap[paginate.offset - pageLimit]) {
|
||||
params['pageToken'] = pageTokenMap[paginate.offset - pageLimit];
|
||||
}
|
||||
|
||||
const getConfigsUrl = addQueryParamsToPath(urls.listConfigs, params);
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR<ApiSuccess<SecurityLogsConfig[]>, ApiError>(
|
||||
getConfigsUrl,
|
||||
fetcher
|
||||
);
|
||||
|
||||
// Delete Splunk Connection
|
||||
const deleteSplunkConnection = async () => {
|
||||
const response = await fetch(urls.deleteById!(connection!), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data?.data) {
|
||||
mutate();
|
||||
setConnection(null);
|
||||
setDelModalVisible(false);
|
||||
onSuccess!(t('bui-slc-delete-success'));
|
||||
} else {
|
||||
onError!(data?.error);
|
||||
}
|
||||
};
|
||||
|
||||
const nextPageToken = data?.pageToken;
|
||||
|
||||
// store the nextPageToken against the pageOffset
|
||||
useEffect(() => {
|
||||
if (nextPageToken) {
|
||||
setPageTokenMap((tokenMap) => ({ ...tokenMap, [paginate.offset]: nextPageToken }));
|
||||
}
|
||||
}, [nextPageToken, paginate.offset]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Error message={error.message} />;
|
||||
}
|
||||
|
||||
const configs = data?.data || [];
|
||||
const noConfigs = configs.length === 0 && paginate.offset === 0;
|
||||
const noMoreResults = configs.length === 0 && paginate.offset > 0;
|
||||
|
||||
let columns = [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('bui-shared-name'),
|
||||
wrap: true,
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: t('bui-shared-type'),
|
||||
wrap: true,
|
||||
dataIndex: 'type',
|
||||
},
|
||||
{
|
||||
key: 'tenant',
|
||||
label: t('bui-shared-tenant'),
|
||||
wrap: true,
|
||||
dataIndex: 'tenant',
|
||||
},
|
||||
{
|
||||
key: 'endpoint',
|
||||
label: t('bui-shared-endpoint'),
|
||||
wrap: true,
|
||||
dataIndex: 'config.endpoint',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: t('bui-shared-actions'),
|
||||
wrap: true,
|
||||
dataIndex: null,
|
||||
},
|
||||
];
|
||||
|
||||
if (skipColumns) {
|
||||
columns = columns.filter((column) => !skipColumns.includes(column.key));
|
||||
}
|
||||
|
||||
const cols = columns.map(({ label }) => label);
|
||||
|
||||
const body: TableBodyType[] = configs.map((config) => {
|
||||
return {
|
||||
id: config.id,
|
||||
cells: columns.map((column) => {
|
||||
const dataIndex = column.dataIndex as string;
|
||||
|
||||
if (dataIndex === null) {
|
||||
return {
|
||||
actions: urls.deleteById
|
||||
? [
|
||||
{
|
||||
text: t('bui-shared-edit'),
|
||||
onClick: () => router?.replace(urls.editById(config.id)),
|
||||
icon: <PencilIcon className='w-5' />,
|
||||
},
|
||||
{
|
||||
color: 'error',
|
||||
text: t('bui-shared-delete'),
|
||||
icon: <TrashIcon className='h-5 w-5' />,
|
||||
onClick: () => {
|
||||
setConnection(config.id);
|
||||
setDelModalVisible(true);
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
text: t('bui-shared-edit'),
|
||||
onClick: () => router?.replace(urls.editById(config.id)),
|
||||
icon: <PencilIcon className='w-5' />,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (dataIndex.indexOf('.') !== -1) {
|
||||
const keys = dataIndex.split('.');
|
||||
const retValue = {
|
||||
wrap: column.wrap,
|
||||
text: config,
|
||||
};
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
retValue.text = retValue.text ? retValue.text[keys[i]] : '';
|
||||
}
|
||||
return retValue;
|
||||
} else if (dataIndex === 'type') {
|
||||
return {
|
||||
wrap: column.wrap,
|
||||
text: getDisplayTypeFromSinkType(config.type),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
wrap: column.wrap,
|
||||
text: config[dataIndex],
|
||||
};
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mb-5 flex items-center justify-between'>
|
||||
<h2 className='font-bold text-gray-700 dark:text-white md:text-xl'>{t('bui-slc')}</h2>
|
||||
<div className='flex'>
|
||||
<LinkPrimary className='m-2' Icon={PlusIcon} href={urls.createConfig}>
|
||||
{t('bui-slc-new')}
|
||||
</LinkPrimary>
|
||||
</div>
|
||||
</div>
|
||||
{noConfigs ? (
|
||||
<>
|
||||
<EmptyState title={t('bui-slc-empty')} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Table noMoreResults={noMoreResults} cols={cols} body={body} />
|
||||
<Pagination
|
||||
itemsCount={configs.length}
|
||||
offset={paginate.offset}
|
||||
onPrevClick={() => {
|
||||
setPaginate({
|
||||
offset: paginate.offset - pageLimit,
|
||||
});
|
||||
}}
|
||||
onNextClick={() => {
|
||||
setPaginate({
|
||||
offset: paginate.offset + pageLimit,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
title='Delete Splunk Connection'
|
||||
visible={delModalVisible}
|
||||
description={t('bui-slc-delete-modal-confirmation')}
|
||||
onConfirm={() => deleteSplunkConnection()}
|
||||
onCancel={() => setDelModalVisible(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
export { SecurityLogsConfigs } from './SecurityLogsConfigs';
|
||||
export { SecurityLogsConfigCreate } from './SecurityLogsConfigCreate';
|
||||
export { SecurityLogsConfigEdit } from './SecurityLogsConfigEdit';
|
||||
export {
|
||||
configMap,
|
||||
getDisplayTypeFromSinkType,
|
||||
getFieldsFromSinkType,
|
||||
getSecurityLogsConfigTypes,
|
||||
} from './lib';
|
||||
export type { SinkConfigMap, SinkConfigMapField, SecurityLogsType } from './lib';
|
|
@ -0,0 +1,59 @@
|
|||
export type SecurityLogsType = 'splunk_hec_logs';
|
||||
|
||||
export type SinkConfigMapField = {
|
||||
index: number;
|
||||
label: string;
|
||||
name: string;
|
||||
type: string;
|
||||
placeholder: string;
|
||||
};
|
||||
|
||||
export type SinkConfigMap = {
|
||||
[key: string]: {
|
||||
type: SecurityLogsType;
|
||||
fields: SinkConfigMapField[];
|
||||
};
|
||||
};
|
||||
|
||||
export const configMap: {
|
||||
[key: string]: {
|
||||
type: SecurityLogsType;
|
||||
fields: SinkConfigMapField[];
|
||||
};
|
||||
} = {
|
||||
Splunk: {
|
||||
type: 'splunk_hec_logs',
|
||||
fields: [
|
||||
{
|
||||
index: 1,
|
||||
label: 'bui-splunk-collector-url',
|
||||
name: 'endpoint',
|
||||
type: 'string',
|
||||
placeholder: 'bui-splunk-hec-endpoint-placeholder',
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
label: 'bui-default-token',
|
||||
name: 'default_token',
|
||||
type: 'string',
|
||||
placeholder: 'bui-default-token-placeholder',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const getDisplayTypeFromSinkType = (type: string): string | undefined => {
|
||||
return Object.keys(configMap).find((key) => configMap[key].type === type);
|
||||
};
|
||||
|
||||
export const getFieldsFromSinkType = (type: string): SinkConfigMapField[] | undefined => {
|
||||
const key = getDisplayTypeFromSinkType(type);
|
||||
if (!key) {
|
||||
return undefined;
|
||||
}
|
||||
return configMap[key].fields;
|
||||
};
|
||||
|
||||
export const getSecurityLogsConfigTypes = (): string[] => {
|
||||
return Object.keys(configMap).map((key) => configMap[key].type);
|
||||
};
|
|
@ -5,6 +5,8 @@ export interface ApiError extends Error {
|
|||
status: number;
|
||||
}
|
||||
|
||||
export type ApiResponse<T = any> = ApiSuccess<T> | { error: ApiError };
|
||||
|
||||
enum DirectorySyncProviders {
|
||||
'azure-scim-v2' = 'Azure SCIM v2.0',
|
||||
'onelogin-scim-v2' = 'OneLogin SCIM v2.0',
|
||||
|
|
|
@ -23,6 +23,8 @@ const retraced = {
|
|||
hostUrl: process.env.RETRACED_HOST_URL,
|
||||
externalUrl: process.env.RETRACED_EXTERNAL_URL || process.env.RETRACED_HOST_URL,
|
||||
adminToken: process.env.RETRACED_ADMIN_ROOT_TOKEN,
|
||||
apiKey: process.env.RETRACED_API_KEY,
|
||||
projectId: process.env.RETRACED_PROJECT_ID,
|
||||
};
|
||||
|
||||
// Terminus
|
||||
|
@ -129,3 +131,5 @@ export { retraced as retracedOptions };
|
|||
export { terminus as terminusOptions };
|
||||
export { apiKeys };
|
||||
export { jacksonOptions };
|
||||
|
||||
export const auditLogEnabledGroup = process.env.AUDIT_LOG_TEAMS ? process.env.AUDIT_LOG_TEAMS.split(',') : [];
|
||||
|
|
|
@ -115,11 +115,14 @@
|
|||
"setup-link-regenerated": "The setup link regenerated.",
|
||||
"setup-link-copied": "The setup link copied to the clipboard.",
|
||||
"setup-link-deleted": "The setup link deleted.",
|
||||
"bui-default-token": "Default Token",
|
||||
"bui-default-token-placeholder": "Token generated by splunk for HEC",
|
||||
"bui-shared-name": "Name",
|
||||
"bui-shared-select-type": "Select a type",
|
||||
"bui-shared-type": "Type",
|
||||
"bui-shared-tenant": "Tenant",
|
||||
"bui-shared-product": "Product",
|
||||
"bui-shared-actions": "Actions",
|
||||
"bui-shared-type": "Type",
|
||||
"bui-shared-edit": "Edit",
|
||||
"bui-shared-save-changes": "Save Changes",
|
||||
"bui-shared-no-more-results": "No more results found",
|
||||
|
@ -138,6 +141,7 @@
|
|||
"bui-shared-close": "Close",
|
||||
"bui-shared-copy": "Copy",
|
||||
"bui-shared-active": "Active",
|
||||
"bui-shared-endpoint": "Endpoint",
|
||||
"bui-shared-email": "Email",
|
||||
"bui-shared-logo-url": "Logo URL",
|
||||
"bui-shared-logo-url-desc": "Provide a URL to your logo. Recommend PNG or SVG formats. The image will be capped to a maximum height of 56px.",
|
||||
|
@ -145,6 +149,21 @@
|
|||
"bui-shared-favicon-url-desc": "Provide a URL to your favicon. Recommend PNG, SVG, or ICO formats.",
|
||||
"bui-shared-primary-color": "Primary Color",
|
||||
"bui-shared-primary-color-desc": "Primary color will be applied to buttons, links, and other elements.",
|
||||
"bui-slc": "Security Logs Configurations",
|
||||
"bui-slc-add": "Add Security Logs Configuration",
|
||||
"bui-slc-create-config": "Create Configuration",
|
||||
"bui-slc-delete": "Delete the Security logs config?",
|
||||
"bui-slc-delete-confirmation": "Delete this Security Logs Configuration",
|
||||
"bui-slc-delete-modal-confirmation": "This action cannot be undone. This will permanently delete the Configuration.",
|
||||
"bui-slc-delete-success": "Security Logs Configuration deleted successfully",
|
||||
"bui-slc-empty": "No Security Logs Configuration found.",
|
||||
"bui-slc-logs-noop": "Security logs won't be sent to this destination.",
|
||||
"bui-slc-new": "New Configuration",
|
||||
"bui-slc-new-success": "Security logs config created successfully.",
|
||||
"bui-slc-update": "Update Security Logs Configuration",
|
||||
"bui-slc-update-success": "Security logs configuration updated successfully.",
|
||||
"bui-splunk-collector-url": "Splunk HTTP Event Collector Url",
|
||||
"bui-splunk-hec-endpoint-placeholder": "https://splunk.example.com:8088",
|
||||
"bui-wku-heading": "Here are the set of URIs you would need access to:",
|
||||
"bui-wku-idp-configuration-links": "Identity Provider Configuration links",
|
||||
"bui-wku-desc-idp-configuration": "Links for SAML/OIDC IdP setup",
|
||||
|
|
|
@ -18,6 +18,7 @@ import type {
|
|||
Storable,
|
||||
SAMLSSORecord,
|
||||
OIDCSSORecord,
|
||||
FederatedSAMLProfile,
|
||||
SSOTracerInstance,
|
||||
OAuthErrorHandlerParams,
|
||||
OIDCAuthzResponsePayload,
|
||||
|
@ -556,9 +557,12 @@ export class OAuthController implements IOAuthController {
|
|||
}
|
||||
}
|
||||
|
||||
public async samlResponse(
|
||||
body: SAMLResponsePayload
|
||||
): Promise<{ redirect_url?: string; app_select_form?: string; response_form?: string }> {
|
||||
public async samlResponse(body: SAMLResponsePayload): Promise<{
|
||||
redirect_url?: string;
|
||||
app_select_form?: string;
|
||||
response_form?: string;
|
||||
profile?: FederatedSAMLProfile;
|
||||
}> {
|
||||
let connection: SAMLSSORecord | undefined;
|
||||
let rawResponse: string | undefined;
|
||||
let sessionId: string | undefined;
|
||||
|
@ -716,11 +720,18 @@ export class OAuthController implements IOAuthController {
|
|||
|
||||
// This is a federated SAML flow, let's create a new SAMLResponse and POST it to the SP
|
||||
if (isSAMLFederated) {
|
||||
const userProfile = {
|
||||
email: profile.claims.email,
|
||||
firstName: profile.claims.firstName,
|
||||
lastName: profile.claims.lastName,
|
||||
requested: session.requested,
|
||||
};
|
||||
|
||||
const { responseForm } = await this.ssoHandler.createSAMLResponse({ profile, session });
|
||||
|
||||
await this.sessionStore.delete(sessionId);
|
||||
|
||||
return { response_form: responseForm };
|
||||
return { response_form: responseForm, profile: userProfile };
|
||||
}
|
||||
|
||||
const code = await this._buildAuthorizationCode(connection, profile, session, isIdPFlow);
|
||||
|
|
|
@ -31,6 +31,9 @@ export enum IndexNames {
|
|||
SetupToken = 'token',
|
||||
ProductService = 'productService',
|
||||
TenantProductService = 'tenantProductService',
|
||||
|
||||
// For Security Logs Config
|
||||
Tenant = 'tenant',
|
||||
}
|
||||
|
||||
// The namespace prefix for the database store
|
||||
|
|
|
@ -43,3 +43,10 @@ export type AppRequestParams =
|
|||
product: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export type FederatedSAMLProfile = {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
requested: Record<string, string>;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import { IndexNames } from '../../controller/utils';
|
||||
import type { Storable, JacksonOption } from '../../typings';
|
||||
import { throwIfInvalidLicense } from '../common/checkLicense';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { SecurityLogsConfig, SecurityLogsConfigCreate } from './types';
|
||||
|
||||
export class SecurityLogsConfigController {
|
||||
private store: Storable;
|
||||
private opts: JacksonOption;
|
||||
|
||||
constructor({ store, opts }: { store: Storable; opts: JacksonOption }) {
|
||||
this.store = store;
|
||||
this.opts = opts;
|
||||
}
|
||||
|
||||
public async createSecurityLogsConfig(params: SecurityLogsConfigCreate): Promise<string> {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
||||
const id = randomUUID();
|
||||
const record = {
|
||||
id,
|
||||
name: params.name,
|
||||
tenant: params.tenant,
|
||||
type: params.type,
|
||||
config: params.config,
|
||||
};
|
||||
|
||||
await this.store.put(id, record, {
|
||||
name: IndexNames.Tenant,
|
||||
value: params.tenant,
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public async getAll(tenant: string, pageOffset?: number, pageLimit?: number, pageToken?: string) {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
||||
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> {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
||||
return await this.store.get(id);
|
||||
}
|
||||
|
||||
public async update(id: string, config: any, name?: string): Promise<SecurityLogsConfig> {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
||||
const currentConfig = await this.get(id);
|
||||
|
||||
if (!currentConfig) {
|
||||
throw new Error(`Security logs config with id ${id} not found`);
|
||||
}
|
||||
|
||||
const newConfig = {
|
||||
type: currentConfig.type,
|
||||
tenant: currentConfig.tenant,
|
||||
config: config ?? currentConfig.config,
|
||||
name: name ?? currentConfig.name,
|
||||
};
|
||||
|
||||
const updatedConfig = {
|
||||
...currentConfig,
|
||||
...newConfig,
|
||||
};
|
||||
|
||||
await this.store.put(id, updatedConfig, {
|
||||
name: IndexNames.Tenant,
|
||||
value: updatedConfig.tenant,
|
||||
});
|
||||
|
||||
return updatedConfig;
|
||||
}
|
||||
|
||||
public async delete(id: string): Promise<void> {
|
||||
await throwIfInvalidLicense(this.opts.boxyhqLicenseKey);
|
||||
|
||||
await this.store.delete(id);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
export type SecurityLogsConfigCreate = {
|
||||
tenant: string;
|
||||
name?: string;
|
||||
config: any;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type SecurityLogsConfig = {
|
||||
id: string;
|
||||
name?: string;
|
||||
tenant: string;
|
||||
config: any;
|
||||
type: string;
|
||||
};
|
|
@ -19,6 +19,7 @@ import { BrandingController } from './ee/branding';
|
|||
import SSOTracer from './sso-tracer';
|
||||
import EventController from './event';
|
||||
import { ProductController } from './ee/product';
|
||||
import { SecurityLogsConfigController } from './ee/security-logs';
|
||||
import { OryController } from './ee/ory/ory';
|
||||
|
||||
const tracerTTL = 7 * 24 * 60 * 60;
|
||||
|
@ -73,6 +74,7 @@ export const controllers = async (
|
|||
spConfig: SPSSOConfig;
|
||||
samlFederatedController: ISAMLFederationController;
|
||||
brandingController: IBrandingController;
|
||||
securityLogsConfigController: ISecurityLogsConfigController;
|
||||
checkLicense: () => Promise<boolean>;
|
||||
productController: ProductController;
|
||||
close: () => Promise<void>;
|
||||
|
@ -89,6 +91,7 @@ export const controllers = async (
|
|||
const setupLinkStore = db.store('setup:link');
|
||||
const certificateStore = db.store('x509:certificates');
|
||||
const settingsStore = db.store('portal:settings');
|
||||
const securityLogsConfigStore = db.store('security:logs:config');
|
||||
const productStore = db.store('product:config');
|
||||
const tracerStore = db.store('saml:tracer', tracerTTL);
|
||||
|
||||
|
@ -114,6 +117,7 @@ export const controllers = async (
|
|||
// Enterprise Features
|
||||
const samlFederatedController = await initFederatedSAML({ db, opts, ssoTracer });
|
||||
const brandingController = new BrandingController({ store: settingsStore, opts });
|
||||
const securityLogsConfig = new SecurityLogsConfigController({ store: securityLogsConfigStore, opts });
|
||||
|
||||
const oauthController = new OAuthController({
|
||||
connectionStore,
|
||||
|
@ -185,6 +189,7 @@ export const controllers = async (
|
|||
return checkLicense(opts.boxyhqLicenseKey);
|
||||
},
|
||||
productController,
|
||||
securityLogsConfigController: securityLogsConfig,
|
||||
close: async () => {
|
||||
await db.close();
|
||||
},
|
||||
|
@ -195,6 +200,8 @@ export default controllers;
|
|||
|
||||
export * from './typings';
|
||||
export * from './ee/federated-saml/types';
|
||||
export * from './ee/security-logs/types';
|
||||
export type SAMLJackson = Awaited<ReturnType<typeof controllers>>;
|
||||
export type ISetupLinkController = InstanceType<typeof SetupLinkController>;
|
||||
export type IBrandingController = InstanceType<typeof BrandingController>;
|
||||
export type ISecurityLogsConfigController = InstanceType<typeof SecurityLogsConfigController>;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "jackson",
|
||||
"version": "1.23.5",
|
||||
"version": "1.23.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jackson",
|
||||
"version": "1.23.5",
|
||||
"version": "1.23.6",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
@ -14,6 +14,7 @@
|
|||
"@boxyhq/metrics": "0.2.6",
|
||||
"@boxyhq/react-ui": "3.3.43",
|
||||
"@boxyhq/saml-jackson": "file:npm",
|
||||
"@boxyhq/security-logs-sink": "file:./ee/security-sinks",
|
||||
"@heroicons/react": "2.1.3",
|
||||
"@retracedhq/logs-viewer": "2.7.3",
|
||||
"@retracedhq/retraced": "0.7.9",
|
||||
|
@ -42,6 +43,7 @@
|
|||
"react-syntax-highlighter": "15.5.0",
|
||||
"react-tagsinput": "3.20.3",
|
||||
"remark-gfm": "3.0.1",
|
||||
"request-ip": "3.3.0",
|
||||
"sharp": "0.33.3",
|
||||
"swr": "2.2.5"
|
||||
},
|
||||
|
@ -75,6 +77,18 @@
|
|||
"npm": ">=10"
|
||||
}
|
||||
},
|
||||
"ee/security-sinks": {
|
||||
"name": "@boxyhq/security-sinks",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "1.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=8"
|
||||
}
|
||||
},
|
||||
"internal-ui": {
|
||||
"name": "@boxyhq/internal-ui",
|
||||
"version": "0.0.0",
|
||||
|
@ -1701,6 +1715,10 @@
|
|||
"xmlbuilder": "15.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@boxyhq/security-logs-sink": {
|
||||
"resolved": "ee/security-sinks",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||
|
@ -6004,17 +6022,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
|
||||
},
|
||||
"node_modules/@types/whatwg-url": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
|
||||
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz",
|
||||
|
@ -6985,16 +6992,6 @@
|
|||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz",
|
||||
"integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.20.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
|
@ -15930,86 +15927,6 @@
|
|||
"obliterator": "^1.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz",
|
||||
"integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bson": "^5.5.0",
|
||||
"mongodb-connection-string-url": "^2.6.0",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.20.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.188.0",
|
||||
"@mongodb-js/zstd": "^1.0.0",
|
||||
"kerberos": "^1.0.0 || ^2.0.0",
|
||||
"mongodb-client-encryption": ">=2.3.0 <3",
|
||||
"snappy": "^7.2.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-providers": {
|
||||
"optional": true
|
||||
},
|
||||
"@mongodb-js/zstd": {
|
||||
"optional": true
|
||||
},
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb-client-encryption": {
|
||||
"optional": true
|
||||
},
|
||||
"snappy": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz",
|
||||
"integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/whatwg-url": "^8.2.1",
|
||||
"whatwg-url": "^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
|
||||
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
|
||||
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tr46": "^3.0.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
|
@ -19724,6 +19641,11 @@
|
|||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/request-ip": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz",
|
||||
"integrity": "sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA=="
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "jackson",
|
||||
"version": "1.23.5",
|
||||
"version": "1.23.6",
|
||||
"private": true,
|
||||
"description": "SAML 2.0 service",
|
||||
"keywords": [
|
||||
|
@ -42,9 +42,10 @@
|
|||
"start": "cross-env PORT=5225 NODE_OPTIONS=--dns-result-order=ipv4first node .next/standalone/server.js",
|
||||
"swagger-jsdoc": "swagger-jsdoc -d swagger/swagger-definition.js npm/src/**/*.ts npm/src/**/**/*.ts -o swagger/swagger.json",
|
||||
"redis": "cross-env JACKSON_API_KEYS=secret DB_ENGINE=redis DB_TYPE=redis DB_URL=redis://localhost:6379/redis npm run dev",
|
||||
"prepare": "npm run prepare:npm && npm run prepare:internal-ui",
|
||||
"prepare": "npm run prepare:npm && npm run prepare:internal-ui && npm run prepare:security-sinks",
|
||||
"prepare:npm": "cd npm && npm install --legacy-peer-deps",
|
||||
"prepare:internal-ui": "cd internal-ui && npm install --legacy-peer-deps",
|
||||
"prepare:security-sinks": "cd ee/security-sinks && npm install",
|
||||
"pretest:e2e": "env-cmd -f .env.test.local ts-node --logError e2e/support/pretest.ts",
|
||||
"test:e2e": "env-cmd -f .env.test.local playwright test",
|
||||
"test": "cd npm && npm run test",
|
||||
|
@ -65,6 +66,7 @@
|
|||
"@boxyhq/metrics": "0.2.6",
|
||||
"@boxyhq/react-ui": "3.3.43",
|
||||
"@boxyhq/saml-jackson": "file:npm",
|
||||
"@boxyhq/security-logs-sink": "file:./ee/security-sinks",
|
||||
"@heroicons/react": "2.1.3",
|
||||
"@retracedhq/logs-viewer": "2.7.3",
|
||||
"@retracedhq/retraced": "0.7.9",
|
||||
|
@ -93,6 +95,7 @@
|
|||
"react-syntax-highlighter": "15.5.0",
|
||||
"react-tagsinput": "3.20.3",
|
||||
"remark-gfm": "3.0.1",
|
||||
"request-ip": "3.3.0",
|
||||
"sharp": "0.33.3",
|
||||
"swr": "2.2.5"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
import jackson from '@lib/jackson';
|
||||
|
||||
export { default } from 'ee/security-logs-config/pages/edit';
|
||||
|
||||
export async function getServerSideProps({ locale }) {
|
||||
const { checkLicense } = await jackson();
|
||||
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common'])),
|
||||
hasValidLicense: await checkLicense(),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
import jackson from '@lib/jackson';
|
||||
|
||||
export { default } from 'ee/security-logs-config/pages/index';
|
||||
|
||||
export async function getServerSideProps({ locale }) {
|
||||
const { checkLicense } = await jackson();
|
||||
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common'])),
|
||||
hasValidLicense: await checkLicense(),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
import jackson from '@lib/jackson';
|
||||
|
||||
export { default } from 'ee/security-logs-config/pages/new';
|
||||
|
||||
export async function getServerSideProps({ locale }) {
|
||||
const { checkLicense } = await jackson();
|
||||
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common'])),
|
||||
hasValidLicense: await checkLicense(),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import jackson from '@lib/jackson';
|
||||
import { oidcMetadataParse, parsePaginateApiParams, strategyChecker } from '@lib/utils';
|
||||
import { adminPortalSSODefaults } from '@lib/env';
|
||||
import retraced from '@ee/retraced';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
import { ApiError } from '@lib/error';
|
||||
import { validateDevelopmentModeLimits } from '@lib/development-mode';
|
||||
|
@ -65,12 +66,33 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
// Create SAML connection
|
||||
if (isSAML) {
|
||||
const connection = await connectionAPIController.createSAMLConnection(req.body);
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'sso.connection.create',
|
||||
crud: 'c',
|
||||
req,
|
||||
target: {
|
||||
id: connection.clientID,
|
||||
type: 'SAML Connection',
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({ data: connection });
|
||||
}
|
||||
|
||||
// Create OIDC connection
|
||||
else {
|
||||
const connection = await connectionAPIController.createOIDCConnection(oidcMetadataParse(req.body));
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'sso.connection.create',
|
||||
crud: 'c',
|
||||
req,
|
||||
target: {
|
||||
id: connection.clientID,
|
||||
type: 'OIDC Connection',
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({ data: connection });
|
||||
}
|
||||
};
|
||||
|
@ -88,12 +110,33 @@ const handlePATCH = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
// Update SAML connection
|
||||
if (isSAML) {
|
||||
await connectionAPIController.updateSAMLConnection(req.body);
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'sso.connection.update',
|
||||
crud: 'u',
|
||||
req,
|
||||
target: {
|
||||
id: req.body.clientID,
|
||||
type: 'SAML Connection',
|
||||
},
|
||||
});
|
||||
|
||||
res.status(204).end();
|
||||
}
|
||||
|
||||
// Update OIDC connection
|
||||
else {
|
||||
await connectionAPIController.updateOIDCConnection(oidcMetadataParse(req.body));
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'sso.connection.update',
|
||||
crud: 'u',
|
||||
req,
|
||||
target: {
|
||||
id: req.body.clientID,
|
||||
type: 'OIDC Connection',
|
||||
},
|
||||
});
|
||||
|
||||
res.status(204).end();
|
||||
}
|
||||
};
|
||||
|
@ -109,6 +152,15 @@ const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
await connectionAPIController.deleteConnections({ clientID, clientSecret });
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'sso.connection.delete',
|
||||
crud: 'd',
|
||||
req,
|
||||
target: {
|
||||
id: clientID,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ data: null });
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson from '@lib/jackson';
|
||||
import retraced from '@ee/retraced';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
import { ApiError } from '@lib/error';
|
||||
import { parsePaginateApiParams } from '@lib/utils';
|
||||
|
@ -57,6 +58,15 @@ const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
.setTenantAndProduct(directory.tenant, directory.product)
|
||||
.deleteAll(directoryId);
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'dsync.webhook_event.delete',
|
||||
crud: 'd',
|
||||
req,
|
||||
target: {
|
||||
id: directoryId,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ data: null });
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson from '@lib/jackson';
|
||||
import retraced from '@ee/retraced';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
import { ApiError } from '@lib/error';
|
||||
|
||||
|
@ -19,6 +20,17 @@ const handlePATCH = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
const { data, error } = await directorySyncController.directories.update(directoryId, req.body);
|
||||
|
||||
if (data) {
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'dsync.connection.update',
|
||||
crud: 'u',
|
||||
req,
|
||||
target: {
|
||||
id: directoryId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw new ApiError(error.message, error.code);
|
||||
}
|
||||
|
@ -53,6 +65,15 @@ const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
throw new ApiError(error.message, error.code);
|
||||
}
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'dsync.connection.delete',
|
||||
crud: 'd',
|
||||
req,
|
||||
target: {
|
||||
id: directoryId,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ data: null });
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type { DirectoryType } from '@boxyhq/saml-jackson';
|
||||
import jackson from '@lib/jackson';
|
||||
import retraced from '@ee/retraced';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
import { ApiError } from '@lib/error';
|
||||
import { parsePaginateApiParams } from '@lib/utils';
|
||||
|
@ -30,6 +31,16 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
google_domain,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'dsync.connection.create',
|
||||
crud: 'c',
|
||||
req,
|
||||
target: {
|
||||
id: data.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (error) {
|
||||
throw new ApiError(error.message, error.code);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import axios from 'axios';
|
|||
import type { Project } from 'types/retraced';
|
||||
import { getToken } from '@lib/retraced';
|
||||
import { retracedOptions } from '@lib/env';
|
||||
import retraced from '@ee/retraced';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
@ -30,6 +31,15 @@ const createProject = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
}
|
||||
);
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'retraced.project.create',
|
||||
crud: 'c',
|
||||
req,
|
||||
target: {
|
||||
id: data.project.id,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
data,
|
||||
});
|
||||
|
|
|
@ -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';
|
|
@ -1,5 +1,7 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson from '@lib/jackson';
|
||||
import retraced from '@ee/retraced';
|
||||
import type { SetupLinkService } from '@boxyhq/saml-jackson';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
import { ApiError } from '@lib/error';
|
||||
import { parsePaginateApiParams } from '@lib/utils';
|
||||
|
@ -18,6 +20,17 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
const setupLink = await setupLinkController.create(req.body);
|
||||
|
||||
const { service } = req.body as { service: SetupLinkService };
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: `${service}.setuplink.create`,
|
||||
crud: 'c',
|
||||
req,
|
||||
target: {
|
||||
id: setupLink.setupID,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({ data: setupLink });
|
||||
};
|
||||
|
||||
|
@ -26,8 +39,18 @@ const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
const { id } = req.query as { id: string };
|
||||
|
||||
const setupLink = await setupLinkController.get(id);
|
||||
await setupLinkController.remove({ id });
|
||||
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: `${setupLink.service}.setuplink.delete`,
|
||||
crud: 'd',
|
||||
req,
|
||||
target: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ data: {} });
|
||||
};
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import jackson from '@lib/jackson';
|
|||
import { validateEmailWithACL } from '@lib/utils';
|
||||
import { jacksonOptions as env } from '@lib/env';
|
||||
import { sessionName } from '@lib/constants';
|
||||
import retraced from '@ee/retraced';
|
||||
|
||||
export default NextAuth({
|
||||
theme: {
|
||||
|
@ -168,4 +169,16 @@ export default NextAuth({
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
adapter: Adapter(),
|
||||
events: {
|
||||
async signIn({ user }): Promise<void> {
|
||||
retraced.reportAdminPortalEvent({
|
||||
action: 'portal.user.login',
|
||||
crud: 'c',
|
||||
actor: {
|
||||
id: `${user.email}`,
|
||||
name: `${user.name}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1 +1 @@
|
|||
export { default } from 'ee/product/api/[productId]';
|
||||
export { default } from '@ee/product/api/[productId]';
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
|
||||
import jackson from '@lib/jackson';
|
||||
import { setErrorCookie } from '@lib/utils';
|
||||
import retraced from '@ee/retraced';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { method } = req;
|
||||
|
@ -20,7 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
};
|
||||
|
||||
// Handle SAML Response generated by IdP
|
||||
const { redirect_url, app_select_form, response_form } = await oauthController.samlResponse({
|
||||
const { redirect_url, app_select_form, response_form, profile } = await oauthController.samlResponse({
|
||||
SAMLResponse,
|
||||
RelayState,
|
||||
idp_hint,
|
||||
|
@ -36,6 +37,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
|
||||
if (response_form) {
|
||||
if (profile) {
|
||||
retraced.reportEvent({
|
||||
action: 'sso.user.login',
|
||||
crud: 'c',
|
||||
actor: {
|
||||
id: profile.email,
|
||||
name: `${profile.firstName} ${profile.lastName}`,
|
||||
fields: {
|
||||
tenant: profile.requested.tenant,
|
||||
},
|
||||
},
|
||||
productId: profile.requested.product,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.send(response_form);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import jackson from '@lib/jackson';
|
||||
import { extractAuthToken } from '@lib/auth';
|
||||
import retraced from '@ee/retraced';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
|
@ -27,6 +28,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
const profile = await oauthController.userInfo(token);
|
||||
|
||||
retraced.reportEvent({
|
||||
action: 'sso.user.login',
|
||||
crud: 'c',
|
||||
actor: {
|
||||
id: profile.email,
|
||||
name: `${profile.firstName} ${profile.lastName}`,
|
||||
fields: {
|
||||
tenant: profile.requested.tenant,
|
||||
},
|
||||
},
|
||||
productId: profile.requested.product,
|
||||
req,
|
||||
});
|
||||
|
||||
res.json(profile);
|
||||
} catch (err: any) {
|
||||
console.error('userinfo error:', err);
|
||||
|
|
|
@ -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