Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions apps/api/src/admin-organizations/admin-frameworks.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AdminFrameworksController } from './admin-frameworks.controller';
import { FrameworksService } from '../frameworks/frameworks.service';

jest.mock('../auth/platform-admin.guard', () => ({
PlatformAdminGuard: class {
canActivate() {
return true;
}
},
}));

jest.mock('../auth/auth.server', () => ({
auth: { api: {} },
}));

jest.mock('@db', () => ({
db: {},
AuditLogEntityType: {
organization: 'organization',
people: 'people',
control: 'control',
policy: 'policy',
task: 'task',
vendor: 'vendor',
risk: 'risk',
finding: 'finding',
framework: 'framework',
integration: 'integration',
trust: 'trust',
pentest: 'pentest',
},
CommentEntityType: {
task: 'task',
vendor: 'vendor',
risk: 'risk',
policy: 'policy',
},
FindingType: { soc2: 'soc2', iso27001: 'iso27001' },
}));

jest.mock('@trigger.dev/sdk', () => ({
tasks: { trigger: jest.fn() },
}));

jest.mock('../frameworks/frameworks-scores.helper', () => ({
getOverviewScores: jest.fn(),
getCurrentMember: jest.fn(),
computeFrameworkComplianceScore: jest.fn(),
}));

describe('AdminFrameworksController', () => {
let controller: AdminFrameworksController;

const mockService = {
findAll: jest.fn(),
findAvailable: jest.fn(),
addFrameworks: jest.fn(),
delete: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AdminFrameworksController],
providers: [{ provide: FrameworksService, useValue: mockService }],
}).compile();

controller = module.get<AdminFrameworksController>(
AdminFrameworksController,
);
jest.clearAllMocks();
});

it('lists active frameworks and only unavailable platform frameworks', async () => {
const activeFramework = {
id: 'fi_1',
framework: { id: 'fw_soc2', name: 'SOC 2' },
customFramework: null,
};
mockService.findAll.mockResolvedValue([activeFramework]);
mockService.findAvailable.mockResolvedValue([
{ id: 'fw_soc2', name: 'SOC 2', isCustom: false },
{ id: 'fw_iso', name: 'ISO 27001', isCustom: false },
{ id: 'cf_1', name: 'Custom', isCustom: true },
]);

const result = await controller.list('org_1');

expect(mockService.findAll).toHaveBeenCalledWith('org_1');
expect(mockService.findAvailable).toHaveBeenCalledWith('org_1');
expect(result).toEqual({
frameworks: [activeFramework],
availableFrameworks: [
{ id: 'fw_iso', name: 'ISO 27001', isCustom: false },
],
});
});

it('adds frameworks to the requested organization', async () => {
const created = { success: true, frameworksAdded: 1 };
mockService.addFrameworks.mockResolvedValue(created);

const result = await controller.addFrameworks('org_1', {
frameworkIds: ['fw_soc2'],
});

expect(mockService.addFrameworks).toHaveBeenCalledWith('org_1', [
'fw_soc2',
]);
expect(result).toEqual(created);
});

it('deletes framework instances from the requested organization', async () => {
mockService.delete.mockResolvedValue({ success: true });

const result = await controller.deleteFramework('org_1', 'fi_1');

expect(mockService.delete).toHaveBeenCalledWith('fi_1', 'org_1');
expect(result).toEqual({ success: true });
});
});
79 changes: 79 additions & 0 deletions apps/api/src/admin-organizations/admin-frameworks.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
UseGuards,
UseInterceptors,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiExcludeController, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { PlatformAdminGuard } from '../auth/platform-admin.guard';
import { FrameworksService } from '../frameworks/frameworks.service';
import { AddFrameworksDto } from '../frameworks/dto/add-frameworks.dto';
import { AdminAuditLogInterceptor } from './admin-audit-log.interceptor';

@ApiExcludeController()
@ApiTags('Admin - Frameworks')
@Controller({ path: 'admin/organizations', version: '1' })
@UseGuards(PlatformAdminGuard)
@UseInterceptors(AdminAuditLogInterceptor)
@Throttle({ default: { ttl: 60000, limit: 30 } })
export class AdminFrameworksController {
constructor(private readonly frameworksService: FrameworksService) {}

@Get(':orgId/frameworks')
@ApiOperation({ summary: 'List frameworks for an organization (admin)' })
async list(@Param('orgId') orgId: string) {
const [frameworks, availableFrameworks] = await Promise.all([
this.frameworksService.findAll(orgId),
this.frameworksService.findAvailable(orgId),
]);

const activeFrameworkIds = new Set(
frameworks
.map(
(framework) =>
framework.framework?.id ?? framework.customFramework?.id,
)
.filter((id): id is string => Boolean(id)),
);

return {
frameworks,
availableFrameworks: availableFrameworks.filter(
(framework) =>
framework.isCustom === false && !activeFrameworkIds.has(framework.id),
),
};
}

@Post(':orgId/frameworks')
@ApiOperation({ summary: 'Add frameworks to an organization (admin)' })
@UsePipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
)
async addFrameworks(
@Param('orgId') orgId: string,
@Body() dto: AddFrameworksDto,
) {
return this.frameworksService.addFrameworks(orgId, dto.frameworkIds);
}

