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_HOST_URL=
|
||||||
RETRACED_EXTERNAL_URL=
|
RETRACED_EXTERNAL_URL=
|
||||||
RETRACED_ADMIN_ROOT_TOKEN=
|
RETRACED_ADMIN_ROOT_TOKEN=
|
||||||
|
RETRACED_API_KEY=
|
||||||
|
RETRACED_PROJECT_ID=
|
||||||
|
AUDIT_LOG_TEAMS=
|
||||||
|
|
||||||
# Admin Portal for Terminus (Privacy Vault)
|
# Admin Portal for Terminus (Privacy Vault)
|
||||||
TERMINUS_PROXY_HOST_URL=
|
TERMINUS_PROXY_HOST_URL=
|
||||||
|
|
|
@ -458,3 +458,20 @@ jobs:
|
||||||
working-directory: ./${{ matrix.package }}
|
working-directory: ./${{ matrix.package }}
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
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 package.json package-lock.json ./
|
||||||
COPY npm npm
|
COPY npm npm
|
||||||
COPY internal-ui internal-ui
|
COPY internal-ui internal-ui
|
||||||
|
COPY ee/security-sinks ee/security-sinks
|
||||||
COPY migrate.sh prebuild.ts ./
|
COPY migrate.sh prebuild.ts ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
RUN npm rebuild --arch=x64 --platform=linux --libc=musl sharp
|
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/npm ./npm
|
||||||
COPY --from=deps /app/internal-ui ./internal-ui
|
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 --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,12 @@ const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const regExp = /\bt\('(.*?)'/gm;
|
const regExp = /\bt\('(.*?)'/gm;
|
||||||
const altRegExp = /\bi18nKey='(.*?)'/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 = {};
|
const allStrings = {};
|
||||||
|
|
||||||
|
@ -43,7 +49,7 @@ files.forEach((file) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(localeFile).forEach((key) => {
|
Object.keys(localeFile).forEach((key) => {
|
||||||
if (!allStrings[key]) {
|
if (!allStrings[key] && !exceptionList.includes(key)) {
|
||||||
error = true;
|
error = true;
|
||||||
console.error(`Unused key: ${key}`);
|
console.error(`Unused key: ${key}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,11 @@ export const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => {
|
||||||
text: 'Branding',
|
text: 'Branding',
|
||||||
active: asPath.includes('/admin/settings/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 type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
import jackson from '@lib/jackson';
|
import jackson from '@lib/jackson';
|
||||||
|
import retraced from '@ee/retraced';
|
||||||
import { defaultHandler } from '@lib/api';
|
import { defaultHandler } from '@lib/api';
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
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;
|
const { logoUrl, faviconUrl, companyName, primaryColor } = req.body;
|
||||||
|
|
||||||
|
retraced.reportAdminPortalEvent({
|
||||||
|
action: 'portal.branding.update',
|
||||||
|
crud: 'u',
|
||||||
|
req,
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
data: await brandingController.update({ logoUrl, faviconUrl, companyName, primaryColor }),
|
data: await brandingController.update({ logoUrl, faviconUrl, companyName, primaryColor }),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
import jackson from '@lib/jackson';
|
import jackson from '@lib/jackson';
|
||||||
|
import retraced from '@ee/retraced';
|
||||||
import { defaultHandler } from '@lib/api';
|
import { defaultHandler } from '@lib/api';
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
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);
|
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 });
|
res.json({ data: updatedApp });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,6 +55,15 @@ const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
|
||||||
await samlFederatedController.app.delete({ id });
|
await samlFederatedController.app.delete({ id });
|
||||||
|
|
||||||
|
retraced.reportAdminPortalEvent({
|
||||||
|
action: 'federation.app.delete',
|
||||||
|
crud: 'd',
|
||||||
|
req,
|
||||||
|
target: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.json({ data: null });
|
res.json({ data: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
import jackson from '@lib/jackson';
|
import jackson from '@lib/jackson';
|
||||||
|
import retraced from '@ee/retraced';
|
||||||
import { defaultHandler } from '@lib/api';
|
import { defaultHandler } from '@lib/api';
|
||||||
import { parsePaginateApiParams } from '@lib/utils';
|
import { parsePaginateApiParams } from '@lib/utils';
|
||||||
import { validateDevelopmentModeLimits } from '@lib/development-mode';
|
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);
|
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 });
|
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
|
# Build the package
|
||||||
rm -rf dist
|
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
|
npm run build
|
||||||
|
|
||||||
# Publish
|
# Publish
|
||||||
|
@ -20,6 +24,9 @@ npm publish --registry http://localhost:4873/
|
||||||
# npm i --save-exact --registry http://localhost:4873/ @boxyhq/internal-ui@$VERSION
|
# npm i --save-exact --registry http://localhost:4873/ @boxyhq/internal-ui@$VERSION
|
||||||
# rm -rf .next
|
# 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`
|
# Install the published version in `boxyhq/saas-app`
|
||||||
cd ../../saas-app
|
cd ../../saas-app
|
||||||
npm uninstall @boxyhq/internal-ui
|
npm uninstall @boxyhq/internal-ui
|
||||||
|
|
|
@ -5,3 +5,4 @@ export * from './dsync';
|
||||||
export * from './provider';
|
export * from './provider';
|
||||||
export * from './sso-tracer';
|
export * from './sso-tracer';
|
||||||
export * from './setup-link';
|
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;
|
status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ApiResponse<T = any> = ApiSuccess<T> | { error: ApiError };
|
||||||
|
|
||||||
enum DirectorySyncProviders {
|
enum DirectorySyncProviders {
|
||||||
'azure-scim-v2' = 'Azure SCIM v2.0',
|
'azure-scim-v2' = 'Azure SCIM v2.0',
|
||||||
'onelogin-scim-v2' = 'OneLogin SCIM v2.0',
|
'onelogin-scim-v2' = 'OneLogin SCIM v2.0',
|
||||||
|
|
|
@ -23,6 +23,8 @@ const retraced = {
|
||||||
hostUrl: process.env.RETRACED_HOST_URL,
|
hostUrl: process.env.RETRACED_HOST_URL,
|
||||||
externalUrl: process.env.RETRACED_EXTERNAL_URL || process.env.RETRACED_HOST_URL,
|
externalUrl: process.env.RETRACED_EXTERNAL_URL || process.env.RETRACED_HOST_URL,
|
||||||
adminToken: process.env.RETRACED_ADMIN_ROOT_TOKEN,
|
adminToken: process.env.RETRACED_ADMIN_ROOT_TOKEN,
|
||||||
|
apiKey: process.env.RETRACED_API_KEY,
|
||||||
|
projectId: process.env.RETRACED_PROJECT_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Terminus
|
// Terminus
|
||||||
|
@ -129,3 +131,5 @@ export { retraced as retracedOptions };
|
||||||
export { terminus as terminusOptions };
|
export { terminus as terminusOptions };
|
||||||
export { apiKeys };
|
export { apiKeys };
|
||||||
export { jacksonOptions };
|
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-regenerated": "The setup link regenerated.",
|
||||||
"setup-link-copied": "The setup link copied to the clipboard.",
|
"setup-link-copied": "The setup link copied to the clipboard.",
|
||||||
"setup-link-deleted": "The setup link deleted.",
|
"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-name": "Name",
|
||||||
|
"bui-shared-select-type": "Select a type",
|
||||||
|
"bui-shared-type": "Type",
|
||||||
"bui-shared-tenant": "Tenant",
|
"bui-shared-tenant": "Tenant",
|
||||||
"bui-shared-product": "Product",
|
"bui-shared-product": "Product",
|
||||||
"bui-shared-actions": "Actions",
|
"bui-shared-actions": "Actions",
|
||||||
"bui-shared-type": "Type",
|
|
||||||
"bui-shared-edit": "Edit",
|
"bui-shared-edit": "Edit",
|
||||||
"bui-shared-save-changes": "Save Changes",
|
"bui-shared-save-changes": "Save Changes",
|
||||||
"bui-shared-no-more-results": "No more results found",
|
"bui-shared-no-more-results": "No more results found",
|
||||||
|
@ -138,6 +141,7 @@
|
||||||
"bui-shared-close": "Close",
|
"bui-shared-close": "Close",
|
||||||
"bui-shared-copy": "Copy",
|
"bui-shared-copy": "Copy",
|
||||||
"bui-shared-active": "Active",
|
"bui-shared-active": "Active",
|
||||||
|
"bui-shared-endpoint": "Endpoint",
|
||||||
"bui-shared-email": "Email",
|
"bui-shared-email": "Email",
|
||||||
"bui-shared-logo-url": "Logo URL",
|
"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.",
|
"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-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": "Primary Color",
|
||||||
"bui-shared-primary-color-desc": "Primary color will be applied to buttons, links, and other elements.",
|
"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-heading": "Here are the set of URIs you would need access to:",
|
||||||
"bui-wku-idp-configuration-links": "Identity Provider Configuration links",
|
"bui-wku-idp-configuration-links": "Identity Provider Configuration links",
|
||||||
"bui-wku-desc-idp-configuration": "Links for SAML/OIDC IdP setup",
|
"bui-wku-desc-idp-configuration": "Links for SAML/OIDC IdP setup",
|
||||||
|
|
|
@ -18,6 +18,7 @@ import type {
|
||||||
Storable,
|
Storable,
|
||||||
SAMLSSORecord,
|
SAMLSSORecord,
|
||||||
OIDCSSORecord,
|
OIDCSSORecord,
|
||||||
|
FederatedSAMLProfile,
|
||||||
SSOTracerInstance,
|
SSOTracerInstance,
|
||||||
OAuthErrorHandlerParams,
|
OAuthErrorHandlerParams,
|
||||||
OIDCAuthzResponsePayload,
|
OIDCAuthzResponsePayload,
|
||||||
|
@ -556,9 +557,12 @@ export class OAuthController implements IOAuthController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async samlResponse(
|
public async samlResponse(body: SAMLResponsePayload): Promise<{
|
||||||
body: SAMLResponsePayload
|
redirect_url?: string;
|
||||||
): Promise<{ redirect_url?: string; app_select_form?: string; response_form?: string }> {
|
app_select_form?: string;
|
||||||
|
response_form?: string;
|
||||||
|
profile?: FederatedSAMLProfile;
|
||||||
|
}> {
|
||||||
let connection: SAMLSSORecord | undefined;
|
let connection: SAMLSSORecord | undefined;
|
||||||
let rawResponse: string | undefined;
|
let rawResponse: string | undefined;
|
||||||
let sessionId: 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
|
// This is a federated SAML flow, let's create a new SAMLResponse and POST it to the SP
|
||||||
if (isSAMLFederated) {
|
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 });
|
const { responseForm } = await this.ssoHandler.createSAMLResponse({ profile, session });
|
||||||
|
|
||||||
await this.sessionStore.delete(sessionId);
|
await this.sessionStore.delete(sessionId);
|
||||||
|
|
||||||
return { response_form: responseForm };
|
return { response_form: responseForm, profile: userProfile };
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = await this._buildAuthorizationCode(connection, profile, session, isIdPFlow);
|
const code = await this._buildAuthorizationCode(connection, profile, session, isIdPFlow);
|
||||||
|
|
|
@ -31,6 +31,9 @@ export enum IndexNames {
|
||||||
SetupToken = 'token',
|
SetupToken = 'token',
|
||||||
ProductService = 'productService',
|
ProductService = 'productService',
|
||||||
TenantProductService = 'tenantProductService',
|
TenantProductService = 'tenantProductService',
|
||||||
|
|
||||||
|
// For Security Logs Config
|
||||||
|
Tenant = 'tenant',
|
||||||
}
|
}
|
||||||
|
|
||||||
// The namespace prefix for the database store
|
// The namespace prefix for the database store
|
||||||
|
|
|
@ -43,3 +43,10 @@ export type AppRequestParams =
|
||||||
product: string;
|
product: string;
|
||||||
type?: 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 SSOTracer from './sso-tracer';
|
||||||
import EventController from './event';
|
import EventController from './event';
|
||||||
import { ProductController } from './ee/product';
|
import { ProductController } from './ee/product';
|
||||||
|
import { SecurityLogsConfigController } from './ee/security-logs';
|
||||||
import { OryController } from './ee/ory/ory';
|
import { OryController } from './ee/ory/ory';
|
||||||
|
|
||||||
const tracerTTL = 7 * 24 * 60 * 60;
|
const tracerTTL = 7 * 24 * 60 * 60;
|
||||||
|
@ -73,6 +74,7 @@ export const controllers = async (
|
||||||
spConfig: SPSSOConfig;
|
spConfig: SPSSOConfig;
|
||||||
samlFederatedController: ISAMLFederationController;
|
samlFederatedController: ISAMLFederationController;
|
||||||
brandingController: IBrandingController;
|
brandingController: IBrandingController;
|
||||||
|
securityLogsConfigController: ISecurityLogsConfigController;
|
||||||
checkLicense: () => Promise<boolean>;
|
checkLicense: () => Promise<boolean>;
|
||||||
productController: ProductController;
|
productController: ProductController;
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
|
@ -89,6 +91,7 @@ export const controllers = async (
|
||||||
const setupLinkStore = db.store('setup:link');
|
const setupLinkStore = db.store('setup:link');
|
||||||
const certificateStore = db.store('x509:certificates');
|
const certificateStore = db.store('x509:certificates');
|
||||||
const settingsStore = db.store('portal:settings');
|
const settingsStore = db.store('portal:settings');
|
||||||
|
const securityLogsConfigStore = db.store('security:logs:config');
|
||||||
const productStore = db.store('product:config');
|
const productStore = db.store('product:config');
|
||||||
const tracerStore = db.store('saml:tracer', tracerTTL);
|
const tracerStore = db.store('saml:tracer', tracerTTL);
|
||||||
|
|
||||||
|
@ -114,6 +117,7 @@ export const controllers = async (
|
||||||
// Enterprise Features
|
// Enterprise Features
|
||||||
const samlFederatedController = await initFederatedSAML({ db, opts, ssoTracer });
|
const samlFederatedController = await initFederatedSAML({ db, opts, ssoTracer });
|
||||||
const brandingController = new BrandingController({ store: settingsStore, opts });
|
const brandingController = new BrandingController({ store: settingsStore, opts });
|
||||||
|
const securityLogsConfig = new SecurityLogsConfigController({ store: securityLogsConfigStore, opts });
|
||||||
|
|
||||||
const oauthController = new OAuthController({
|
const oauthController = new OAuthController({
|
||||||
connectionStore,
|
connectionStore,
|
||||||
|
@ -185,6 +189,7 @@ export const controllers = async (
|
||||||
return checkLicense(opts.boxyhqLicenseKey);
|
return checkLicense(opts.boxyhqLicenseKey);
|
||||||
},
|
},
|
||||||
productController,
|
productController,
|
||||||
|
securityLogsConfigController: securityLogsConfig,
|
||||||
close: async () => {
|
close: async () => {
|
||||||
await db.close();
|
await db.close();
|
||||||
},
|
},
|
||||||
|
@ -195,6 +200,8 @@ export default controllers;
|
||||||
|
|
||||||
export * from './typings';
|
export * from './typings';
|
||||||
export * from './ee/federated-saml/types';
|
export * from './ee/federated-saml/types';
|
||||||
|
export * from './ee/security-logs/types';
|
||||||
export type SAMLJackson = Awaited<ReturnType<typeof controllers>>;
|
export type SAMLJackson = Awaited<ReturnType<typeof controllers>>;
|
||||||
export type ISetupLinkController = InstanceType<typeof SetupLinkController>;
|
export type ISetupLinkController = InstanceType<typeof SetupLinkController>;
|
||||||
export type IBrandingController = InstanceType<typeof BrandingController>;
|
export type IBrandingController = InstanceType<typeof BrandingController>;
|
||||||
|
export type ISecurityLogsConfigController = InstanceType<typeof SecurityLogsConfigController>;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "jackson",
|
"name": "jackson",
|
||||||
"version": "1.23.5",
|
"version": "1.23.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jackson",
|
"name": "jackson",
|
||||||
"version": "1.23.5",
|
"version": "1.23.6",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -14,6 +14,7 @@
|
||||||
"@boxyhq/metrics": "0.2.6",
|
"@boxyhq/metrics": "0.2.6",
|
||||||
"@boxyhq/react-ui": "3.3.43",
|
"@boxyhq/react-ui": "3.3.43",
|
||||||
"@boxyhq/saml-jackson": "file:npm",
|
"@boxyhq/saml-jackson": "file:npm",
|
||||||
|
"@boxyhq/security-logs-sink": "file:./ee/security-sinks",
|
||||||
"@heroicons/react": "2.1.3",
|
"@heroicons/react": "2.1.3",
|
||||||
"@retracedhq/logs-viewer": "2.7.3",
|
"@retracedhq/logs-viewer": "2.7.3",
|
||||||
"@retracedhq/retraced": "0.7.9",
|
"@retracedhq/retraced": "0.7.9",
|
||||||
|
@ -42,6 +43,7 @@
|
||||||
"react-syntax-highlighter": "15.5.0",
|
"react-syntax-highlighter": "15.5.0",
|
||||||
"react-tagsinput": "3.20.3",
|
"react-tagsinput": "3.20.3",
|
||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
|
"request-ip": "3.3.0",
|
||||||
"sharp": "0.33.3",
|
"sharp": "0.33.3",
|
||||||
"swr": "2.2.5"
|
"swr": "2.2.5"
|
||||||
},
|
},
|
||||||
|
@ -75,6 +77,18 @@
|
||||||
"npm": ">=10"
|
"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": {
|
"internal-ui": {
|
||||||
"name": "@boxyhq/internal-ui",
|
"name": "@boxyhq/internal-ui",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
@ -1701,6 +1715,10 @@
|
||||||
"xmlbuilder": "15.1.1"
|
"xmlbuilder": "15.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@boxyhq/security-logs-sink": {
|
||||||
|
"resolved": "ee/security-sinks",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@colors/colors": {
|
"node_modules/@colors/colors": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
|
"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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "7.8.0",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz",
|
"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": "^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": {
|
"node_modules/buffer": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
@ -15930,86 +15927,6 @@
|
||||||
"obliterator": "^1.6.1"
|
"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": {
|
"node_modules/mri": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||||
|
@ -19724,6 +19641,11 @@
|
||||||
"url": "https://opencollective.com/unified"
|
"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": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "jackson",
|
"name": "jackson",
|
||||||
"version": "1.23.5",
|
"version": "1.23.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "SAML 2.0 service",
|
"description": "SAML 2.0 service",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
@ -42,9 +42,10 @@
|
||||||
"start": "cross-env PORT=5225 NODE_OPTIONS=--dns-result-order=ipv4first node .next/standalone/server.js",
|
"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",
|
"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",
|
"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:npm": "cd npm && npm install --legacy-peer-deps",
|
||||||
"prepare:internal-ui": "cd internal-ui && 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",
|
"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:e2e": "env-cmd -f .env.test.local playwright test",
|
||||||
"test": "cd npm && npm run test",
|
"test": "cd npm && npm run test",
|
||||||
|
@ -65,6 +66,7 @@
|
||||||
"@boxyhq/metrics": "0.2.6",
|
"@boxyhq/metrics": "0.2.6",
|
||||||
"@boxyhq/react-ui": "3.3.43",
|
"@boxyhq/react-ui": "3.3.43",
|
||||||
"@boxyhq/saml-jackson": "file:npm",
|
"@boxyhq/saml-jackson": "file:npm",
|
||||||
|
"@boxyhq/security-logs-sink": "file:./ee/security-sinks",
|
||||||
"@heroicons/react": "2.1.3",
|
"@heroicons/react": "2.1.3",
|
||||||
"@retracedhq/logs-viewer": "2.7.3",
|
"@retracedhq/logs-viewer": "2.7.3",
|
||||||
"@retracedhq/retraced": "0.7.9",
|
"@retracedhq/retraced": "0.7.9",
|
||||||
|
@ -93,6 +95,7 @@
|
||||||
"react-syntax-highlighter": "15.5.0",
|
"react-syntax-highlighter": "15.5.0",
|
||||||
"react-tagsinput": "3.20.3",
|
"react-tagsinput": "3.20.3",
|
||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
|
"request-ip": "3.3.0",
|
||||||
"sharp": "0.33.3",
|
"sharp": "0.33.3",
|
||||||
"swr": "2.2.5"
|
"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 jackson from '@lib/jackson';
|
||||||
import { oidcMetadataParse, parsePaginateApiParams, strategyChecker } from '@lib/utils';
|
import { oidcMetadataParse, parsePaginateApiParams, strategyChecker } from '@lib/utils';
|
||||||
import { adminPortalSSODefaults } from '@lib/env';
|
import { adminPortalSSODefaults } from '@lib/env';
|
||||||
|
import retraced from '@ee/retraced';
|
||||||
import { defaultHandler } from '@lib/api';
|
import { defaultHandler } from '@lib/api';
|
||||||
import { ApiError } from '@lib/error';
|
import { ApiError } from '@lib/error';
|
||||||
import { validateDevelopmentModeLimits } from '@lib/development-mode';
|
import { validateDevelopmentModeLimits } from '@lib/development-mode';
|
||||||
|
@ -65,12 +66,33 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
// Create SAML connection
|
// Create SAML connection
|
||||||
if (isSAML) {
|
if (isSAML) {
|
||||||
const connection = await connectionAPIController.createSAMLConnection(req.body);
|
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 });
|
res.status(201).json({ data: connection });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create OIDC connection
|
// Create OIDC connection
|
||||||
else {
|
else {
|
||||||
const connection = await connectionAPIController.createOIDCConnection(oidcMetadataParse(req.body));
|
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 });
|
res.status(201).json({ data: connection });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -88,12 +110,33 @@ const handlePATCH = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
// Update SAML connection
|
// Update SAML connection
|
||||||
if (isSAML) {
|
if (isSAML) {
|
||||||
await connectionAPIController.updateSAMLConnection(req.body);
|
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();
|
res.status(204).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update OIDC connection
|
// Update OIDC connection
|
||||||
else {
|
else {
|
||||||
await connectionAPIController.updateOIDCConnection(oidcMetadataParse(req.body));
|
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();
|
res.status(204).end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -109,6 +152,15 @@ const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
|
||||||
await connectionAPIController.deleteConnections({ clientID, clientSecret });
|
await connectionAPIController.deleteConnections({ clientID, clientSecret });
|
||||||
|
|
||||||
|
retraced.reportAdminPortalEvent({
|
||||||
|
action: 'sso.connection.delete',
|
||||||
|
crud: 'd',
|
||||||
|
req,
|
||||||
|
target: {
|
||||||
|
id: clientID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.json({ data: null });
|
res.json({ data: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import jackson from '@lib/jackson';
|
import jackson from '@lib/jackson';
|
||||||
|
import retraced from '@ee/retraced';
|
||||||
import { defaultHandler } from '@lib/api';
|
import { defaultHandler } from '@lib/api';
|
||||||
import { ApiError } from '@lib/error';
|
import { ApiError } from '@lib/error';
|
||||||
import { parsePaginateApiParams } from '@lib/utils';
|
import { parsePaginateApiParams } from '@lib/utils';
|
||||||
|
@ -57,6 +58,15 @@ const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
.setTenantAndProduct(directory.tenant, directory.product)
|
.setTenantAndProduct(directory.tenant, directory.product)
|
||||||
.deleteAll(directoryId);
|
.deleteAll(directoryId);
|
||||||
|
|
||||||
|
retraced.reportAdminPortalEvent({
|
||||||
|
action: 'dsync.webhook_event.delete',
|
||||||
|
crud: 'd',
|
||||||
|
req,
|
||||||
|
target: {
|
||||||
|
id: directoryId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.json({ data: null });
|
res.json({ data: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import jackson from '@lib/jackson';
|
import jackson from '@lib/jackson';
|
||||||
|
import retraced from '@ee/retraced';
|
||||||
import { defaultHandler } from '@lib/api';
|
import { defaultHandler } from '@lib/api';
|
||||||
import { ApiError } from '@lib/error';
|
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);
|
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) {
|
if (error) {
|
||||||
throw new ApiError(error.message, error.code);
|
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);
|
throw new ApiError(error.message, error.code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retraced.reportAdminPortalEvent({
|
||||||
|
action: 'dsync.connection.delete',
|
||||||
|
crud: 'd',
|
||||||
|
req,
|
||||||
|
target: {
|
||||||
|
id: directoryId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.json({ data: null });
|
res.json({ data: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import type { DirectoryType } from '@boxyhq/saml-jackson';
|
import type { DirectoryType } from '@boxyhq/saml-jackson';
|
||||||
import jackson from '@lib/jackson';
|
import jackson from '@lib/jackson';
|
||||||
|
import retraced from '@ee/retraced';
|
||||||
import { defaultHandler } from '@lib/api';
|
import { defaultHandler } from '@lib/api';
|
||||||
import { ApiError } from '@lib/error';
|
import { ApiError } from '@lib/error';
|
||||||
import { parsePaginateApiParams } from '@lib/utils';
|
import { parsePaginateApiParams } from '@lib/utils';
|
||||||
|
@ -30,6 +31,16 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
google_domain,
|
google_domain,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
retraced.reportAdminPortalEvent({
|
||||||
|
action: 'dsync.connection.create',
|
||||||
|
crud: 'c',
|
||||||
|
req,
|
||||||
|
target: {
|
||||||
|
id: data.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new ApiError(error.message, error.code);
|
throw new ApiError(error.message, error.code);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import axios from 'axios';
|
||||||
import type { Project } from 'types/retraced';
|
import type { Project } from 'types/retraced';
|
||||||
import { getToken } from '@lib/retraced';
|
import { getToken } from '@lib/retraced';
|
||||||
import { retracedOptions } from '@lib/env';
|
import { retracedOptions } from '@lib/env';
|
||||||
|
import retraced from '@ee/retraced';
|
||||||
import { defaultHandler } from '@lib/api';
|
import { defaultHandler } from '@lib/api';
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
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({
|
res.status(201).json({
|
||||||
data,
|
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 type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import jackson from '@lib/jackson';
|
import jackson from '@lib/jackson';
|
||||||
|
import retraced from '@ee/retraced';
|
||||||
|
import type { SetupLinkService } from '@boxyhq/saml-jackson';
|
||||||
import { defaultHandler } from '@lib/api';
|
import { defaultHandler } from '@lib/api';
|
||||||
import { ApiError } from '@lib/error';
|
import { ApiError } from '@lib/error';
|
||||||
import { parsePaginateApiParams } from '@lib/utils';
|
import { parsePaginateApiParams } from '@lib/utils';
|
||||||
|
@ -18,6 +20,17 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
|
||||||
const setupLink = await setupLinkController.create(req.body);
|
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 });
|
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 { id } = req.query as { id: string };
|
||||||
|
|
||||||
|
const setupLink = await setupLinkController.get(id);
|
||||||
await setupLinkController.remove({ id });
|
await setupLinkController.remove({ id });
|
||||||
|
|
||||||
|
retraced.reportAdminPortalEvent({
|
||||||
|
action: `${setupLink.service}.setuplink.delete`,
|
||||||
|
crud: 'd',
|
||||||
|
req,
|
||||||
|
target: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.json({ data: {} });
|
res.json({ data: {} });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import jackson from '@lib/jackson';
|
||||||
import { validateEmailWithACL } from '@lib/utils';
|
import { validateEmailWithACL } from '@lib/utils';
|
||||||
import { jacksonOptions as env } from '@lib/env';
|
import { jacksonOptions as env } from '@lib/env';
|
||||||
import { sessionName } from '@lib/constants';
|
import { sessionName } from '@lib/constants';
|
||||||
|
import retraced from '@ee/retraced';
|
||||||
|
|
||||||
export default NextAuth({
|
export default NextAuth({
|
||||||
theme: {
|
theme: {
|
||||||
|
@ -168,4 +169,16 @@ export default NextAuth({
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
adapter: Adapter(),
|
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 jackson from '@lib/jackson';
|
||||||
import { setErrorCookie } from '@lib/utils';
|
import { setErrorCookie } from '@lib/utils';
|
||||||
|
import retraced from '@ee/retraced';
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { method } = req;
|
const { method } = req;
|
||||||
|
@ -20,7 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle SAML Response generated by IdP
|
// 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,
|
SAMLResponse,
|
||||||
RelayState,
|
RelayState,
|
||||||
idp_hint,
|
idp_hint,
|
||||||
|
@ -36,6 +37,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response_form) {
|
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.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
res.send(response_form);
|
res.send(response_form);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import jackson from '@lib/jackson';
|
import jackson from '@lib/jackson';
|
||||||
import { extractAuthToken } from '@lib/auth';
|
import { extractAuthToken } from '@lib/auth';
|
||||||
|
import retraced from '@ee/retraced';
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
|
@ -27,6 +28,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
const profile = await oauthController.userInfo(token);
|
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);
|
res.json(profile);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('userinfo error:', err);
|
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