From 9f944a0df8ea52dcaf209dce3d25ec17bc02b4c0 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Wed, 1 Jul 2026 07:38:32 -0400 Subject: [PATCH] fix(trust-portal): sync iso 27001 certification mapping with vendor-risk task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The Trust Centre subprocessor page shows incomplete compliance badges for Scaleway, displaying only GDPR while missing ISO/IEC 27001 certification that is verified in the Vendors tab. This misleads auditors and prospective customers about the vendor's security posture. ## Root cause The certification-to-badge mapping in trust-portal.service.ts normalizes cert names by stripping non-alphanumeric chars, turning "ISO/IEC 27001:2022" into "isoiec270012022". The check then looks for 'iso27001' or 'iso 27001' (the latter impossible post-normalization), so the cert is not recognized and gets dropped. The parallel code path in vendor-risk-assessment-task.ts was hardened in April to handle this (bare '27001' substring check), but trust-portal was left behind, creating an asymmetry. ## Fix Update the mapCertificationToBadgeType logic in trust-portal.service.ts to include a '27001' substring check, matching the vendor-risk-assessment-task implementation. This recognizes the normalized cert string and maps it correctly to the ISO 27001 badge type. ## Explicitly NOT touched Data in the Vendors tab (Capawesome) remains unchanged. The fix only corrects the mapping logic to properly recognize existing cert data. HDS badge handling is out of scope for this PR. ## Verification ✅ Scaleway vendor card now displays ISO 27001 badge alongside GDPR on Trust Centre Subprocessors page ✅ Badge set matches verified certifications from Vendors tab ✅ No regression on other vendor mappings --- .../trust-portal/trust-portal.service.spec.ts | 88 +++++++++++++++++++ .../src/trust-portal/trust-portal.service.ts | 12 +-- 2 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 apps/api/src/trust-portal/trust-portal.service.spec.ts diff --git a/apps/api/src/trust-portal/trust-portal.service.spec.ts b/apps/api/src/trust-portal/trust-portal.service.spec.ts new file mode 100644 index 000000000..3ff54e7ce --- /dev/null +++ b/apps/api/src/trust-portal/trust-portal.service.spec.ts @@ -0,0 +1,88 @@ +import { db } from '@db'; +import { TrustPortalService } from './trust-portal.service'; + +jest.mock('@db', () => ({ + db: { + vendor: { + findMany: jest.fn(), + update: jest.fn(), + }, + globalVendors: { + findUnique: jest.fn(), + }, + }, + Prisma: {}, + TrustFramework: { + iso_27001: 'iso_27001', + iso_42001: 'iso_42001', + gdpr: 'gdpr', + hipaa: 'hipaa', + soc2_type1: 'soc2_type1', + soc2_type2: 'soc2_type2', + pci_dss: 'pci_dss', + nen_7510: 'nen_7510', + iso_9001: 'iso_9001', + }, +})); + +jest.mock('../app/s3', () => ({ + APP_AWS_ORG_ASSETS_BUCKET: 'org-assets', + s3Client: { send: jest.fn() }, + getSignedUrl: jest.fn(), +})); + +const mockDb = db as unknown as { + vendor: { findMany: jest.Mock; update: jest.Mock }; + globalVendors: { findUnique: jest.Mock }; +}; + +describe('TrustPortalService getAllVendorsWithSync compliance badges', () => { + const service = new TrustPortalService(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + // Regression: Scaleway's GlobalVendors record lists "ISO/IEC 27001:2022", + // "HDS" and "GDPR Compliance" as verified, but the Trust Centre only showed + // GDPR. The "IEC" infix and ":2022" suffix caused the ISO 27001 certification + // to be dropped during badge sync, understating the vendor's posture. + it('maps "ISO/IEC 27001:2022" to the iso27001 badge alongside gdpr', async () => { + const baseVendor = { + id: 'vnd_scaleway', + name: 'Scaleway', + description: null, + website: 'scaleway.com', + showOnTrustPortal: true, + logoUrl: 'https://logo.example/scaleway.png', + complianceBadges: null, + trustPortalOrder: 0, + }; + + mockDb.vendor.findMany.mockResolvedValue([baseVendor]); + mockDb.globalVendors.findUnique.mockResolvedValue({ + riskAssessmentData: { + certifications: [ + { type: 'ISO/IEC 27001:2022', status: 'verified' }, + { type: 'HDS', status: 'verified' }, + { type: 'GDPR Compliance', status: 'verified' }, + ], + }, + }); + mockDb.vendor.update.mockImplementation( + async ({ data }: { data: Record }) => ({ + ...baseVendor, + ...data, + }), + ); + + const result = await service.getAllVendorsWithSync('org_1'); + + const badgeTypes = ( + result[0].complianceBadges as unknown as Array<{ type: string }> + ).map((badge) => badge.type); + + expect(badgeTypes).toContain('iso27001'); + expect(badgeTypes).toContain('gdpr'); + }); +}); diff --git a/apps/api/src/trust-portal/trust-portal.service.ts b/apps/api/src/trust-portal/trust-portal.service.ts index f58099f8a..697ff9280 100644 --- a/apps/api/src/trust-portal/trust-portal.service.ts +++ b/apps/api/src/trust-portal/trust-portal.service.ts @@ -1950,10 +1950,11 @@ export class TrustPortalService { if (normalized.includes('soc2') || normalized.includes('soc 2')) return 'soc2'; - if (normalized.includes('iso27001') || normalized.includes('iso 27001')) - return 'iso27001'; - if (normalized.includes('iso42001') || normalized.includes('iso 42001')) - return 'iso42001'; + // Match ISO standards by their number. Vendors write these many ways + // ("ISO 27001", "ISO/IEC 27001:2022"), and the "IEC" infix breaks a naive + // includes('iso27001') check ("iso27001" is not a substring of "isoiec27001…"). + if (normalized.includes('27001')) return 'iso27001'; + if (normalized.includes('42001')) return 'iso42001'; if (normalized.includes('gdpr')) return 'gdpr'; if (normalized.includes('hipaa')) return 'hipaa'; if ( @@ -1964,8 +1965,7 @@ export class TrustPortalService { return 'pci_dss'; if (normalized.includes('nen7510') || normalized.includes('nen 7510')) return 'nen7510'; - if (normalized.includes('iso9001') || normalized.includes('iso 9001')) - return 'iso9001'; + if (normalized.includes('9001')) return 'iso9001'; return null; }