mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-27162] Add runtime type guards for decrypted JSON data (#16996)
* Add runtime type guards for decrypted JSON data - Create risk-insights-type-guards.ts with validation functions - Replace unsafe type assertions with runtime validation in encryption service - Validate ApplicationHealthReportDetail, OrganizationReportSummary, and OrganizationReportApplication - Add detailed error messages for validation failures - Remove TODO comments for type guard implementation Improves security by preventing malformed data from bypassing type safety and ensures data integrity for decrypted report structures. * test file fix * date validation * add runtime type guards and validation failure tests Issue 1: Missing Test Coverage for Type Guard Validation Failures - Create comprehensive test suite with 17 tests covering all validation scenarios - Test invalid structures, missing fields, wrong types, and edge cases - Verify proper error messages and validation logic for all data types Issue 2: Silent Failure on Validation Errors (Security Concern) - Re-throw validation errors instead of silently returning empty/default data - Add descriptive error messages indicating potential data corruption or tampering - Ensure all validation failures are surfaced as security issues, not swallowed Additional Fix: Date Validation Vulnerability - Validate date strings before creating Date objects to prevent Invalid Date (NaN) - Throw explicit errors for unparseable date strings - Update error handling to catch and properly surface date validation failures * add empty string validation and sanitize error messages - Validate array elements are non-empty strings (atRiskCipherIds, cipherIds, newApplications) - Sanitize validation error messages to prevent information disclosure - Log detailed errors for debugging, re-throw generic messages - Add tests for empty string validation and error message sanitization * add comprehensive validation for scalar strings and numeric ranges - Validate all scalar string fields are non-empty (applicationName, userName, email, cipherId, userGuid) - Add numeric range validation (finite, non-negative) for all count fields - Export type guard functions for testability and reusability - Add 19 new tests covering edge cases (empty strings, NaN, Infinity, negative numbers) * prevent prototype pollution and unexpected property injection in type guards - Validate object prototype is Object.prototype (prevents __proto__ attacks) - Check for dangerous own properties (constructor, prototype) - Strict property enumeration - reject objects with unexpected properties - Add comprehensive security tests (prototype pollution, unexpected props) - Protects against data tampering and information leakage * security: always sanitize error messages to prevent information disclosure - Remove fragile pattern matching in error handlers - Always throw generic error messages by default - Log detailed errors for debugging, never expose to callers - Future-proof against validation error message changes - Prevents disclosure of internal data structure details Applies to all decryption/validation methods in encryption service * security: comprehensive hardening of type validation system CRITICAL FIXES: - Add __proto__ to prototype pollution checks (loop-based) - Remove conditional error sanitization (always sanitize) SECURITY ENHANCEMENTS: - Add integer overflow protection (Number.isSafeInteger) - Add DoS prevention (array/string length limits: 50K/1K) - Strengthen all 4 type guards with 10-layer validation LIMITS: - Max string length: 1,000 characters - Max array length: 50,000 elements - Max safe integer: 2^53 - 1 DOCUMENTATION: - Update code-review-methodology.md with patterns - Update .cursorrules with security best practices - Create comprehensive security audit document All 57 tests passing. No linting errors. Defense-in-depth complete - production ready. * fix: consolidate security constants and add upper bound validation CRITICAL FIXES: - Consolidate MAX_STRING_LENGTH and MAX_ARRAY_LENGTH to file level (DRY) - Add MAX_COUNT constant (10M) for upper bound validation - Apply upper bound checks to all 12 count fields BENEFITS: - Single source of truth for security limits - Prevents business logic issues from extreme values - Easier maintenance and updates
This commit is contained in:
@@ -151,14 +151,17 @@ describe("RiskInsightsEncryptionService", () => {
|
|||||||
|
|
||||||
describe("decryptRiskInsightsReport", () => {
|
describe("decryptRiskInsightsReport", () => {
|
||||||
it("should decrypt data and return original object", async () => {
|
it("should decrypt data and return original object", async () => {
|
||||||
// Arrange: setup our mocks
|
// Arrange: setup our mocks with valid data structures
|
||||||
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey);
|
mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey);
|
||||||
mockEncryptService.decryptString.mockResolvedValue(JSON.stringify(testData));
|
|
||||||
|
|
||||||
// act: call the decrypt method - with any params
|
// Mock decryption to return valid data for each call
|
||||||
// actual decryption does not happen here,
|
mockEncryptService.decryptString
|
||||||
// we just want to ensure the method calls are correct
|
.mockResolvedValueOnce(JSON.stringify(mockReportData))
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(mockSummaryData))
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(mockApplicationData));
|
||||||
|
|
||||||
|
// act: call the decrypt method
|
||||||
const result = await service.decryptRiskInsightsReport(
|
const result = await service.decryptRiskInsightsReport(
|
||||||
{ organizationId: orgId, userId },
|
{ organizationId: orgId, userId },
|
||||||
mockEncryptedData,
|
mockEncryptedData,
|
||||||
@@ -169,33 +172,37 @@ describe("RiskInsightsEncryptionService", () => {
|
|||||||
expect(mockEncryptService.unwrapSymmetricKey).toHaveBeenCalledWith(mockKey, orgKey);
|
expect(mockEncryptService.unwrapSymmetricKey).toHaveBeenCalledWith(mockKey, orgKey);
|
||||||
expect(mockEncryptService.decryptString).toHaveBeenCalledTimes(3);
|
expect(mockEncryptService.decryptString).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
// Mock decrypt returns JSON.stringify(testData)
|
// Verify decrypted data matches the mocked valid data
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
reportData: testData,
|
reportData: mockReportData,
|
||||||
summaryData: testData,
|
summaryData: mockSummaryData,
|
||||||
applicationData: testData,
|
applicationData: mockApplicationData,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should invoke data type validation method during decryption", async () => {
|
it("should invoke data type validation method during decryption", async () => {
|
||||||
// Arrange: setup our mocks
|
// Arrange: setup our mocks with valid data structures
|
||||||
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey);
|
mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey);
|
||||||
mockEncryptService.decryptString.mockResolvedValue(JSON.stringify(testData));
|
|
||||||
|
|
||||||
// act: call the decrypt method - with any params
|
// Mock decryption to return valid data for each call
|
||||||
// actual decryption does not happen here,
|
mockEncryptService.decryptString
|
||||||
// we just want to ensure the method calls are correct
|
.mockResolvedValueOnce(JSON.stringify(mockReportData))
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(mockSummaryData))
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(mockApplicationData));
|
||||||
|
|
||||||
|
// act: call the decrypt method
|
||||||
const result = await service.decryptRiskInsightsReport(
|
const result = await service.decryptRiskInsightsReport(
|
||||||
{ organizationId: orgId, userId },
|
{ organizationId: orgId, userId },
|
||||||
mockEncryptedData,
|
mockEncryptedData,
|
||||||
mockKey,
|
mockKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Verify that validation passed and returned the correct data
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
reportData: testData,
|
reportData: mockReportData,
|
||||||
summaryData: testData,
|
summaryData: mockSummaryData,
|
||||||
applicationData: testData,
|
applicationData: mockApplicationData,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -211,7 +218,7 @@ describe("RiskInsightsEncryptionService", () => {
|
|||||||
).rejects.toEqual(Error("Organization key not found"));
|
).rejects.toEqual(Error("Organization key not found"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return null if decrypt throws", async () => {
|
it("should throw if decrypt throws", async () => {
|
||||||
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
mockEncryptService.unwrapSymmetricKey.mockRejectedValue(new Error("fail"));
|
mockEncryptService.unwrapSymmetricKey.mockRejectedValue(new Error("fail"));
|
||||||
|
|
||||||
@@ -224,5 +231,106 @@ describe("RiskInsightsEncryptionService", () => {
|
|||||||
),
|
),
|
||||||
).rejects.toEqual(Error("fail"));
|
).rejects.toEqual(Error("fail"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should throw error when report data validation fails", async () => {
|
||||||
|
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey);
|
||||||
|
|
||||||
|
// Mock decryption to return invalid data
|
||||||
|
mockEncryptService.decryptString
|
||||||
|
.mockResolvedValueOnce(JSON.stringify([{ invalid: "data" }])) // invalid report data
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(mockSummaryData))
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(mockApplicationData));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.decryptRiskInsightsReport(
|
||||||
|
{ organizationId: orgId, userId },
|
||||||
|
mockEncryptedData,
|
||||||
|
mockKey,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(
|
||||||
|
/Report data validation failed.*This may indicate data corruption or tampering/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when summary data validation fails", async () => {
|
||||||
|
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey);
|
||||||
|
|
||||||
|
// Clear and reset the mock
|
||||||
|
mockEncryptService.decryptString.mockReset();
|
||||||
|
|
||||||
|
// Mock decryption - report data should succeed, summary should fail
|
||||||
|
mockEncryptService.decryptString
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(mockReportData)) // valid
|
||||||
|
.mockResolvedValueOnce(JSON.stringify({ invalid: "summary" })) // invalid summary data - fails here
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(mockApplicationData)); // won't be called but prevents fallback
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.decryptRiskInsightsReport(
|
||||||
|
{ organizationId: orgId, userId },
|
||||||
|
mockEncryptedData,
|
||||||
|
mockKey,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(
|
||||||
|
/Summary data validation failed.*This may indicate data corruption or tampering/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when application data validation fails", async () => {
|
||||||
|
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey);
|
||||||
|
|
||||||
|
// Clear and reset the mock
|
||||||
|
mockEncryptService.decryptString.mockReset();
|
||||||
|
|
||||||
|
// Mock decryption - report and summary should succeed, application should fail
|
||||||
|
mockEncryptService.decryptString
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(mockReportData)) // valid
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(mockSummaryData)) // valid
|
||||||
|
.mockResolvedValueOnce(JSON.stringify([{ invalid: "application" }])); // invalid app data
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.decryptRiskInsightsReport(
|
||||||
|
{ organizationId: orgId, userId },
|
||||||
|
mockEncryptedData,
|
||||||
|
mockKey,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(
|
||||||
|
/Application data validation failed.*This may indicate data corruption or tampering/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid date in application data", async () => {
|
||||||
|
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey);
|
||||||
|
|
||||||
|
const invalidApplicationData = [
|
||||||
|
{
|
||||||
|
applicationName: "Test App",
|
||||||
|
isCritical: true,
|
||||||
|
reviewedDate: "invalid-date-string",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Clear and reset the mock
|
||||||
|
mockEncryptService.decryptString.mockReset();
|
||||||
|
|
||||||
|
// Mock decryption - report and summary succeed, application with invalid date fails
|
||||||
|
mockEncryptService.decryptString
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(mockReportData)) // valid
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(mockSummaryData)) // valid
|
||||||
|
.mockResolvedValueOnce(JSON.stringify(invalidApplicationData)); // invalid date
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.decryptRiskInsightsReport(
|
||||||
|
{ organizationId: orgId, userId },
|
||||||
|
mockEncryptedData,
|
||||||
|
mockKey,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(
|
||||||
|
/Application data validation failed.*This may indicate data corruption or tampering/,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,14 +10,20 @@ import { LogService } from "@bitwarden/logging";
|
|||||||
|
|
||||||
import { createNewSummaryData } from "../../helpers";
|
import { createNewSummaryData } from "../../helpers";
|
||||||
import {
|
import {
|
||||||
DecryptedReportData,
|
|
||||||
EncryptedReportData,
|
|
||||||
EncryptedDataWithKey,
|
|
||||||
ApplicationHealthReportDetail,
|
ApplicationHealthReportDetail,
|
||||||
OrganizationReportSummary,
|
DecryptedReportData,
|
||||||
|
EncryptedDataWithKey,
|
||||||
|
EncryptedReportData,
|
||||||
OrganizationReportApplication,
|
OrganizationReportApplication,
|
||||||
|
OrganizationReportSummary,
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
|
|
||||||
|
import {
|
||||||
|
validateApplicationHealthReportDetailArray,
|
||||||
|
validateOrganizationReportApplicationArray,
|
||||||
|
validateOrganizationReportSummary,
|
||||||
|
} from "./risk-insights-type-guards";
|
||||||
|
|
||||||
export class RiskInsightsEncryptionService {
|
export class RiskInsightsEncryptionService {
|
||||||
constructor(
|
constructor(
|
||||||
private keyService: KeyService,
|
private keyService: KeyService,
|
||||||
@@ -182,11 +188,16 @@ export class RiskInsightsEncryptionService {
|
|||||||
const decryptedData = await this.encryptService.decryptString(encryptedData, key);
|
const decryptedData = await this.encryptService.decryptString(encryptedData, key);
|
||||||
const parsedData = JSON.parse(decryptedData);
|
const parsedData = JSON.parse(decryptedData);
|
||||||
|
|
||||||
// TODO Add type guard to check that parsed data is actual type
|
// Validate parsed data structure with runtime type guards
|
||||||
return parsedData as ApplicationHealthReportDetail[];
|
return validateApplicationHealthReportDetailArray(parsedData);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
// Log detailed error for debugging
|
||||||
this.logService.error("[RiskInsightsEncryptionService] Failed to decrypt report", error);
|
this.logService.error("[RiskInsightsEncryptionService] Failed to decrypt report", error);
|
||||||
return [];
|
// Always throw generic message to prevent information disclosure
|
||||||
|
// Original error with detailed validation info is logged, not exposed to caller
|
||||||
|
throw new Error(
|
||||||
|
"Report data validation failed. This may indicate data corruption or tampering.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,14 +213,19 @@ export class RiskInsightsEncryptionService {
|
|||||||
const decryptedData = await this.encryptService.decryptString(encryptedData, key);
|
const decryptedData = await this.encryptService.decryptString(encryptedData, key);
|
||||||
const parsedData = JSON.parse(decryptedData);
|
const parsedData = JSON.parse(decryptedData);
|
||||||
|
|
||||||
// TODO Add type guard to check that parsed data is actual type
|
// Validate parsed data structure with runtime type guards
|
||||||
return parsedData as OrganizationReportSummary;
|
return validateOrganizationReportSummary(parsedData);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
// Log detailed error for debugging
|
||||||
this.logService.error(
|
this.logService.error(
|
||||||
"[RiskInsightsEncryptionService] Failed to decrypt report summary",
|
"[RiskInsightsEncryptionService] Failed to decrypt report summary",
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
return createNewSummaryData();
|
// Always throw generic message to prevent information disclosure
|
||||||
|
// Original error with detailed validation info is logged, not exposed to caller
|
||||||
|
throw new Error(
|
||||||
|
"Summary data validation failed. This may indicate data corruption or tampering.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,14 +241,19 @@ export class RiskInsightsEncryptionService {
|
|||||||
const decryptedData = await this.encryptService.decryptString(encryptedData, key);
|
const decryptedData = await this.encryptService.decryptString(encryptedData, key);
|
||||||
const parsedData = JSON.parse(decryptedData);
|
const parsedData = JSON.parse(decryptedData);
|
||||||
|
|
||||||
// TODO Add type guard to check that parsed data is actual type
|
// Validate parsed data structure with runtime type guards
|
||||||
return parsedData as OrganizationReportApplication[];
|
return validateOrganizationReportApplicationArray(parsedData);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
// Log detailed error for debugging
|
||||||
this.logService.error(
|
this.logService.error(
|
||||||
"[RiskInsightsEncryptionService] Failed to decrypt report applications",
|
"[RiskInsightsEncryptionService] Failed to decrypt report applications",
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
return [];
|
// Always throw generic message to prevent information disclosure
|
||||||
|
// Original error with detailed validation info is logged, not exposed to caller
|
||||||
|
throw new Error(
|
||||||
|
"Application data validation failed. This may indicate data corruption or tampering.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,668 @@
|
|||||||
|
import { MemberDetails } from "../../models";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isApplicationHealthReportDetail,
|
||||||
|
isMemberDetails,
|
||||||
|
isOrganizationReportApplication,
|
||||||
|
isOrganizationReportSummary,
|
||||||
|
validateApplicationHealthReportDetailArray,
|
||||||
|
validateOrganizationReportApplicationArray,
|
||||||
|
validateOrganizationReportSummary,
|
||||||
|
} from "./risk-insights-type-guards";
|
||||||
|
|
||||||
|
describe("Risk Insights Type Guards", () => {
|
||||||
|
describe("validateApplicationHealthReportDetailArray", () => {
|
||||||
|
it("should validate valid ApplicationHealthReportDetail array", () => {
|
||||||
|
const validData = [
|
||||||
|
{
|
||||||
|
applicationName: "Test App",
|
||||||
|
passwordCount: 10,
|
||||||
|
atRiskPasswordCount: 2,
|
||||||
|
atRiskCipherIds: ["cipher-1", "cipher-2"],
|
||||||
|
memberCount: 5,
|
||||||
|
atRiskMemberCount: 1,
|
||||||
|
memberDetails: [
|
||||||
|
{
|
||||||
|
userGuid: "user-1",
|
||||||
|
userName: "John Doe",
|
||||||
|
email: "john@example.com",
|
||||||
|
cipherId: "cipher-1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
atRiskMemberDetails: [
|
||||||
|
{
|
||||||
|
userGuid: "user-2",
|
||||||
|
userName: "Jane Doe",
|
||||||
|
email: "jane@example.com",
|
||||||
|
cipherId: "cipher-2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cipherIds: ["cipher-1", "cipher-2"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => validateApplicationHealthReportDetailArray(validData)).not.toThrow();
|
||||||
|
expect(validateApplicationHealthReportDetailArray(validData)).toEqual(validData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for non-array input", () => {
|
||||||
|
expect(() => validateApplicationHealthReportDetailArray("not an array")).toThrow(
|
||||||
|
"Invalid report data: expected array of ApplicationHealthReportDetail, received non-array",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for array with invalid elements", () => {
|
||||||
|
const invalidData = [
|
||||||
|
{
|
||||||
|
applicationName: "Test App",
|
||||||
|
// missing required fields
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => validateApplicationHealthReportDetailArray(invalidData)).toThrow(
|
||||||
|
/Invalid report data: array contains 1 invalid ApplicationHealthReportDetail element\(s\) at indices: 0/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for array with multiple invalid elements", () => {
|
||||||
|
const invalidData = [
|
||||||
|
{ applicationName: "App 1" }, // invalid
|
||||||
|
{
|
||||||
|
applicationName: "App 2",
|
||||||
|
passwordCount: 10,
|
||||||
|
atRiskPasswordCount: 2,
|
||||||
|
atRiskCipherIds: ["cipher-1"],
|
||||||
|
memberCount: 5,
|
||||||
|
atRiskMemberCount: 1,
|
||||||
|
memberDetails: [] as MemberDetails[],
|
||||||
|
atRiskMemberDetails: [] as MemberDetails[],
|
||||||
|
cipherIds: ["cipher-1"],
|
||||||
|
}, // valid
|
||||||
|
{ applicationName: "App 3" }, // invalid
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => validateApplicationHealthReportDetailArray(invalidData)).toThrow(
|
||||||
|
/Invalid report data: array contains 2 invalid ApplicationHealthReportDetail element\(s\) at indices: 0, 2/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid memberDetails", () => {
|
||||||
|
const invalidData = [
|
||||||
|
{
|
||||||
|
applicationName: "Test App",
|
||||||
|
passwordCount: 10,
|
||||||
|
atRiskPasswordCount: 2,
|
||||||
|
atRiskCipherIds: ["cipher-1"],
|
||||||
|
memberCount: 5,
|
||||||
|
atRiskMemberCount: 1,
|
||||||
|
memberDetails: [{ userGuid: "user-1" }] as any, // missing required fields
|
||||||
|
atRiskMemberDetails: [] as MemberDetails[],
|
||||||
|
cipherIds: ["cipher-1"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => validateApplicationHealthReportDetailArray(invalidData)).toThrow(
|
||||||
|
/Invalid report data/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for empty string in atRiskCipherIds", () => {
|
||||||
|
const invalidData = [
|
||||||
|
{
|
||||||
|
applicationName: "Test App",
|
||||||
|
passwordCount: 10,
|
||||||
|
atRiskPasswordCount: 2,
|
||||||
|
atRiskCipherIds: ["cipher-1", "", "cipher-3"], // empty string
|
||||||
|
memberCount: 5,
|
||||||
|
atRiskMemberCount: 1,
|
||||||
|
memberDetails: [] as MemberDetails[],
|
||||||
|
atRiskMemberDetails: [] as MemberDetails[],
|
||||||
|
cipherIds: ["cipher-1"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => validateApplicationHealthReportDetailArray(invalidData)).toThrow(
|
||||||
|
/Invalid report data/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for empty string in cipherIds", () => {
|
||||||
|
const invalidData = [
|
||||||
|
{
|
||||||
|
applicationName: "Test App",
|
||||||
|
passwordCount: 10,
|
||||||
|
atRiskPasswordCount: 2,
|
||||||
|
atRiskCipherIds: ["cipher-1"],
|
||||||
|
memberCount: 5,
|
||||||
|
atRiskMemberCount: 1,
|
||||||
|
memberDetails: [] as MemberDetails[],
|
||||||
|
atRiskMemberDetails: [] as MemberDetails[],
|
||||||
|
cipherIds: ["", "cipher-2"], // empty string
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => validateApplicationHealthReportDetailArray(invalidData)).toThrow(
|
||||||
|
/Invalid report data/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validateOrganizationReportSummary", () => {
|
||||||
|
it("should validate valid OrganizationReportSummary", () => {
|
||||||
|
const validData = {
|
||||||
|
totalMemberCount: 10,
|
||||||
|
totalApplicationCount: 5,
|
||||||
|
totalAtRiskMemberCount: 2,
|
||||||
|
totalAtRiskApplicationCount: 1,
|
||||||
|
totalCriticalApplicationCount: 3,
|
||||||
|
totalCriticalMemberCount: 4,
|
||||||
|
totalCriticalAtRiskMemberCount: 1,
|
||||||
|
totalCriticalAtRiskApplicationCount: 1,
|
||||||
|
newApplications: ["app-1", "app-2"],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => validateOrganizationReportSummary(validData)).not.toThrow();
|
||||||
|
expect(validateOrganizationReportSummary(validData)).toEqual(validData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for missing totalMemberCount", () => {
|
||||||
|
const invalidData = {
|
||||||
|
totalApplicationCount: 5,
|
||||||
|
totalAtRiskMemberCount: 2,
|
||||||
|
totalAtRiskApplicationCount: 1,
|
||||||
|
totalCriticalApplicationCount: 3,
|
||||||
|
totalCriticalMemberCount: 4,
|
||||||
|
totalCriticalAtRiskMemberCount: 1,
|
||||||
|
totalCriticalAtRiskApplicationCount: 1,
|
||||||
|
newApplications: ["app-1"],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => validateOrganizationReportSummary(invalidData)).toThrow(
|
||||||
|
/Invalid OrganizationReportSummary: missing or invalid fields: totalMemberCount \(number\)/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for multiple missing fields", () => {
|
||||||
|
const invalidData = {
|
||||||
|
totalMemberCount: 10,
|
||||||
|
// missing multiple fields
|
||||||
|
newApplications: ["app-1"],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => validateOrganizationReportSummary(invalidData)).toThrow(
|
||||||
|
/Invalid OrganizationReportSummary: missing or invalid fields:.*totalApplicationCount/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid field types", () => {
|
||||||
|
const invalidData = {
|
||||||
|
totalMemberCount: "10", // should be number
|
||||||
|
totalApplicationCount: 5,
|
||||||
|
totalAtRiskMemberCount: 2,
|
||||||
|
totalAtRiskApplicationCount: 1,
|
||||||
|
totalCriticalApplicationCount: 3,
|
||||||
|
totalCriticalMemberCount: 4,
|
||||||
|
totalCriticalAtRiskMemberCount: 1,
|
||||||
|
totalCriticalAtRiskApplicationCount: 1,
|
||||||
|
newApplications: ["app-1"],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => validateOrganizationReportSummary(invalidData)).toThrow(
|
||||||
|
/Invalid OrganizationReportSummary/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for non-array newApplications", () => {
|
||||||
|
const invalidData = {
|
||||||
|
totalMemberCount: 10,
|
||||||
|
totalApplicationCount: 5,
|
||||||
|
totalAtRiskMemberCount: 2,
|
||||||
|
totalAtRiskApplicationCount: 1,
|
||||||
|
totalCriticalApplicationCount: 3,
|
||||||
|
totalCriticalMemberCount: 4,
|
||||||
|
totalCriticalAtRiskMemberCount: 1,
|
||||||
|
totalCriticalAtRiskApplicationCount: 1,
|
||||||
|
newApplications: "not-an-array",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => validateOrganizationReportSummary(invalidData)).toThrow(
|
||||||
|
/Invalid OrganizationReportSummary.*newApplications/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for empty string in newApplications", () => {
|
||||||
|
const invalidData = {
|
||||||
|
totalMemberCount: 10,
|
||||||
|
totalApplicationCount: 5,
|
||||||
|
totalAtRiskMemberCount: 2,
|
||||||
|
totalAtRiskApplicationCount: 1,
|
||||||
|
totalCriticalApplicationCount: 3,
|
||||||
|
totalCriticalMemberCount: 4,
|
||||||
|
totalCriticalAtRiskMemberCount: 1,
|
||||||
|
totalCriticalAtRiskApplicationCount: 1,
|
||||||
|
newApplications: ["app-1", "", "app-3"], // empty string
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => validateOrganizationReportSummary(invalidData)).toThrow(
|
||||||
|
/Invalid OrganizationReportSummary/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validateOrganizationReportApplicationArray", () => {
|
||||||
|
it("should validate valid OrganizationReportApplication array", () => {
|
||||||
|
const validData = [
|
||||||
|
{
|
||||||
|
applicationName: "Test App",
|
||||||
|
isCritical: true,
|
||||||
|
reviewedDate: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
applicationName: "Another App",
|
||||||
|
isCritical: false,
|
||||||
|
reviewedDate: new Date("2024-01-01"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => validateOrganizationReportApplicationArray(validData)).not.toThrow();
|
||||||
|
const result = validateOrganizationReportApplicationArray(validData);
|
||||||
|
expect(result[0].applicationName).toBe("Test App");
|
||||||
|
expect(result[1].reviewedDate).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert string dates to Date objects", () => {
|
||||||
|
const validData = [
|
||||||
|
{
|
||||||
|
applicationName: "Test App",
|
||||||
|
isCritical: true,
|
||||||
|
reviewedDate: "2024-01-01T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = validateOrganizationReportApplicationArray(validData);
|
||||||
|
expect(result[0].reviewedDate).toBeInstanceOf(Date);
|
||||||
|
expect(result[0].reviewedDate?.toISOString()).toBe("2024-01-01T00:00:00.000Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid date strings", () => {
|
||||||
|
const invalidData = [
|
||||||
|
{
|
||||||
|
applicationName: "Test App",
|
||||||
|
isCritical: true,
|
||||||
|
reviewedDate: "invalid-date",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => validateOrganizationReportApplicationArray(invalidData)).toThrow(
|
||||||
|
"Invalid date string: invalid-date",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for non-array input", () => {
|
||||||
|
expect(() => validateOrganizationReportApplicationArray("not an array")).toThrow(
|
||||||
|
"Invalid application data: expected array of OrganizationReportApplication, received non-array",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for array with invalid elements", () => {
|
||||||
|
const invalidData = [
|
||||||
|
{
|
||||||
|
applicationName: "Test App",
|
||||||
|
reviewedDate: null as any,
|
||||||
|
// missing isCritical field
|
||||||
|
} as any,
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => validateOrganizationReportApplicationArray(invalidData)).toThrow(
|
||||||
|
/Invalid application data: array contains 1 invalid OrganizationReportApplication element\(s\) at indices: 0/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error for invalid field types", () => {
|
||||||
|
const invalidData = [
|
||||||
|
{
|
||||||
|
applicationName: 123 as any, // should be string
|
||||||
|
isCritical: true,
|
||||||
|
reviewedDate: null as any,
|
||||||
|
} as any,
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => validateOrganizationReportApplicationArray(invalidData)).toThrow(
|
||||||
|
/Invalid application data/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept null reviewedDate", () => {
|
||||||
|
const validData = [
|
||||||
|
{
|
||||||
|
applicationName: "Test App",
|
||||||
|
isCritical: true,
|
||||||
|
reviewedDate: null as any,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(() => validateOrganizationReportApplicationArray(validData)).not.toThrow();
|
||||||
|
const result = validateOrganizationReportApplicationArray(validData);
|
||||||
|
expect(result[0].reviewedDate).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tests for exported type guard functions
|
||||||
|
describe("isMemberDetails", () => {
|
||||||
|
it("should return true for valid MemberDetails", () => {
|
||||||
|
const validData = {
|
||||||
|
userGuid: "user-1",
|
||||||
|
userName: "John Doe",
|
||||||
|
email: "john@example.com",
|
||||||
|
cipherId: "cipher-1",
|
||||||
|
};
|
||||||
|
expect(isMemberDetails(validData)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for empty userGuid", () => {
|
||||||
|
const invalidData = {
|
||||||
|
userGuid: "",
|
||||||
|
userName: "John Doe",
|
||||||
|
email: "john@example.com",
|
||||||
|
cipherId: "cipher-1",
|
||||||
|
};
|
||||||
|
expect(isMemberDetails(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for empty userName", () => {
|
||||||
|
const invalidData = {
|
||||||
|
userGuid: "user-1",
|
||||||
|
userName: "",
|
||||||
|
email: "john@example.com",
|
||||||
|
cipherId: "cipher-1",
|
||||||
|
};
|
||||||
|
expect(isMemberDetails(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for empty email", () => {
|
||||||
|
const invalidData = {
|
||||||
|
userGuid: "user-1",
|
||||||
|
userName: "John Doe",
|
||||||
|
email: "",
|
||||||
|
cipherId: "cipher-1",
|
||||||
|
};
|
||||||
|
expect(isMemberDetails(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for empty cipherId", () => {
|
||||||
|
const invalidData = {
|
||||||
|
userGuid: "user-1",
|
||||||
|
userName: "John Doe",
|
||||||
|
email: "john@example.com",
|
||||||
|
cipherId: "",
|
||||||
|
};
|
||||||
|
expect(isMemberDetails(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for objects with unexpected properties", () => {
|
||||||
|
const invalidData = {
|
||||||
|
userGuid: "user-1",
|
||||||
|
userName: "John Doe",
|
||||||
|
email: "john@example.com",
|
||||||
|
cipherId: "cipher-1",
|
||||||
|
unexpectedProperty: "should fail",
|
||||||
|
};
|
||||||
|
expect(isMemberDetails(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for prototype pollution attempts", () => {
|
||||||
|
const invalidData = {
|
||||||
|
userGuid: "user-1",
|
||||||
|
userName: "John Doe",
|
||||||
|
email: "john@example.com",
|
||||||
|
cipherId: "cipher-1",
|
||||||
|
__proto__: { malicious: "payload" },
|
||||||
|
};
|
||||||
|
expect(isMemberDetails(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isApplicationHealthReportDetail", () => {
|
||||||
|
it("should return true for valid ApplicationHealthReportDetail", () => {
|
||||||
|
const validData = {
|
||||||
|
applicationName: "Test App",
|
||||||
|
passwordCount: 10,
|
||||||
|
atRiskPasswordCount: 2,
|
||||||
|
atRiskCipherIds: ["cipher-1"],
|
||||||
|
memberCount: 5,
|
||||||
|
atRiskMemberCount: 1,
|
||||||
|
memberDetails: [] as MemberDetails[],
|
||||||
|
atRiskMemberDetails: [] as MemberDetails[],
|
||||||
|
cipherIds: ["cipher-1"],
|
||||||
|
};
|
||||||
|
expect(isApplicationHealthReportDetail(validData)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for empty applicationName", () => {
|
||||||
|
const invalidData = {
|
||||||
|
applicationName: "",
|
||||||
|
passwordCount: 10,
|
||||||
|
atRiskPasswordCount: 2,
|
||||||
|
atRiskCipherIds: ["cipher-1"],
|
||||||
|
memberCount: 5,
|
||||||
|
atRiskMemberCount: 1,
|
||||||
|
memberDetails: [] as MemberDetails[],
|
||||||
|
atRiskMemberDetails: [] as MemberDetails[],
|
||||||
|
cipherIds: ["cipher-1"],
|
||||||
|
};
|
||||||
|
expect(isApplicationHealthReportDetail(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for NaN passwordCount", () => {
|
||||||
|
const invalidData = {
|
||||||
|
applicationName: "Test App",
|
||||||
|
passwordCount: NaN,
|
||||||
|
atRiskPasswordCount: 2,
|
||||||
|
atRiskCipherIds: ["cipher-1"],
|
||||||
|
memberCount: 5,
|
||||||
|
atRiskMemberCount: 1,
|
||||||
|
memberDetails: [] as MemberDetails[],
|
||||||
|
atRiskMemberDetails: [] as MemberDetails[],
|
||||||
|
cipherIds: ["cipher-1"],
|
||||||
|
};
|
||||||
|
expect(isApplicationHealthReportDetail(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for Infinity passwordCount", () => {
|
||||||
|
const invalidData = {
|
||||||
|
applicationName: "Test App",
|
||||||
|
passwordCount: Infinity,
|
||||||
|
atRiskPasswordCount: 2,
|
||||||
|
atRiskCipherIds: ["cipher-1"],
|
||||||
|
memberCount: 5,
|
||||||
|
atRiskMemberCount: 1,
|
||||||
|
memberDetails: [] as MemberDetails[],
|
||||||
|
atRiskMemberDetails: [] as MemberDetails[],
|
||||||
|
cipherIds: ["cipher-1"],
|
||||||
|
};
|
||||||
|
expect(isApplicationHealthReportDetail(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for negative passwordCount", () => {
|
||||||
|
const invalidData = {
|
||||||
|
applicationName: "Test App",
|
||||||
|
passwordCount: -5,
|
||||||
|
atRiskPasswordCount: 2,
|
||||||
|
atRiskCipherIds: ["cipher-1"],
|
||||||
|
memberCount: 5,
|
||||||
|
atRiskMemberCount: 1,
|
||||||
|
memberDetails: [] as MemberDetails[],
|
||||||
|
atRiskMemberDetails: [] as MemberDetails[],
|
||||||
|
cipherIds: ["cipher-1"],
|
||||||
|
};
|
||||||
|
expect(isApplicationHealthReportDetail(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for negative memberCount", () => {
|
||||||
|
const invalidData = {
|
||||||
|
applicationName: "Test App",
|
||||||
|
passwordCount: 10,
|
||||||
|
atRiskPasswordCount: 2,
|
||||||
|
atRiskCipherIds: ["cipher-1"],
|
||||||
|
memberCount: -1,
|
||||||
|
atRiskMemberCount: 1,
|
||||||
|
memberDetails: [] as MemberDetails[],
|
||||||
|
atRiskMemberDetails: [] as MemberDetails[],
|
||||||
|
cipherIds: ["cipher-1"],
|
||||||
|
};
|
||||||
|
expect(isApplicationHealthReportDetail(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for objects with unexpected properties", () => {
|
||||||
|
const invalidData = {
|
||||||
|
applicationName: "Test App",
|
||||||
|
passwordCount: 10,
|
||||||
|
atRiskPasswordCount: 2,
|
||||||
|
atRiskCipherIds: ["cipher-1"],
|
||||||
|
memberCount: 5,
|
||||||
|
atRiskMemberCount: 1,
|
||||||
|
memberDetails: [] as MemberDetails[],
|
||||||
|
atRiskMemberDetails: [] as MemberDetails[],
|
||||||
|
cipherIds: ["cipher-1"],
|
||||||
|
injectedProperty: "malicious",
|
||||||
|
};
|
||||||
|
expect(isApplicationHealthReportDetail(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isOrganizationReportSummary", () => {
|
||||||
|
it("should return true for valid OrganizationReportSummary", () => {
|
||||||
|
const validData = {
|
||||||
|
totalMemberCount: 10,
|
||||||
|
totalApplicationCount: 5,
|
||||||
|
totalAtRiskMemberCount: 2,
|
||||||
|
totalAtRiskApplicationCount: 1,
|
||||||
|
totalCriticalApplicationCount: 3,
|
||||||
|
totalCriticalMemberCount: 4,
|
||||||
|
totalCriticalAtRiskMemberCount: 1,
|
||||||
|
totalCriticalAtRiskApplicationCount: 1,
|
||||||
|
newApplications: ["app-1"],
|
||||||
|
};
|
||||||
|
expect(isOrganizationReportSummary(validData)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for NaN totalMemberCount", () => {
|
||||||
|
const invalidData = {
|
||||||
|
totalMemberCount: NaN,
|
||||||
|
totalApplicationCount: 5,
|
||||||
|
totalAtRiskMemberCount: 2,
|
||||||
|
totalAtRiskApplicationCount: 1,
|
||||||
|
totalCriticalApplicationCount: 3,
|
||||||
|
totalCriticalMemberCount: 4,
|
||||||
|
totalCriticalAtRiskMemberCount: 1,
|
||||||
|
totalCriticalAtRiskApplicationCount: 1,
|
||||||
|
newApplications: ["app-1"],
|
||||||
|
};
|
||||||
|
expect(isOrganizationReportSummary(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for Infinity totalApplicationCount", () => {
|
||||||
|
const invalidData = {
|
||||||
|
totalMemberCount: 10,
|
||||||
|
totalApplicationCount: Infinity,
|
||||||
|
totalAtRiskMemberCount: 2,
|
||||||
|
totalAtRiskApplicationCount: 1,
|
||||||
|
totalCriticalApplicationCount: 3,
|
||||||
|
totalCriticalMemberCount: 4,
|
||||||
|
totalCriticalAtRiskMemberCount: 1,
|
||||||
|
totalCriticalAtRiskApplicationCount: 1,
|
||||||
|
newApplications: ["app-1"],
|
||||||
|
};
|
||||||
|
expect(isOrganizationReportSummary(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for negative totalAtRiskMemberCount", () => {
|
||||||
|
const invalidData = {
|
||||||
|
totalMemberCount: 10,
|
||||||
|
totalApplicationCount: 5,
|
||||||
|
totalAtRiskMemberCount: -1,
|
||||||
|
totalAtRiskApplicationCount: 1,
|
||||||
|
totalCriticalApplicationCount: 3,
|
||||||
|
totalCriticalMemberCount: 4,
|
||||||
|
totalCriticalAtRiskMemberCount: 1,
|
||||||
|
totalCriticalAtRiskApplicationCount: 1,
|
||||||
|
newApplications: ["app-1"],
|
||||||
|
};
|
||||||
|
expect(isOrganizationReportSummary(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for objects with unexpected properties", () => {
|
||||||
|
const invalidData = {
|
||||||
|
totalMemberCount: 10,
|
||||||
|
totalApplicationCount: 5,
|
||||||
|
totalAtRiskMemberCount: 2,
|
||||||
|
totalAtRiskApplicationCount: 1,
|
||||||
|
totalCriticalApplicationCount: 3,
|
||||||
|
totalCriticalMemberCount: 4,
|
||||||
|
totalCriticalAtRiskMemberCount: 1,
|
||||||
|
totalCriticalAtRiskApplicationCount: 1,
|
||||||
|
newApplications: ["app-1"],
|
||||||
|
extraField: "should be rejected",
|
||||||
|
};
|
||||||
|
expect(isOrganizationReportSummary(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isOrganizationReportApplication", () => {
|
||||||
|
it("should return true for valid OrganizationReportApplication", () => {
|
||||||
|
const validData = {
|
||||||
|
applicationName: "Test App",
|
||||||
|
isCritical: true,
|
||||||
|
reviewedDate: null as Date | null,
|
||||||
|
};
|
||||||
|
expect(isOrganizationReportApplication(validData)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for empty applicationName", () => {
|
||||||
|
const invalidData = {
|
||||||
|
applicationName: "",
|
||||||
|
isCritical: true,
|
||||||
|
reviewedDate: null as Date | null,
|
||||||
|
};
|
||||||
|
expect(isOrganizationReportApplication(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true for Date reviewedDate", () => {
|
||||||
|
const validData = {
|
||||||
|
applicationName: "Test App",
|
||||||
|
isCritical: true,
|
||||||
|
reviewedDate: new Date(),
|
||||||
|
};
|
||||||
|
expect(isOrganizationReportApplication(validData)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true for string reviewedDate", () => {
|
||||||
|
const validData = {
|
||||||
|
applicationName: "Test App",
|
||||||
|
isCritical: false,
|
||||||
|
reviewedDate: "2024-01-01",
|
||||||
|
};
|
||||||
|
expect(isOrganizationReportApplication(validData)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for objects with unexpected properties", () => {
|
||||||
|
const invalidData = {
|
||||||
|
applicationName: "Test App",
|
||||||
|
isCritical: true,
|
||||||
|
reviewedDate: null as Date | null,
|
||||||
|
injectedProperty: "malicious",
|
||||||
|
};
|
||||||
|
expect(isOrganizationReportApplication(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for prototype pollution attempts via __proto__", () => {
|
||||||
|
const invalidData = {
|
||||||
|
applicationName: "Test App",
|
||||||
|
isCritical: true,
|
||||||
|
reviewedDate: null as Date | null,
|
||||||
|
__proto__: { polluted: true },
|
||||||
|
};
|
||||||
|
expect(isOrganizationReportApplication(invalidData)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
import {
|
||||||
|
ApplicationHealthReportDetail,
|
||||||
|
MemberDetails,
|
||||||
|
OrganizationReportApplication,
|
||||||
|
OrganizationReportSummary,
|
||||||
|
} from "../../models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security limits for validation (prevent DoS attacks and ensure reasonable data sizes)
|
||||||
|
*/
|
||||||
|
const MAX_STRING_LENGTH = 1000; // Reasonable limit for names, emails, GUIDs
|
||||||
|
const MAX_ARRAY_LENGTH = 50000; // Reasonable limit for report arrays
|
||||||
|
const MAX_COUNT = 10000000; // 10 million - reasonable upper bound for count fields
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to validate MemberDetails structure
|
||||||
|
* Exported for testability
|
||||||
|
* Strict validation: rejects objects with unexpected properties and prototype pollution
|
||||||
|
*/
|
||||||
|
export function isMemberDetails(obj: any): obj is MemberDetails {
|
||||||
|
if (typeof obj !== "object" || obj === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent prototype pollution - check prototype is Object.prototype
|
||||||
|
if (Object.getPrototypeOf(obj) !== Object.prototype) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent dangerous properties that could be used for prototype pollution
|
||||||
|
// Check for __proto__, constructor, and prototype as own properties
|
||||||
|
const dangerousKeys = ["__proto__", "constructor", "prototype"];
|
||||||
|
for (const key of dangerousKeys) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strict property validation - reject unexpected properties
|
||||||
|
const allowedKeys = ["userGuid", "userName", "email", "cipherId"];
|
||||||
|
const actualKeys = Object.keys(obj);
|
||||||
|
const hasUnexpectedProps = actualKeys.some((key) => !allowedKeys.includes(key));
|
||||||
|
if (hasUnexpectedProps) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
typeof obj.userGuid === "string" &&
|
||||||
|
obj.userGuid.length > 0 &&
|
||||||
|
obj.userGuid.length <= MAX_STRING_LENGTH &&
|
||||||
|
typeof obj.userName === "string" &&
|
||||||
|
obj.userName.length > 0 &&
|
||||||
|
obj.userName.length <= MAX_STRING_LENGTH &&
|
||||||
|
typeof obj.email === "string" &&
|
||||||
|
obj.email.length > 0 &&
|
||||||
|
obj.email.length <= MAX_STRING_LENGTH &&
|
||||||
|
typeof obj.cipherId === "string" &&
|
||||||
|
obj.cipherId.length > 0 &&
|
||||||
|
obj.cipherId.length <= MAX_STRING_LENGTH
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to validate ApplicationHealthReportDetail structure
|
||||||
|
* Exported for testability
|
||||||
|
* Strict validation: rejects objects with unexpected properties and prototype pollution
|
||||||
|
*/
|
||||||
|
export function isApplicationHealthReportDetail(obj: any): obj is ApplicationHealthReportDetail {
|
||||||
|
if (typeof obj !== "object" || obj === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent prototype pollution - check prototype is Object.prototype
|
||||||
|
if (Object.getPrototypeOf(obj) !== Object.prototype) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent dangerous properties that could be used for prototype pollution
|
||||||
|
// Check for __proto__, constructor, and prototype as own properties
|
||||||
|
const dangerousKeys = ["__proto__", "constructor", "prototype"];
|
||||||
|
for (const key of dangerousKeys) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strict property validation - reject unexpected properties
|
||||||
|
const allowedKeys = [
|
||||||
|
"applicationName",
|
||||||
|
"passwordCount",
|
||||||
|
"atRiskPasswordCount",
|
||||||
|
"atRiskCipherIds",
|
||||||
|
"memberCount",
|
||||||
|
"atRiskMemberCount",
|
||||||
|
"memberDetails",
|
||||||
|
"atRiskMemberDetails",
|
||||||
|
"cipherIds",
|
||||||
|
];
|
||||||
|
const actualKeys = Object.keys(obj);
|
||||||
|
const hasUnexpectedProps = actualKeys.some((key) => !allowedKeys.includes(key));
|
||||||
|
if (hasUnexpectedProps) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
typeof obj.applicationName === "string" &&
|
||||||
|
obj.applicationName.length > 0 &&
|
||||||
|
obj.applicationName.length <= MAX_STRING_LENGTH &&
|
||||||
|
typeof obj.passwordCount === "number" &&
|
||||||
|
Number.isFinite(obj.passwordCount) &&
|
||||||
|
Number.isSafeInteger(obj.passwordCount) &&
|
||||||
|
obj.passwordCount >= 0 &&
|
||||||
|
obj.passwordCount <= MAX_COUNT &&
|
||||||
|
typeof obj.atRiskPasswordCount === "number" &&
|
||||||
|
Number.isFinite(obj.atRiskPasswordCount) &&
|
||||||
|
Number.isSafeInteger(obj.atRiskPasswordCount) &&
|
||||||
|
obj.atRiskPasswordCount >= 0 &&
|
||||||
|
obj.atRiskPasswordCount <= MAX_COUNT &&
|
||||||
|
Array.isArray(obj.atRiskCipherIds) &&
|
||||||
|
obj.atRiskCipherIds.length <= MAX_ARRAY_LENGTH &&
|
||||||
|
obj.atRiskCipherIds.every(
|
||||||
|
(id: any) => typeof id === "string" && id.length > 0 && id.length <= MAX_STRING_LENGTH,
|
||||||
|
) &&
|
||||||
|
typeof obj.memberCount === "number" &&
|
||||||
|
Number.isFinite(obj.memberCount) &&
|
||||||
|
Number.isSafeInteger(obj.memberCount) &&
|
||||||
|
obj.memberCount >= 0 &&
|
||||||
|
obj.memberCount <= MAX_COUNT &&
|
||||||
|
typeof obj.atRiskMemberCount === "number" &&
|
||||||
|
Number.isFinite(obj.atRiskMemberCount) &&
|
||||||
|
Number.isSafeInteger(obj.atRiskMemberCount) &&
|
||||||
|
obj.atRiskMemberCount >= 0 &&
|
||||||
|
obj.atRiskMemberCount <= MAX_COUNT &&
|
||||||
|
Array.isArray(obj.memberDetails) &&
|
||||||
|
obj.memberDetails.length <= MAX_ARRAY_LENGTH &&
|
||||||
|
obj.memberDetails.every(isMemberDetails) &&
|
||||||
|
Array.isArray(obj.atRiskMemberDetails) &&
|
||||||
|
obj.atRiskMemberDetails.length <= MAX_ARRAY_LENGTH &&
|
||||||
|
obj.atRiskMemberDetails.every(isMemberDetails) &&
|
||||||
|
Array.isArray(obj.cipherIds) &&
|
||||||
|
obj.cipherIds.length <= MAX_ARRAY_LENGTH &&
|
||||||
|
obj.cipherIds.every(
|
||||||
|
(id: any) => typeof id === "string" && id.length > 0 && id.length <= MAX_STRING_LENGTH,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to validate OrganizationReportSummary structure
|
||||||
|
* Exported for testability
|
||||||
|
* Strict validation: rejects objects with unexpected properties and prototype pollution
|
||||||
|
*/
|
||||||
|
export function isOrganizationReportSummary(obj: any): obj is OrganizationReportSummary {
|
||||||
|
if (typeof obj !== "object" || obj === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent prototype pollution - check prototype is Object.prototype
|
||||||
|
if (Object.getPrototypeOf(obj) !== Object.prototype) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent dangerous properties that could be used for prototype pollution
|
||||||
|
// Check for __proto__, constructor, and prototype as own properties
|
||||||
|
const dangerousKeys = ["__proto__", "constructor", "prototype"];
|
||||||
|
for (const key of dangerousKeys) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strict property validation - reject unexpected properties
|
||||||
|
const allowedKeys = [
|
||||||
|
"totalMemberCount",
|
||||||
|
"totalApplicationCount",
|
||||||
|
"totalAtRiskMemberCount",
|
||||||
|
"totalAtRiskApplicationCount",
|
||||||
|
"totalCriticalApplicationCount",
|
||||||
|
"totalCriticalMemberCount",
|
||||||
|
"totalCriticalAtRiskMemberCount",
|
||||||
|
"totalCriticalAtRiskApplicationCount",
|
||||||
|
"newApplications",
|
||||||
|
];
|
||||||
|
const actualKeys = Object.keys(obj);
|
||||||
|
const hasUnexpectedProps = actualKeys.some((key) => !allowedKeys.includes(key));
|
||||||
|
if (hasUnexpectedProps) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
typeof obj.totalMemberCount === "number" &&
|
||||||
|
Number.isFinite(obj.totalMemberCount) &&
|
||||||
|
Number.isSafeInteger(obj.totalMemberCount) &&
|
||||||
|
obj.totalMemberCount >= 0 &&
|
||||||
|
obj.totalMemberCount <= MAX_COUNT &&
|
||||||
|
typeof obj.totalApplicationCount === "number" &&
|
||||||
|
Number.isFinite(obj.totalApplicationCount) &&
|
||||||
|
Number.isSafeInteger(obj.totalApplicationCount) &&
|
||||||
|
obj.totalApplicationCount >= 0 &&
|
||||||
|
obj.totalApplicationCount <= MAX_COUNT &&
|
||||||
|
typeof obj.totalAtRiskMemberCount === "number" &&
|
||||||
|
Number.isFinite(obj.totalAtRiskMemberCount) &&
|
||||||
|
Number.isSafeInteger(obj.totalAtRiskMemberCount) &&
|
||||||
|
obj.totalAtRiskMemberCount >= 0 &&
|
||||||
|
obj.totalAtRiskMemberCount <= MAX_COUNT &&
|
||||||
|
typeof obj.totalAtRiskApplicationCount === "number" &&
|
||||||
|
Number.isFinite(obj.totalAtRiskApplicationCount) &&
|
||||||
|
Number.isSafeInteger(obj.totalAtRiskApplicationCount) &&
|
||||||
|
obj.totalAtRiskApplicationCount >= 0 &&
|
||||||
|
obj.totalAtRiskApplicationCount <= MAX_COUNT &&
|
||||||
|
typeof obj.totalCriticalApplicationCount === "number" &&
|
||||||
|
Number.isFinite(obj.totalCriticalApplicationCount) &&
|
||||||
|
Number.isSafeInteger(obj.totalCriticalApplicationCount) &&
|
||||||
|
obj.totalCriticalApplicationCount >= 0 &&
|
||||||
|
obj.totalCriticalApplicationCount <= MAX_COUNT &&
|
||||||
|
typeof obj.totalCriticalMemberCount === "number" &&
|
||||||
|
Number.isFinite(obj.totalCriticalMemberCount) &&
|
||||||
|
Number.isSafeInteger(obj.totalCriticalMemberCount) &&
|
||||||
|
obj.totalCriticalMemberCount >= 0 &&
|
||||||
|
obj.totalCriticalMemberCount <= MAX_COUNT &&
|
||||||
|
typeof obj.totalCriticalAtRiskMemberCount === "number" &&
|
||||||
|
Number.isFinite(obj.totalCriticalAtRiskMemberCount) &&
|
||||||
|
Number.isSafeInteger(obj.totalCriticalAtRiskMemberCount) &&
|
||||||
|
obj.totalCriticalAtRiskMemberCount >= 0 &&
|
||||||
|
obj.totalCriticalAtRiskMemberCount <= MAX_COUNT &&
|
||||||
|
typeof obj.totalCriticalAtRiskApplicationCount === "number" &&
|
||||||
|
Number.isFinite(obj.totalCriticalAtRiskApplicationCount) &&
|
||||||
|
Number.isSafeInteger(obj.totalCriticalAtRiskApplicationCount) &&
|
||||||
|
obj.totalCriticalAtRiskApplicationCount >= 0 &&
|
||||||
|
obj.totalCriticalAtRiskApplicationCount <= MAX_COUNT &&
|
||||||
|
Array.isArray(obj.newApplications) &&
|
||||||
|
obj.newApplications.length <= MAX_ARRAY_LENGTH &&
|
||||||
|
obj.newApplications.every(
|
||||||
|
(app: any) => typeof app === "string" && app.length > 0 && app.length <= MAX_STRING_LENGTH,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to validate OrganizationReportApplication structure
|
||||||
|
* Exported for testability
|
||||||
|
* Strict validation: rejects objects with unexpected properties and prototype pollution
|
||||||
|
*/
|
||||||
|
export function isOrganizationReportApplication(obj: any): obj is OrganizationReportApplication {
|
||||||
|
if (typeof obj !== "object" || obj === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent prototype pollution - check prototype is Object.prototype
|
||||||
|
if (Object.getPrototypeOf(obj) !== Object.prototype) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent dangerous properties that could be used for prototype pollution
|
||||||
|
// Check for __proto__, constructor, and prototype as own properties
|
||||||
|
const dangerousKeys = ["__proto__", "constructor", "prototype"];
|
||||||
|
for (const key of dangerousKeys) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strict property validation - reject unexpected properties
|
||||||
|
const allowedKeys = ["applicationName", "isCritical", "reviewedDate"];
|
||||||
|
const actualKeys = Object.keys(obj);
|
||||||
|
const hasUnexpectedProps = actualKeys.some((key) => !allowedKeys.includes(key));
|
||||||
|
if (hasUnexpectedProps) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
typeof obj.applicationName === "string" &&
|
||||||
|
obj.applicationName.length > 0 &&
|
||||||
|
obj.applicationName.length <= MAX_STRING_LENGTH &&
|
||||||
|
typeof obj.isCritical === "boolean" &&
|
||||||
|
(obj.reviewedDate === null ||
|
||||||
|
obj.reviewedDate instanceof Date ||
|
||||||
|
typeof obj.reviewedDate === "string")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and returns an array of ApplicationHealthReportDetail
|
||||||
|
* @throws Error if validation fails
|
||||||
|
*/
|
||||||
|
export function validateApplicationHealthReportDetailArray(
|
||||||
|
data: any,
|
||||||
|
): ApplicationHealthReportDetail[] {
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid report data: expected array of ApplicationHealthReportDetail, received non-array",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length > MAX_ARRAY_LENGTH) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid report data: array length ${data.length} exceeds maximum allowed length ${MAX_ARRAY_LENGTH}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidItems = data
|
||||||
|
.map((item, index) => ({ item, index }))
|
||||||
|
.filter(({ item }) => !isApplicationHealthReportDetail(item));
|
||||||
|
|
||||||
|
if (invalidItems.length > 0) {
|
||||||
|
const invalidIndices = invalidItems.map(({ index }) => index).join(", ");
|
||||||
|
throw new Error(
|
||||||
|
`Invalid report data: array contains ${invalidItems.length} invalid ApplicationHealthReportDetail element(s) at indices: ${invalidIndices}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as ApplicationHealthReportDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and returns OrganizationReportSummary
|
||||||
|
* @throws Error if validation fails
|
||||||
|
*/
|
||||||
|
export function validateOrganizationReportSummary(data: any): OrganizationReportSummary {
|
||||||
|
if (!isOrganizationReportSummary(data)) {
|
||||||
|
const missingFields: string[] = [];
|
||||||
|
|
||||||
|
if (typeof data?.totalMemberCount !== "number") {
|
||||||
|
missingFields.push("totalMemberCount (number)");
|
||||||
|
}
|
||||||
|
if (typeof data?.totalApplicationCount !== "number") {
|
||||||
|
missingFields.push("totalApplicationCount (number)");
|
||||||
|
}
|
||||||
|
if (typeof data?.totalAtRiskMemberCount !== "number") {
|
||||||
|
missingFields.push("totalAtRiskMemberCount (number)");
|
||||||
|
}
|
||||||
|
if (typeof data?.totalAtRiskApplicationCount !== "number") {
|
||||||
|
missingFields.push("totalAtRiskApplicationCount (number)");
|
||||||
|
}
|
||||||
|
if (typeof data?.totalCriticalApplicationCount !== "number") {
|
||||||
|
missingFields.push("totalCriticalApplicationCount (number)");
|
||||||
|
}
|
||||||
|
if (typeof data?.totalCriticalMemberCount !== "number") {
|
||||||
|
missingFields.push("totalCriticalMemberCount (number)");
|
||||||
|
}
|
||||||
|
if (typeof data?.totalCriticalAtRiskMemberCount !== "number") {
|
||||||
|
missingFields.push("totalCriticalAtRiskMemberCount (number)");
|
||||||
|
}
|
||||||
|
if (typeof data?.totalCriticalAtRiskApplicationCount !== "number") {
|
||||||
|
missingFields.push("totalCriticalAtRiskApplicationCount (number)");
|
||||||
|
}
|
||||||
|
if (!Array.isArray(data?.newApplications)) {
|
||||||
|
missingFields.push("newApplications (string[])");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Invalid OrganizationReportSummary: ${missingFields.length > 0 ? `missing or invalid fields: ${missingFields.join(", ")}` : "structure validation failed"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as OrganizationReportSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and returns an array of OrganizationReportApplication
|
||||||
|
* @throws Error if validation fails
|
||||||
|
*/
|
||||||
|
export function validateOrganizationReportApplicationArray(
|
||||||
|
data: any,
|
||||||
|
): OrganizationReportApplication[] {
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error(
|
||||||
|
"Invalid application data: expected array of OrganizationReportApplication, received non-array",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length > MAX_ARRAY_LENGTH) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid application data: array length ${data.length} exceeds maximum allowed length ${MAX_ARRAY_LENGTH}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidItems = data
|
||||||
|
.map((item, index) => ({ item, index }))
|
||||||
|
.filter(({ item }) => !isOrganizationReportApplication(item));
|
||||||
|
|
||||||
|
if (invalidItems.length > 0) {
|
||||||
|
const invalidIndices = invalidItems.map(({ index }) => index).join(", ");
|
||||||
|
throw new Error(
|
||||||
|
`Invalid application data: array contains ${invalidItems.length} invalid OrganizationReportApplication element(s) at indices: ${invalidIndices}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert string dates to Date objects for reviewedDate
|
||||||
|
return data.map((item) => ({
|
||||||
|
...item,
|
||||||
|
reviewedDate: item.reviewedDate
|
||||||
|
? item.reviewedDate instanceof Date
|
||||||
|
? item.reviewedDate
|
||||||
|
: (() => {
|
||||||
|
const date = new Date(item.reviewedDate);
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
throw new Error(`Invalid date string: ${item.reviewedDate}`);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
})()
|
||||||
|
: null,
|
||||||
|
})) as OrganizationReportApplication[];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user