@tshifhiwa/ohrm-ui-automation-framework
Version:
Playwright and TypeScript–based test automation framework for validating core UI features and workflows of the OrangeHRM demo application.
413 lines (359 loc) • 13.4 kB
text/typescript
import EnvironmentDetector from "../../environment/detector/environmentDetector.js";
import SecretFileManager from "../rotation/manager/secretFileManager.js";
import EnvironmentConfigManager from "../../environment/manager/handlers/environmentConfigManager.js";
import SecretMetadataManager from "../rotation/manager/secretMetadataManager.js";
import SecretKeyRotationManager from "../rotation/manager/secretKeyRotationManager.js";
import SystemInfo from "../../shared/systemInfo.js";
import CryptoConstants from "../rotation/types/cryptoConstants.js";
import type {
RotationOptions,
RotationResult,
SecretKeyRotationEntry,
SecretKeyRotationFile,
RotationHistoryEntry,
RotationStatusResult,
} from "../rotation/types/rotation.types.js";
import ErrorHandler from "../../errorHandling/errorHandler.js";
import logger from "../../logger/loggerManager.js";
export default class RotationOrchestrator {
/**
* Performs complete key rotation with decryption and re-encryption.
*/
public static async rotateKeyWithReEncryption(
options: RotationOptions = {},
): Promise<RotationResult> {
const startTime = Date.now();
const {
rotationReason = "scheduled",
rotationDays = 90,
performedBy = SystemInfo.getCurrentUsername(),
forceRotation = false,
dryRun = false,
} = options;
const currentEnvKey = EnvironmentConfigManager.getCurrentEnvSecretKey();
const currentEnv = EnvironmentDetector.getCurrentEnvironmentStage();
const filePath = EnvironmentConfigManager.getCurrentEnvFilePath();
logger.info(`${dryRun ? "[DRY RUN] " : ""}Starting key rotation for "${currentEnvKey}"...`);
try {
// Step 1: Validate rotation is needed
await SecretKeyRotationManager.validateRotationNecessary(currentEnvKey, forceRotation);
// Step 2: Get old key before rotation
const oldKey = await SecretKeyRotationManager.getOldSecretKey(currentEnvKey);
const oldKeyHash = SecretKeyRotationManager.hashKey(oldKey);
// Step 3: Decrypt all encrypted variables with old key
const decryptedVariables = await SecretKeyRotationManager.decryptAllEnvironmentVariables(
filePath,
currentEnvKey,
oldKey,
);
if (dryRun) {
logger.info(
`[DRY RUN] Would decrypt and re-encrypt ${decryptedVariables.filter((v) => v.wasEncrypted).length} variables`,
);
return SecretKeyRotationManager.createDryRunResult(
currentEnvKey,
currentEnv,
decryptedVariables,
startTime,
);
}
// Step 4: Generate new key
const newKey = await SecretKeyRotationManager.generateNewSecretKey();
const newKeyHash = SecretKeyRotationManager.hashKey(newKey);
// Step 5: Store new key
await SecretKeyRotationManager.storeNewSecretKey(currentEnvKey, newKey);
// Step 6: Re-encrypt all variables with new key
const { variablesProcessed, variablesFailed } =
await SecretKeyRotationManager.reEncryptAllVariables(
filePath,
decryptedVariables,
currentEnvKey,
newKey,
);
// Step 7: Update tracking metadata
await this.updateRotationTracking(
currentEnvKey,
currentEnv,
oldKeyHash,
newKeyHash,
rotationReason,
rotationDays,
performedBy,
true,
);
const duration = Date.now() - startTime;
logger.info(
`Key rotation completed successfully for "${currentEnvKey}" ` +
`(${variablesProcessed} variables re-encrypted in ${duration}ms)`,
);
return {
success: true,
keyName: currentEnvKey,
environment: currentEnv,
variablesProcessed,
variablesFailed,
oldKeyHash,
newKeyHash,
duration,
};
} catch (error) {
const duration = Date.now() - startTime;
// Record failed rotation
await this.updateRotationTracking(
currentEnvKey,
currentEnv,
undefined,
undefined,
rotationReason,
rotationDays,
performedBy,
false,
);
ErrorHandler.captureError(
error,
"rotateKeyWithReEncryption",
`Failed to rotate key "${currentEnvKey}"`,
);
return {
success: false,
keyName: currentEnvKey,
environment: currentEnv,
variablesProcessed: 0,
variablesFailed: [],
duration,
};
}
}
/**
* Rotates all expired keys across all environments.
*/
public static async rotateAllExpiredKeys(
options: {
performedBy?: string;
dryRun?: boolean;
} = {},
): Promise<RotationResult[]> {
try {
const { performedBy = SystemInfo.getCurrentUsername(), dryRun = false } = options;
logger.info(`${dryRun ? "[DRY RUN] " : ""}Checking for expired keys...`);
const expiredKeys = await SecretMetadataManager.getKeysNeedingRotation();
if (expiredKeys.length === 0) {
logger.info("No expired keys found.");
return [];
}
logger.warn(`Found ${expiredKeys.length} expired key(s) that need rotation`);
const results: RotationResult[] = [];
for (const keyMetadata of expiredKeys) {
logger.info(`Processing expired key: ${keyMetadata.keyName} (${keyMetadata.environment})`);
const result = await this.rotateKeyWithReEncryption({
rotationReason: "expired",
performedBy,
forceRotation: true,
dryRun,
});
results.push(result);
}
const successCount = results.filter((r) => r.success).length;
logger.info(
`${dryRun ? "[DRY RUN] " : ""}Batch rotation complete: ${successCount}/${results.length} successful`,
);
return results;
} catch (error) {
ErrorHandler.captureError(error, "rotateAllExpiredKeys", "Failed to rotate expired keys");
throw error;
}
}
// ==================== PUBLIC STATUS & AUDIT OPERATIONS ====================
/**
* Checks if key rotation is needed and returns recommendation.
*/
public static async checkRotationStatus(): Promise<RotationStatusResult> {
try {
const currentEnvKey = EnvironmentConfigManager.getCurrentEnvSecretKey();
const filePath = EnvironmentConfigManager.getCurrentEnvFilePath();
// Check expiration status
const rotationStatus = await SecretMetadataManager.checkKeyRotationStatus(currentEnvKey);
const metadata = await SecretMetadataManager.getKeyMetadata(currentEnvKey);
// Count encrypted variables
const encryptedCount = await SecretKeyRotationManager.countEncryptedVariables(filePath);
let recommendation = "";
if (rotationStatus.needsRotation) {
recommendation = `🔴 URGENT: Key expired ${Math.abs(rotationStatus.daysUntilExpiration)} days ago. Rotate immediately.`;
} else if (rotationStatus.status === "expiring_soon") {
recommendation = `🟡 WARNING: Key expires in ${rotationStatus.daysUntilExpiration} days. Plan rotation soon.`;
} else {
recommendation = `🟢 OK: Key is valid for ${rotationStatus.daysUntilExpiration} more days.`;
}
return {
needsRotation: rotationStatus.needsRotation,
recommendation,
details: {
daysUntilExpiration: rotationStatus.daysUntilExpiration,
status: rotationStatus.status,
encryptedVariableCount: encryptedCount,
metadata: metadata
? {
createdAt: metadata.createdAt,
rotationCount: metadata.rotationCount,
lastRotatedAt: metadata.lastRotatedAt,
}
: undefined,
},
};
} catch (error) {
ErrorHandler.captureError(error, "checkRotationStatus", "Failed to check rotation status");
throw error;
}
}
/**
* Checks all tracked keys and reports their rotation status.
*/
public static async auditAllSecretKeys(): Promise<void> {
try {
await SecretKeyRotationManager.auditAllSecretKeys();
} catch (error) {
ErrorHandler.captureError(error, "auditAllSecretKeys", "Failed to audit secret keys");
throw error;
}
}
// ==================== PUBLIC HISTORY OPERATIONS ====================
/**
* Gets rotation history for the current secret key.
*/
public static async getRotationHistory(limit: number = 10): Promise<RotationHistoryEntry[]> {
try {
const currentEnvKey = EnvironmentConfigManager.getCurrentEnvSecretKey();
const history = await this.getKeyRotationHistory(currentEnvKey, limit);
return history.map((entry) => ({
keyName: entry.keyName,
rotationDate: entry.rotationDate,
rotationReason: entry.rotationReason,
performedBy: entry.performedBy,
success: entry.success,
}));
} catch (error) {
ErrorHandler.captureError(error, "getRotationHistory", "Failed to get rotation history");
throw error;
}
}
/**
* Gets rotation history for a specific key.
*/
public static async getKeyRotationHistory(
keyName: string,
limit?: number,
): Promise<SecretKeyRotationEntry[]> {
try {
const rotationFile = await this.loadRotationHistory();
const keyRotations = rotationFile.rotations.filter((r) => r.keyName === keyName);
return limit ? keyRotations.slice(0, limit) : keyRotations;
} catch (error) {
ErrorHandler.captureError(
error,
"getKeyRotationHistory",
`Failed to get rotation history for key "${keyName}"`,
);
return [];
}
}
/**
* Records a key rotation event in the rotation history.
*/
public static async recordRotation(
keyName: string,
environment: string,
options: {
rotationReason?: "scheduled" | "manual" | "compromised" | "expired";
previousKeyHash?: string;
newKeyHash?: string;
performedBy?: string;
success?: boolean;
} = {},
): Promise<void> {
try {
const {
rotationReason = "scheduled",
previousKeyHash,
newKeyHash,
performedBy = SystemInfo.getCurrentUsername(),
success = true,
} = options;
const rotationFile = await this.loadRotationHistory();
const now = new Date().toISOString();
const rotationEntry: SecretKeyRotationEntry = {
keyName,
environment,
rotationDate: now,
previousKeyHash,
newKeyHash,
rotationReason,
performedBy,
success,
};
rotationFile.rotations.unshift(rotationEntry);
rotationFile.lastRotation = now;
await this.saveRotationHistory(rotationFile);
logger.info(
`Rotation recorded for key "${keyName}" - Reason: ${rotationReason}, Success: ${success}`,
);
} catch (error) {
ErrorHandler.captureError(
error,
"recordRotation",
`Failed to record rotation for key "${keyName}"`,
);
throw error;
}
}
// ==================== PRIVATE TRACKING ====================
/**
* Updates rotation tracking metadata.
*/
private static async updateRotationTracking(
keyName: string,
environment: string,
oldKeyHash: string | undefined,
newKeyHash: string | undefined,
rotationReason: "scheduled" | "manual" | "compromised" | "expired",
rotationDays: number,
performedBy: string,
success: boolean,
): Promise<void> {
try {
await SecretMetadataManager.trackSecretKey(keyName, environment, {
rotationDays,
isRotation: true,
algorithm: "base64",
keyLength: 256,
performedBy,
});
await this.recordRotation(keyName, environment, {
rotationReason,
...(oldKeyHash !== undefined && { previousKeyHash: oldKeyHash }),
...(newKeyHash !== undefined && { newKeyHash }),
performedBy,
success,
});
logger.debug(`Rotation tracking updated for "${keyName}"`);
} catch (error) {
ErrorHandler.captureError(
error,
"updateRotationTracking",
"Failed to update rotation tracking",
);
// Don't throw - tracking failure shouldn't break the rotation
}
}
// ==================== PRIVATE FILE OPERATIONS ====================
private static async loadRotationHistory(): Promise<SecretKeyRotationFile> {
const filePath = SecretFileManager.getFilePath(CryptoConstants.ROTATION_FILE);
return SecretFileManager.loadJsonFile<SecretKeyRotationFile>(filePath, {
rotations: [],
lastRotation: new Date().toISOString(),
});
}
private static async saveRotationHistory(data: SecretKeyRotationFile): Promise<void> {
const filePath = SecretFileManager.getFilePath(CryptoConstants.ROTATION_FILE);
await SecretFileManager.saveJsonFile(filePath, data);
}
}