Add SOC 2 readiness evidence endpoint
This commit is contained in:
parent
1b0f1c08d2
commit
f7160fceab
@ -28,6 +28,7 @@ import { NotificationsModule } from "./notifications/notifications.module";
|
|||||||
import { BillPayModule } from "./bill-pay/bill-pay.module";
|
import { BillPayModule } from "./bill-pay/bill-pay.module";
|
||||||
import { CreditScoreModule } from "./credit-score/credit-score.module";
|
import { CreditScoreModule } from "./credit-score/credit-score.module";
|
||||||
import { ViewModule } from "./view/view.module";
|
import { ViewModule } from "./view/view.module";
|
||||||
|
import { ComplianceModule } from "./compliance/compliance.module";
|
||||||
import { LoggerModule } from "nestjs-pino";
|
import { LoggerModule } from "nestjs-pino";
|
||||||
import { JwtAuthGuard } from "./common/guards/jwt-auth.guard";
|
import { JwtAuthGuard } from "./common/guards/jwt-auth.guard";
|
||||||
import { BrowserUntrustedInterceptor } from "./common/browser-untrusted.interceptor";
|
import { BrowserUntrustedInterceptor } from "./common/browser-untrusted.interceptor";
|
||||||
@ -88,6 +89,7 @@ import { BrowserUntrustedInterceptor } from "./common/browser-untrusted.intercep
|
|||||||
BillPayModule,
|
BillPayModule,
|
||||||
CreditScoreModule,
|
CreditScoreModule,
|
||||||
ViewModule,
|
ViewModule,
|
||||||
|
ComplianceModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// Apply rate limiting globally
|
// Apply rate limiting globally
|
||||||
|
|||||||
14
src/compliance/compliance.controller.ts
Normal file
14
src/compliance/compliance.controller.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Controller, Get } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { CurrentUser } from "../common/decorators/current-user.decorator";
|
||||||
|
import { ComplianceService } from "./compliance.service";
|
||||||
|
|
||||||
|
@Controller("compliance")
|
||||||
|
export class ComplianceController {
|
||||||
|
constructor(private readonly complianceService: ComplianceService) {}
|
||||||
|
|
||||||
|
@Get("soc2/readiness")
|
||||||
|
async soc2Readiness(@CurrentUser() userId: string) {
|
||||||
|
return this.complianceService.getSoc2Readiness(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/compliance/compliance.module.ts
Normal file
10
src/compliance/compliance.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { ComplianceController } from "./compliance.controller";
|
||||||
|
import { ComplianceService } from "./compliance.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ComplianceController],
|
||||||
|
providers: [ComplianceService],
|
||||||
|
})
|
||||||
|
export class ComplianceModule {}
|
||||||
120
src/compliance/compliance.service.ts
Normal file
120
src/compliance/compliance.service.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
|
||||||
|
export type ControlStatus = "implemented" | "partial" | "external_required";
|
||||||
|
|
||||||
|
export interface Soc2Control {
|
||||||
|
id: string;
|
||||||
|
trustServiceCriterion: string;
|
||||||
|
status: ControlStatus;
|
||||||
|
control: string;
|
||||||
|
evidence: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Soc2ReadinessReport {
|
||||||
|
framework: "SOC 2";
|
||||||
|
readinessStatus: "technical_baseline_ready";
|
||||||
|
certificationStatus: "not_certified";
|
||||||
|
generatedAt: string;
|
||||||
|
evidenceSummary: {
|
||||||
|
auditLogCount: number;
|
||||||
|
exportLogCount: number;
|
||||||
|
abuseEventCount: number;
|
||||||
|
activeSessionCount: number;
|
||||||
|
};
|
||||||
|
controls: Soc2Control[];
|
||||||
|
pendingExternalControls: string[];
|
||||||
|
note: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ComplianceService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async getSoc2Readiness(userId: string): Promise<Soc2ReadinessReport> {
|
||||||
|
const [auditLogCount, exportLogCount, abuseEventCount, activeSessionCount] = await Promise.all([
|
||||||
|
this.prisma.auditLog.count({ where: { userId } }),
|
||||||
|
this.prisma.exportLog.count({ where: { userId } }),
|
||||||
|
this.prisma.abuseEvent.count({ where: { userId } }),
|
||||||
|
this.prisma.session.count({ where: { userId, revokedAt: null, expiresAt: { gt: new Date() } } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const controls: Soc2Control[] = [
|
||||||
|
{
|
||||||
|
id: "CC6.1",
|
||||||
|
trustServiceCriterion: "Logical access",
|
||||||
|
status: "implemented",
|
||||||
|
control: "Authenticated access is bound to a server-side session with IP, user-agent, refresh token, and rotating nonce checks.",
|
||||||
|
evidence: [
|
||||||
|
"JwtAuthGuard validates session ID, IP hash, user-agent hash, and per-request nonce hash.",
|
||||||
|
`${activeSessionCount} active session(s) currently recorded for this user.`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "CC6.7",
|
||||||
|
trustServiceCriterion: "Data confidentiality",
|
||||||
|
status: "implemented",
|
||||||
|
control: "Sensitive tokens and raw transaction payloads are encrypted before storage, and browser APIs return presentation data.",
|
||||||
|
evidence: [
|
||||||
|
"Plaid tokens and raw transaction payloads use server-side encryption services.",
|
||||||
|
"Browser-facing finance routes are served through sanitized view APIs.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "CC7.2",
|
||||||
|
trustServiceCriterion: "Security monitoring",
|
||||||
|
status: "implemented",
|
||||||
|
control: "Abuse and anomaly signals are recorded for risky usage patterns.",
|
||||||
|
evidence: [`${abuseEventCount} abuse/risk event(s) currently recorded for this user.`],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "CC7.3",
|
||||||
|
trustServiceCriterion: "Auditability",
|
||||||
|
status: "implemented",
|
||||||
|
control: "Security-relevant account, export, and collaboration actions write audit evidence.",
|
||||||
|
evidence: [
|
||||||
|
`${auditLogCount} audit event(s) currently recorded for this user.`,
|
||||||
|
`${exportLogCount} export audit record(s) currently recorded for this user.`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "CC8.1",
|
||||||
|
trustServiceCriterion: "Change management",
|
||||||
|
status: "partial",
|
||||||
|
control: "Repository-level CI and committed implementation history exist, but formal approval and release controls remain operational processes.",
|
||||||
|
evidence: ["Technical CI coverage can support SOC 2 evidence, but policy attestation is outside the application runtime."],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "P1.1",
|
||||||
|
trustServiceCriterion: "Privacy",
|
||||||
|
status: "partial",
|
||||||
|
control: "User erasure is available through the authenticated account deletion flow.",
|
||||||
|
evidence: ["GDPR-style account deletion endpoint is implemented; formal privacy notices and DSR operating procedures remain external controls."],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
framework: "SOC 2",
|
||||||
|
readinessStatus: "technical_baseline_ready",
|
||||||
|
certificationStatus: "not_certified",
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
evidenceSummary: {
|
||||||
|
auditLogCount,
|
||||||
|
exportLogCount,
|
||||||
|
abuseEventCount,
|
||||||
|
activeSessionCount,
|
||||||
|
},
|
||||||
|
controls,
|
||||||
|
pendingExternalControls: [
|
||||||
|
"Independent SOC 2 auditor engagement and examination period",
|
||||||
|
"Board/management-approved security, access control, incident response, vendor, and change management policies",
|
||||||
|
"Recurring access reviews with retained evidence",
|
||||||
|
"Vendor risk reviews for infrastructure, email, payment, and data-provider subprocessors",
|
||||||
|
"Incident response tabletop or equivalent drill evidence",
|
||||||
|
"Employee security training and onboarding/offboarding records",
|
||||||
|
],
|
||||||
|
note: "This endpoint reports application control evidence only. It does not represent SOC 2 certification or audit opinion.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
38
test/compliance.service.spec.ts
Normal file
38
test/compliance.service.spec.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { ComplianceService } from "../src/compliance/compliance.service";
|
||||||
|
|
||||||
|
const createService = () => {
|
||||||
|
const prisma = {
|
||||||
|
auditLog: { count: jest.fn() },
|
||||||
|
exportLog: { count: jest.fn() },
|
||||||
|
abuseEvent: { count: jest.fn() },
|
||||||
|
session: { count: jest.fn() },
|
||||||
|
};
|
||||||
|
|
||||||
|
return { service: new ComplianceService(prisma as any), prisma };
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("ComplianceService", () => {
|
||||||
|
it("returns SOC 2 readiness evidence without claiming certification", async () => {
|
||||||
|
const { service, prisma } = createService();
|
||||||
|
prisma.auditLog.count.mockResolvedValue(12);
|
||||||
|
prisma.exportLog.count.mockResolvedValue(3);
|
||||||
|
prisma.abuseEvent.count.mockResolvedValue(2);
|
||||||
|
prisma.session.count.mockResolvedValue(1);
|
||||||
|
|
||||||
|
const result = await service.getSoc2Readiness("user_1");
|
||||||
|
|
||||||
|
expect(result.readinessStatus).toBe("technical_baseline_ready");
|
||||||
|
expect(result.certificationStatus).toBe("not_certified");
|
||||||
|
expect(result.evidenceSummary).toEqual({
|
||||||
|
auditLogCount: 12,
|
||||||
|
exportLogCount: 3,
|
||||||
|
abuseEventCount: 2,
|
||||||
|
activeSessionCount: 1,
|
||||||
|
});
|
||||||
|
expect(result.controls).toEqual(expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: "CC6.1", status: "implemented" }),
|
||||||
|
expect.objectContaining({ id: "CC8.1", status: "partial" }),
|
||||||
|
]));
|
||||||
|
expect(result.pendingExternalControls.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user