token-guardian
Version:
A comprehensive solution for protecting and managing API tokens and secrets
364 lines (312 loc) • 12.3 kB
text/typescript
import crypto from 'crypto';
import { promises as fs } from 'fs';
import { GuardianConfig } from './interfaces/GuardianConfig';
import { TokenConfig } from './interfaces/TokenConfig';
import { ScanResult } from './interfaces/ScanResult';
import { RotationResult } from './interfaces/RotationResult';
import { PatternScanner } from './scanners/PatternScanner';
import { TokenRotator } from './rotation/TokenRotator';
import { CanaryService } from './canary/CanaryService';
import { TokenStore, AuditLogEntry } from './storage/TokenStore';
import { Logger, LogLevel } from './utils/Logger';
import { TokenPatterns } from './scanners/TokenPatterns';
/**
* TokenGuardian - Main class that provides token protection functionality
*/
export class TokenGuardian {
private readonly defaultRotationInterval = '30d';
private config: GuardianConfig & { logLevel: LogLevel };
private scanner: PatternScanner;
private rotator: TokenRotator;
private canaryService: CanaryService;
private tokenStore: TokenStore;
private logger: Logger;
private patterns: TokenPatterns[];
private rotationSchedules: Map<string, NodeJS.Timeout>;
/**
* Creates a new TokenGuardian instance
* @param config Configuration options for TokenGuardian
*/
constructor(config: Partial<GuardianConfig> = {}) {
const logLevel = config.logLevel || 'info';
if (logLevel !== 'debug' && logLevel !== 'info' && logLevel !== 'warn' && logLevel !== 'error') {
throw new Error('Invalid log level. Must be one of: debug, info, warn, error');
}
this.logger = new Logger(logLevel);
const rotationInterval = this.normalizeInterval(config.rotationInterval, this.defaultRotationInterval);
this.config = {
services: config.services || ['default'],
rotationInterval,
canaryEnabled: config.canaryEnabled !== undefined ? config.canaryEnabled : true,
encryptionKey: config.encryptionKey || this.generateEncryptionKey(),
logLevel
};
this.patterns = [new TokenPatterns()];
this.scanner = new PatternScanner(this.patterns);
this.tokenStore = new TokenStore(this.config.encryptionKey);
this.rotator = new TokenRotator();
this.canaryService = new CanaryService(this.config.canaryEnabled);
this.rotationSchedules = new Map();
this.logger.info('TokenGuardian initialized');
}
/**
* Generates a secure encryption key
* @returns A secure encryption key
*/
private generateEncryptionKey(): string {
return crypto.randomBytes(32).toString('hex');
}
/**
* Parses an interval string to milliseconds
* @param interval Interval string (e.g. '30d', '6h')
* @returns Milliseconds
*/
private parseIntervalToMs(interval: string): number {
const match = interval.trim().match(/^(\d+)([dhms])$/i);
if (!match || Number(match[1]) <= 0) {
const fallbackMs = this.parseIntervalToMs(this.defaultRotationInterval);
this.logger.warn(`Invalid rotation interval "${interval}" provided. Falling back to default ${this.defaultRotationInterval}`);
return fallbackMs;
}
const value = Number(match[1]);
const unit = match[2].toLowerCase();
const multipliers: Record<string, number> = {
d: 24 * 60 * 60 * 1000,
h: 60 * 60 * 1000,
m: 60 * 1000,
s: 1000
};
return value * multipliers[unit];
}
/**
* Normalizes interval strings to a validated format, falling back when invalid
* @param interval Interval string to normalize
* @param fallback Fallback interval if the provided one is invalid
* @returns Normalized interval string
*/
private normalizeInterval(interval?: string, fallback?: string): string {
const normalizedFallback = fallback && /^(\d+)([dhms])$/i.test(fallback)
? fallback
: this.defaultRotationInterval;
if (!interval) {
return normalizedFallback;
}
const trimmed = interval.trim();
const match = trimmed.match(/^(\d+)([dhms])$/i);
if (!match) {
this.logger.warn(`Invalid rotation interval "${interval}" provided. Using fallback ${normalizedFallback}`);
return normalizedFallback;
}
const value = Number(match[1]);
if (!Number.isFinite(value) || value <= 0) {
this.logger.warn(`Rotation interval must be greater than 0. Using fallback ${normalizedFallback}`);
return normalizedFallback;
}
return `${value}${match[2].toLowerCase()}`;
}
/**
* Schedules automatic rotation for a token
* @param tokenName The name/identifier of the token
* @param serviceType The type of service the token is for
* @param interval Rotation interval (e.g. '30d', '6h')
*/
private scheduleRotation(tokenName: string, serviceType: string, interval: string): void {
// Cancel any existing rotation schedule for this token
this.cancelRotation(tokenName);
// Parse the interval string to milliseconds
const intervalMs = this.parseIntervalToMs(interval);
// Schedule the rotation
const timer = setTimeout(async () => {
await this.rotateToken(tokenName);
// Reschedule after rotation
this.scheduleRotation(tokenName, serviceType, interval);
}, intervalMs);
this.rotationSchedules.set(tokenName, timer);
}
/**
* Cancels a scheduled rotation
* @param tokenName The name/identifier of the token
*/
private cancelRotation(tokenName: string): void {
this.stopRotation(tokenName);
}
/**
* Scans a string for potential tokens or secrets
* @param input The string to scan
* @returns Results of the scan
*/
public scanString(input: string): ScanResult[] {
this.logger.debug('Scanning string for sensitive data');
return this.scanner.scan(input, 'memory');
}
/**
* Protects a token by storing it securely and optionally enabling rotation and canary features
* @param tokenName A name/identifier for the token
* @param tokenValue The actual token value to protect
* @param tokenConfig Configuration options for this specific token
* @returns True if the token was successfully protected
*/
public protect(tokenName: string, tokenValue: string, tokenConfig: Partial<TokenConfig> = {}): boolean {
this.logger.info(`Protecting token: ${tokenName}`);
const config: TokenConfig = {
rotationEnabled: tokenConfig.rotationEnabled !== undefined ? tokenConfig.rotationEnabled : true,
rotationInterval: this.normalizeInterval(tokenConfig.rotationInterval, this.config.rotationInterval),
canaryEnabled: tokenConfig.canaryEnabled !== undefined ? tokenConfig.canaryEnabled : this.config.canaryEnabled,
serviceType: tokenConfig.serviceType || 'default',
};
// Validate the token format
const scanResults = this.scanString(tokenValue);
if (scanResults.length === 0 && config.serviceType === 'default') {
this.logger.warn(`Token ${tokenName} does not match any known patterns`);
}
// Add canary markers if enabled
let protectedValue = tokenValue;
if (config.canaryEnabled) {
protectedValue = this.canaryService.embedCanary(tokenValue, tokenName);
this.logger.debug(`Canary markers embedded in token ${tokenName}`);
}
// Store the token
const stored = this.tokenStore.storeToken(tokenName, protectedValue, config);
if (!stored) {
this.logger.error(`Failed to store token ${tokenName}`);
return false;
}
// Set up rotation if enabled
if (config.rotationEnabled) {
this.scheduleRotation(tokenName, config.serviceType, config.rotationInterval);
this.logger.debug(`Rotation scheduled for token ${tokenName}`);
}
this.logger.info(`Token ${tokenName} protected successfully`);
return true;
}
/**
* Retrieves a protected token
* @param tokenName The name/identifier of the token to retrieve
* @returns The token value, or null if not found
*/
public getToken(tokenName: string): string | null {
this.logger.debug(`Retrieving token: ${tokenName}`);
const token = this.tokenStore.getToken(tokenName);
if (!token) {
this.logger.warn(`Token ${tokenName} not found`);
return null;
}
// Record usage for auditing
this.tokenStore.recordTokenUsage(tokenName);
return token.value;
}
/**
* Forcibly rotates a token immediately
* @param tokenName The name/identifier of the token to rotate
* @returns Result of the rotation attempt
*/
public async rotateToken(tokenName: string): Promise<RotationResult> {
this.logger.info(`Manually rotating token: ${tokenName}`);
const tokenData = this.tokenStore.getTokenData(tokenName);
if (!tokenData) {
this.logger.error(`Token ${tokenName} not found for rotation`);
return {
success: false,
message: `Token ${tokenName} not found`,
newExpiry: null
};
}
const result = await this.rotator.rotateToken(tokenData.value);
if (result.success && result.newToken) {
// Update the stored token with the new value
let newValue = result.newToken;
// Re-embed canary if enabled
if (tokenData.config.canaryEnabled) {
newValue = this.canaryService.embedCanary(result.newToken, tokenName);
}
const updated = this.tokenStore.updateToken(tokenName, newValue, result.newExpiry);
if (!updated) {
this.logger.error(`Failed to update token ${tokenName} after rotation`);
return {
success: false,
message: 'Rotation succeeded but failed to update stored token',
newExpiry: result.newExpiry
};
}
} else {
this.logger.error(`Failed to rotate token ${tokenName}: ${result.message}`);
}
return result;
}
/**
* Gets a list of all protected token names
* @returns Array of token names
*/
public listTokens(): string[] {
return this.tokenStore.listTokens();
}
/**
* Removes a protected token
* @param tokenName The name/identifier of the token to remove
* @returns True if the token was successfully removed
*/
public removeToken(tokenName: string): boolean {
this.logger.info(`Removing token: ${tokenName}`);
// Cancel any scheduled rotation
this.cancelRotation(tokenName);
// Remove from storage
const removed = this.tokenStore.removeToken(tokenName);
if (!removed) {
this.logger.error(`Failed to remove token ${tokenName}`);
return false;
}
this.logger.info(`Token ${tokenName} removed successfully`);
return true;
}
/**
* Stops scheduled rotation for a specific token
* @param tokenName The name/identifier of the token
* @returns True if a rotation schedule was cancelled
*/
public stopRotation(tokenName: string): boolean {
const timer = this.rotationSchedules.get(tokenName);
if (!timer) {
this.logger.debug(`No rotation schedule found for token ${tokenName}`);
return false;
}
clearTimeout(timer);
this.rotationSchedules.delete(tokenName);
this.logger.info(`Rotation schedule cancelled for token ${tokenName}`);
return true;
}
/**
* Stops all scheduled rotations
*/
public stopAllRotations(): void {
for (const tokenName of this.rotationSchedules.keys()) {
this.stopRotation(tokenName);
}
}
/**
* Gets the audit log for a specific token or all tokens
* @param tokenName Optional token name to filter the log
* @returns Array of audit log entries
*/
public getAuditLog(tokenName?: string): AuditLogEntry[] {
return this.tokenStore.getAuditLog(tokenName);
}
/**
* Scans a file for potential tokens or secrets
* @param filepath Path to the file to scan
* @returns Results of the scan
*/
public async scanFile(filepath: string): Promise<ScanResult[]> {
this.logger.debug(`Scanning file: ${filepath}`);
const content = await fs.readFile(filepath, 'utf8');
return this.scanContent(content, filepath);
}
/**
* Scans content from a file for potential tokens or secrets
* @param content Content to scan
* @param filepath Original file path (for reporting)
* @returns Results of the scan
*/
public async scanContent(content: string, filepath: string): Promise<ScanResult[]> {
return this.scanner.scan(content, filepath);
}
}