Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
db0e7e7
feat(pentest): full v1 rebuild — split-view UI, SDK swap, signed webh…
tofikwest Apr 27, 2026
f494299
feat(pentest): credits wallet, admin grants, audit logging, UX polish
tofikwest Apr 29, 2026
9da3780
Merge main into feat/pentest-credits
tofikwest Apr 29, 2026
ebed6cc
fix(pentest): address cubic-AI review — refund tx, audit dedup, UX bugs
tofikwest Apr 29, 2026
648ad0e
chore(pentest): delete stale frontend test files
tofikwest Apr 29, 2026
dbffafb
chore: merge release v3.35.0 back to main [skip ci]
github-actions[bot] Apr 29, 2026
4aeef2a
fix(pentest): api-side maced filter, schema docs, drop stale planning…
tofikwest Apr 29, 2026
23396c6
Merge branch 'main' into feat/pentest-credits
tofikwest Apr 29, 2026
f5e2528
fix(pentest): second cubic review pass — refund retry, ux + correctness
tofikwest Apr 29, 2026
d8e8237
chore(integrations-catalog): refresh 2026-04-29
tofikwest Apr 29, 2026
f7a9ce9
fix(api): purge snapshot — null out stripe IDs from dropped tables
tofikwest Apr 29, 2026
9a65c3b
Merge pull request #2691 from trycompai/feat/pentest-credits
tofikwest Apr 29, 2026
5c89b1d
Merge branch 'main' into feat/integrations-catalog
tofikwest Apr 29, 2026
8e4ca07
Merge pull request #2693 from trycompai/feat/integrations-catalog
tofikwest Apr 29, 2026
c8cf87e
chore(pentest): tighten empty-state copy
tofikwest Apr 29, 2026
4556196
chore(pentest): drop "agents" wording from empty state
tofikwest Apr 29, 2026
0a2d766
fix: fix progress counts for frameworks, requirements and controls
github-actions[bot] Apr 29, 2026
0e7492c
feat(app): add SOC 3
github-actions[bot] Apr 29, 2026
6a3f631
fix(app): able to upload markdowns or CSVs as evidence for documents …
github-actions[bot] Apr 29, 2026
8d747c1
Merge branch 'main' into chore/pentest-empty-state-copy
tofikwest Apr 29, 2026
948e287
Merge pull request #2696 from trycompai/chore/pentest-empty-state-copy
tofikwest Apr 29, 2026
967a44f
chore(pentest): genericize target URL placeholder
tofikwest Apr 29, 2026
57de619
Merge pull request #2697 from trycompai/chore/pentest-target-placeholder
tofikwest Apr 29, 2026
589d576
chore(integrations-catalog): refresh 2026-04-29 (round 2)
tofikwest Apr 29, 2026
9fc74a2
Merge branch 'main' into feat/integrations-catalog
tofikwest Apr 29, 2026
2670b15
Merge pull request #2698 from trycompai/feat/integrations-catalog
tofikwest Apr 29, 2026
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
36 changes: 0 additions & 36 deletions .github/workflows/maced-contract-canary.yml

This file was deleted.

5 changes: 4 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@aws-sdk/s3-request-presigner": "3.1013.0",
"@browserbasehq/sdk": "2.6.0",
"@browserbasehq/stagehand": "^3.2.1",
"@maced/api-client": "^0.9.1",
"@mendable/firecrawl-js": "^4.9.3",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
Expand Down Expand Up @@ -157,6 +158,9 @@
"transform": {
"^.+\\.(t|j)sx?$": "ts-jest"
},
"transformIgnorePatterns": [
"node_modules/(?!(@maced/api-client|better-auth)/)"
],
"collectCoverageFrom": [
"**/*.(t|j)s"
],
Expand Down Expand Up @@ -197,7 +201,6 @@
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:e2e:maced": "MACED_CONTRACT_E2E=1 jest --config ./test/jest-e2e.json --runInBand ./maced-contract.e2e-spec.ts",
"test:watch": "jest --watch",
"typecheck": "tsc --noEmit"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const SEGMENT_TO_RESOURCE: Record<
tasks: { entity: AuditLogEntityType.task, singular: 'task' },
vendors: { entity: AuditLogEntityType.vendor, singular: 'vendor' },
context: { entity: AuditLogEntityType.organization, singular: 'context' },
'pentest-credits': {
entity: AuditLogEntityType.pentest,
singular: 'pentest credits',
},
};

