@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.
297 lines (261 loc) • 9.86 kB
text/typescript
import { AsyncFileManager } from "../../../fileManager/asyncFileManager.js";
import SecretFileManager from "./secretFileManager.js";
import SecretAuditManager from "./secretAuditManager.js";
import * as path from "path";
import KeyExpirationCalculator from "../utils/keyExpirationCalculator.js";
import SystemInfo from "../../../shared/systemInfo.js";
import CryptoConstants from "../types/cryptoConstants.js";
import type { SecretKeyMetadata, KeyMetadataFile } from "../types/metadata.types.js";
import ErrorHandler from "../../../errorHandling/errorHandler.js";
import logger from "../../../logger/loggerManager.js";
export default class SecretKeyMetadataManager {
public static async trackSecretKey(
keyName: string,
environment: string,
options: {
rotationDays?: number;
isRotation?: boolean;
algorithm?: string;
keyLength?: number;
performedBy?: string;
} = {},
): Promise<void> {
try {
const {
rotationDays = CryptoConstants.DEFAULT_ROTATION_DAYS,
isRotation = false,
algorithm = "base64",
keyLength = 256,
performedBy = SystemInfo.getCurrentUsername(),
} = options;
await this.ensureTrackingDirectoryExists();
const metadataFile = await this.loadKeyMetadata();
const now = new Date().toISOString();
const expiresAt = KeyExpirationCalculator.calculateExpirationDate(rotationDays);
const existingMetadata = metadataFile.keys[keyName];
const rotationCount = isRotation && existingMetadata ? existingMetadata.rotationCount + 1 : 0;
const metadata: SecretKeyMetadata = {
keyName,
environment,
createdAt: isRotation && existingMetadata ? existingMetadata.createdAt : now,
expiresAt,
rotationDays,
lastRotatedAt: isRotation ? now : undefined,
rotationCount,
status: "active",
algorithm,
keyLength,
performedBy,
};
metadataFile.keys[keyName] = metadata;
metadataFile.lastUpdated = now;
await this.saveKeyMetadata(metadataFile);
await SecretAuditManager.logAudit({
action: isRotation ? "rotate" : "create",
keyName,
environment,
status: "success",
details: isRotation ? "Key rotated successfully" : "Key created and tracked",
performedBy,
});
logger.info(
`Secret key "${keyName}" tracked successfully for environment "${environment}" by ${performedBy}` +
(isRotation ? ` (Rotation #${rotationCount})` : ""),
);
} catch (error) {
ErrorHandler.captureError(error, "trackSecretKey", `Failed to track secret key "${keyName}"`);
throw error;
}
}
/**
* Gets metadata for a specific secret key.
*/
public static async getKeyMetadata(keyName: string): Promise<SecretKeyMetadata | undefined> {
try {
const metadataFile = await this.loadKeyMetadata();
const metadata = metadataFile.keys[keyName];
if (metadata) {
metadata.status = KeyExpirationCalculator.determineKeyStatus(metadata.expiresAt);
}
return metadata;
} catch (error) {
ErrorHandler.captureError(
error,
"getKeyMetadata",
`Failed to get metadata for key "${keyName}"`,
);
return undefined;
}
}
/**
* Checks if a secret key needs rotation.
*/
public static async checkKeyRotationStatus(
keyName: string,
): Promise<{ needsRotation: boolean; daysUntilExpiration: number; status: string }> {
try {
const metadata = await this.getKeyMetadata(keyName);
if (!metadata) {
logger.warn(`No tracking data found for key "${keyName}"`);
return { needsRotation: true, daysUntilExpiration: 0, status: "unknown" };
}
const daysUntilExpiration = KeyExpirationCalculator.calculateDaysUntilExpiration(
metadata.expiresAt,
);
const needsRotation = daysUntilExpiration <= 0;
const status = metadata.status;
if (needsRotation) {
logger.warn(
`Secret key "${keyName}" has expired and needs rotation (expired ${Math.abs(daysUntilExpiration)} days ago)`,
);
} else if (status === "expiring_soon") {
logger.info(
`Secret key "${keyName}" is expiring soon (${daysUntilExpiration} days remaining)`,
);
}
return { needsRotation, daysUntilExpiration, status };
} catch (error) {
ErrorHandler.captureError(
error,
"checkKeyRotationStatus",
`Failed to check rotation status for key "${keyName}"`,
);
throw error;
}
}
/**
* Gets all secret keys that need rotation.
*/
public static async getKeysNeedingRotation(): Promise<SecretKeyMetadata[]> {
try {
const metadataFile = await this.loadKeyMetadata();
const keysNeedingRotation: SecretKeyMetadata[] = [];
for (const metadata of Object.values(metadataFile.keys)) {
const daysUntilExpiration = KeyExpirationCalculator.calculateDaysUntilExpiration(
metadata.expiresAt,
);
if (daysUntilExpiration <= 0) {
metadata.status = "expired";
keysNeedingRotation.push(metadata);
}
}
if (keysNeedingRotation.length > 0) {
logger.warn(`Found ${keysNeedingRotation.length} key(s) that need rotation`);
}
return keysNeedingRotation;
} catch (error) {
ErrorHandler.captureError(
error,
"getKeysNeedingRotation",
"Failed to get keys needing rotation",
);
throw error;
}
}
/**
* Gets all secret keys that are expiring soon.
*/
public static async getKeysExpiringSoon(
thresholdDays: number = KeyExpirationCalculator.EXPIRING_SOON_THRESHOLD_DAYS,
): Promise<SecretKeyMetadata[]> {
try {
const metadataFile = await this.loadKeyMetadata();
const keysExpiringSoon: SecretKeyMetadata[] = [];
for (const metadata of Object.values(metadataFile.keys)) {
const daysUntilExpiration = KeyExpirationCalculator.calculateDaysUntilExpiration(
metadata.expiresAt,
);
if (daysUntilExpiration > 0 && daysUntilExpiration <= thresholdDays) {
metadata.status = "expiring_soon";
keysExpiringSoon.push(metadata);
}
}
return keysExpiringSoon;
} catch (error) {
ErrorHandler.captureError(error, "getKeysExpiringSoon", "Failed to get keys expiring soon");
throw error;
}
}
/**
* Gets all tracked secret keys.
*/
public static async getAllTrackedKeys(): Promise<SecretKeyMetadata[]> {
try {
const metadataFile = await this.loadKeyMetadata();
return Object.values(metadataFile.keys).map((metadata) => {
metadata.status = KeyExpirationCalculator.determineKeyStatus(metadata.expiresAt);
return metadata;
});
} catch (error) {
ErrorHandler.captureError(error, "getAllTrackedKeys", "Failed to get all tracked keys");
throw error;
}
}
public static async untrackSecretKey(keyName: string, performedBy: string): Promise<void> {
try {
const metadataFile = await this.loadKeyMetadata();
if (!metadataFile.keys[keyName]) {
logger.warn(`Key "${keyName}" is not currently tracked (attempted by ${performedBy})`);
await SecretAuditManager.logAudit({
action: "delete",
keyName,
environment: "unknown",
status: "warning",
details: "Attempted to untrack non-existent key",
performedBy,
});
return;
}
const deletedKey = metadataFile.keys[keyName];
delete metadataFile.keys[keyName];
metadataFile.lastUpdated = new Date().toISOString();
await this.saveKeyMetadata(metadataFile);
logger.info(
`Secret key "${keyName}" removed from tracking by ${performedBy} (Environment: ${deletedKey.environment})`,
);
await SecretAuditManager.logAudit({
action: "delete",
keyName,
environment: deletedKey.environment,
status: "success",
details: "Key removed from tracking",
performedBy,
});
} catch (error) {
ErrorHandler.captureError(
error,
"untrackSecretKey",
`Failed to untrack secret key "${keyName}" by ${performedBy}`,
);
await SecretAuditManager.logAudit({
action: "delete",
keyName,
environment: "unknown",
status: "failure",
details: "Error occurred while untracking key",
performedBy,
});
throw error;
}
}
// Private file operations
private static async ensureTrackingDirectoryExists(): Promise<void> {
const dirPath = path.join(process.cwd(), CryptoConstants.TRACKING_DIR);
const dirExists = await AsyncFileManager.doesFileExist(dirPath);
if (!dirExists) {
logger.info(`Creating tracking directory at "${dirPath}"`);
await AsyncFileManager.createDirectory(dirPath);
}
}
private static async loadKeyMetadata(): Promise<KeyMetadataFile> {
const filePath = SecretFileManager.getFilePath(CryptoConstants.METADATA_FILE);
return SecretFileManager.loadJsonFile<KeyMetadataFile>(filePath, {
keys: {},
lastUpdated: new Date().toISOString(),
});
}
private static async saveKeyMetadata(data: KeyMetadataFile): Promise<void> {
const filePath = SecretFileManager.getFilePath(CryptoConstants.METADATA_FILE);
await SecretFileManager.saveJsonFile(filePath, data);
}
}