@Delete(':orgId/frameworks/:frameworkInstanceId')
@ApiOperation({ summary: 'Remove a framework from an organization (admin)' })
async deleteFramework(
@Param('orgId') orgId: string,
@Param('frameworkInstanceId') frameworkInstanceId: string,
) {
return this.frameworksService.delete(frameworkInstanceId, orgId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CommentsModule } from '../comments/comments.module';
import { AttachmentsModule } from '../attachments/attachments.module';
import { BillingModule } from '../billing/billing.module';
import { SecurityPenetrationTestsModule } from '../security-penetration-tests/security-penetration-tests.module';
import { FrameworksModule } from '../frameworks/frameworks.module';
import { AdminBillingActionsService } from './admin-billing-actions.service';
import { AdminBillingController } from './admin-billing.controller';
import { AdminBillingService } from './admin-billing.service';
Expand All @@ -24,6 +25,7 @@ import { AdminVendorsController } from './admin-vendors.controller';
import { AdminContextController } from './admin-context.controller';
import { AdminEvidenceController } from './admin-evidence.controller';
import { AdminPentestCreditsController } from './admin-pentest-credits.controller';
import { AdminFrameworksController } from './admin-frameworks.controller';

@Module({
imports: [
Expand All @@ -37,6 +39,7 @@ import { AdminPentestCreditsController } from './admin-pentest-credits.controlle
AttachmentsModule,
BillingModule,
SecurityPenetrationTestsModule,
FrameworksModule,
],
controllers: [
AdminOrganizationsController,
Expand All @@ -48,6 +51,7 @@ import { AdminPentestCreditsController } from './admin-pentest-credits.controlle
AdminEvidenceController,
AdminPentestCreditsController,
AdminBillingController,
AdminFrameworksController,
],
providers: [
AdminOrganizationsService,
Expand Down
41 changes: 37 additions & 4 deletions apps/api/src/admin-organizations/admin-security.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AdminTasksController } from './admin-tasks.controller';
import { AdminVendorsController } from './admin-vendors.controller';
import { AdminContextController } from './admin-context.controller';
import { AdminEvidenceController } from './admin-evidence.controller';
import { AdminFrameworksController } from './admin-frameworks.controller';
import { AdminIntegrationsController } from '../integration-platform/controllers/admin-integrations.controller';
import { PlatformAuditLogInterceptor } from '../integration-platform/interceptors/platform-audit-log.interceptor';

Expand All @@ -21,13 +22,33 @@ jest.mock('../auth/auth.server', () => ({

jest.mock('@db', () => ({
db: {},
AuditLogEntityType: {
organization: 'organization',
people: 'people',
control: 'control',
policy: 'policy',
task: 'task',
vendor: 'vendor',
risk: 'risk',
finding: 'finding',
framework: 'framework',
integration: 'integration',
trust: 'trust',
pentest: 'pentest',
},
FindingStatus: {
open: 'open',
ready_for_review: 'ready_for_review',
needs_revision: 'needs_revision',
closed: 'closed',
},
FindingType: { soc2: 'soc2', iso27001: 'iso27001' },
FindingSeverity: {
low: 'low',
medium: 'medium',
high: 'high',
critical: 'critical',
},
TaskStatus: { todo: 'todo', in_progress: 'in_progress', done: 'done' },
TaskFrequency: { daily: 'daily', weekly: 'weekly', monthly: 'monthly' },
Departments: { none: 'none', engineering: 'engineering' },
Expand All @@ -38,6 +59,17 @@ jest.mock('@db', () => ({
Prisma: {},
}));

jest.mock('@trycompai/auth', () => ({
RESTRICTED_ROLES: ['employee', 'contractor'],
PRIVILEGED_ROLES: ['owner', 'admin', 'auditor'],
}));

jest.mock('../frameworks/frameworks-scores.helper', () => ({
getOverviewScores: jest.fn(),
getCurrentMember: jest.fn(),
computeFrameworkComplianceScore: jest.fn(),
}));

jest.mock('@trigger.dev/sdk', () => ({
auth: { createPublicToken: jest.fn() },
tasks: { trigger: jest.fn() },
Expand All @@ -59,6 +91,7 @@ const ORG_ADMIN_CONTROLLERS = [
{ name: 'AdminVendorsController', controller: AdminVendorsController },
{ name: 'AdminContextController', controller: AdminContextController },
{ name: 'AdminEvidenceController', controller: AdminEvidenceController },
{ name: 'AdminFrameworksController', controller: AdminFrameworksController },
];

describe('Admin controllers security baseline', () => {
Expand Down Expand Up @@ -103,8 +136,8 @@ describe('Admin controllers security baseline', () => {
});
});

it('covers all 7 expected org-scoped admin controllers', () => {
expect(ORG_ADMIN_CONTROLLERS).toHaveLength(7);
it('covers all 8 expected org-scoped admin controllers', () => {
expect(ORG_ADMIN_CONTROLLERS).toHaveLength(8);
});

describe('AdminIntegrationsController', () => {
Expand Down Expand Up @@ -150,8 +183,8 @@ describe('Admin controllers security baseline', () => {
});
});

it('covers all 8 admin controllers (7 org-scoped + 1 platform-scoped)', () => {
expect(ORG_ADMIN_CONTROLLERS).toHaveLength(7);
it('covers all 9 admin controllers (8 org-scoped + 1 platform-scoped)', () => {
expect(ORG_ADMIN_CONTROLLERS).toHaveLength(8);
expect(AdminIntegrationsController).toBeDefined();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ vi.mock('@/lib/api-client', () => ({
get: vi.fn().mockResolvedValue({ data: [] }),
post: vi.fn().mockResolvedValue({ data: {} }),
patch: vi.fn().mockResolvedValue({ data: {} }),
delete: vi.fn().mockResolvedValue({ data: {} }),
},
}));

Expand Down Expand Up @@ -63,6 +64,7 @@ describe('AdminOrgTabs', () => {

expect(screen.getByRole('tab', { name: /overview/i })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /findings/i })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /frameworks/i })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /tasks/i })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /vendors/i })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /context/i })).toBeInTheDocument();
Expand Down
Loading
Loading