const SPECIAL_ACTION_DESCRIPTIONS: Record<string, string> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { EvidenceFormsModule } from '../evidence-forms/evidence-forms.module';
import { PoliciesModule } from '../policies/policies.module';
import { CommentsModule } from '../comments/comments.module';
import { AttachmentsModule } from '../attachments/attachments.module';
import { SecurityPenetrationTestsModule } from '../security-penetration-tests/security-penetration-tests.module';
import { AdminOrganizationsController } from './admin-organizations.controller';
import { AdminOrganizationsService } from './admin-organizations.service';
import { PurgeOrganizationService } from './purge-organization.service';
Expand All @@ -18,6 +19,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 { AdminPentestCreditsController } from './admin-pentest-credits.controller';

@Module({
imports: [
Expand All @@ -29,6 +31,7 @@ import { AdminEvidenceController } from './admin-evidence.controller';
PoliciesModule,
CommentsModule,
AttachmentsModule,
SecurityPenetrationTestsModule,
],
controllers: [
AdminOrganizationsController,
Expand All @@ -38,6 +41,7 @@ import { AdminEvidenceController } from './admin-evidence.controller';
AdminVendorsController,
AdminContextController,
AdminEvidenceController,
AdminPentestCreditsController,
],
providers: [
AdminOrganizationsService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
BadRequestException,
Body,
Controller,
Get,
Param,
Post,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { ApiExcludeController, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
import { PlatformAdminGuard } from '../auth/platform-admin.guard';
import { PentestCreditsService } from '../security-penetration-tests/pentest-credits.service';
import { AdminAuditLogInterceptor } from './admin-audit-log.interceptor';

/**
* Request body for granting credits via the admin panel. `amount` is capped
* at 1000 to prevent typo-induced runaway grants — admins can submit
* multiple times if a larger pool is genuinely needed.
*/
class GrantPentestCreditsDto {
@IsInt()
@Min(1)
@Max(1000)
amount!: number;

/**
* Free-form note. Persisted on the audit log entry as `data.note` so
* support / compliance can reconstruct *why* a grant happened.
*/
@IsOptional()
@IsString()
note?: string;
}

@ApiExcludeController()
@ApiTags('Admin - Pentest Credits')
@Controller({ path: 'admin/organizations', version: '1' })
@UseGuards(PlatformAdminGuard)
@UseInterceptors(AdminAuditLogInterceptor)
@Throttle({ default: { ttl: 60_000, limit: 30 } })
export class AdminPentestCreditsController {
constructor(private readonly credits: PentestCreditsService) {}

@Get(':orgId/pentest-credits')
@ApiOperation({
summary: 'Get pentest credit wallet status for any organization',
})
async getStatus(@Param('orgId') orgId: string) {
return this.credits.getStatus(orgId);
}

// POST /:orgId/pentest-credits (no `/grant` suffix). The
// AdminAuditLogInterceptor's URL parser treats the segment after the
// resource as an entity id; if we used `/grant`, the audit log
// would record `entityId: "grant"` which is meaningless and breaks
// the admin audit trail. Keeping the route shape standard
// (`:orgId/<resource>`) lets the interceptor produce correct metadata.
@Post(':orgId/pentest-credits')
@ApiOperation({
summary: 'Grant pentest credits to an organization (platform admin)',
})
async grant(
@Param('orgId') orgId: string,
@Body() body: GrantPentestCreditsDto,
) {
if (!Number.isInteger(body.amount) || body.amount < 1) {
throw new BadRequestException('amount must be a positive integer');
}
await this.credits.grant(orgId, body.amount, 'manual');
return this.credits.getStatus(orgId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ export class PurgeOrganizationSnapshotService {
}

const [
billing,
pentest,
trustResources,
trustNdas,
trustDocs,
Expand All @@ -36,14 +34,13 @@ export class PurgeOrganizationSnapshotService {
integrations,
counts,
] = await Promise.all([
db.organizationBilling.findUnique({
where: { organizationId },
select: { stripeCustomerId: true },
}),
db.pentestSubscription.findUnique({
where: { organizationId },
select: { stripeSubscriptionId: true },
}),
// The legacy `organization_billing` and `pentest_subscriptions`
// tables were dropped in migration 20260427000000_pentest_credits;
// they were Stripe-coupled records that never had production data
// and have been superseded by the `pentest_credits` wallet model.
// The snapshot intentionally omits them — there's nothing to
// capture. If/when v2 introduces real Stripe billing, the new
// tables get added here at that point.
db.trustResource.findMany({
where: { organizationId },
select: { s3Key: true },
Expand Down Expand Up @@ -130,9 +127,13 @@ export class PurgeOrganizationSnapshotService {
return {
organization: { id: org.id, name: org.name, slug: org.slug },
counts,
// Stripe IDs intentionally null — the source tables were dropped
// in 20260427000000_pentest_credits. The shape is preserved so
// downstream consumers (purge orchestrator) don't need to change
// until v2 billing replaces these.
stripe: {
customerId: billing?.stripeCustomerId ?? null,
subscriptionId: pentest?.stripeSubscriptionId ?? null,
customerId: null,
subscriptionId: null,
},
s3KeysByBucket,
knowledgeBaseDocumentIds: kbDocs.map((d) => d.id),
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/audit/audit-log.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const RESOURCE_TO_ENTITY_TYPE: Record<
trust: AuditLogEntityType.trust,
app: AuditLogEntityType.organization,
questionnaire: AuditLogEntityType.organization,
pentest: AuditLogEntityType.pentest,
audit: null,
};

Expand Down
26 changes: 25 additions & 1 deletion apps/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import { adminAuthRateLimiter } from './auth/admin-rate-limit.middleware';
import { originCheckMiddleware } from './auth/origin-check.middleware';
import { mkdirSync, writeFileSync, existsSync } from 'fs';

declare module 'express-serve-static-core' {
interface Request {
rawBody?: Buffer;
}
}

let app: INestApplication | null = null;

function describeServer(baseUrl: string): string {
Expand Down Expand Up @@ -78,6 +84,23 @@ async function bootstrap(): Promise<void> {
// request stream to properly read the body (including OAuth callbackURL).
// Express-level middleware runs BEFORE NestJS module middleware, so without this
// skip, express.json() would consume the stream before better-auth's handler.
// Routes that need the exact request bytes for HMAC signature verification.
// Anything matched here gets `req.rawBody` populated; everything else uses
// the standard parser which discards the buffer to avoid keeping a 150MB
// copy of every JSON payload alive on the heap.
const RAW_BODY_PATHS = [
'/v1/security-penetration-tests/webhook',
'/security-penetration-tests/webhook',
];
const needsRawBody = (req: express.Request): boolean =>
RAW_BODY_PATHS.some((p) => req.path.endsWith(p));

const jsonParserWithRaw = express.json({
limit: '150mb',
verify: (req, _res, buf) => {
(req as express.Request).rawBody = buf;
},
});
const jsonParser = express.json({ limit: '150mb' });
const urlencodedParser = express.urlencoded({
limit: '150mb',
Expand All @@ -92,7 +115,8 @@ async function bootstrap(): Promise<void> {
if (req.path.startsWith('/api/auth')) {
return next();
}
jsonParser(req, res, (err?: unknown) => {
const parser = needsRawBody(req) ? jsonParserWithRaw : jsonParser;
parser(req, res, (err?: unknown) => {
if (err) return next(err);
urlencodedParser(req, res, next);
});
Expand Down
51 changes: 0 additions & 51 deletions apps/api/src/security-penetration-tests/README.md

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,6 @@ export class CreatePenetrationTestDto {
@IsUrl()
repoUrl?: string;

@ApiPropertyOptional({
description: 'GitHub token used for cloning private repositories',
required: false,
})
@IsOptional()
@IsString()
githubToken?: string;

@ApiPropertyOptional({
description: 'Optional YAML configuration for the pentest run',
required: false,
})
@IsOptional()
@IsString()
configYaml?: string;

@ApiPropertyOptional({
description: 'Whether to enable pipeline testing mode',
required: false,
Expand All @@ -43,14 +27,6 @@ export class CreatePenetrationTestDto {
@IsBoolean()
pipelineTesting?: boolean;

@ApiPropertyOptional({
description: 'Workspace identifier used by the pentest engine',
required: false,
})
@IsOptional()
@IsString()
workspace?: string;

@ApiPropertyOptional({
description:
'Optional webhook URL to notify when report generation completes',
Expand Down
Loading
Loading