feat: protect sensitive files and auto-backup before modifications
Implements GitHub issues #104 and #102: **#104: Sensitive File Protection** - Block writes to .env, credentials, SSH keys, cloud configs - Categories: environment, credentials, ssh_keys, api_tokens, certificates, cloud_config - Warn on reads of .env files (may contain secrets) - Block writes to id_rsa, *.pem, *.key, credentials.json, etc. **#102: Auto-Backup System** - Automatic backup before every write/edit operation - Backups stored in .codetyper-backup/ (preserves directory structure) - Max 10 backups per file - 7-day retention with auto-cleanup - listBackups, getLatestBackup, restoreFromBackup functions Closes #104 Closes #102
This commit is contained in:
163
src/services/sensitive-file-guard.ts
Normal file
163
src/services/sensitive-file-guard.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Sensitive File Guard Service
|
||||
*
|
||||
* Protects sensitive files (credentials, keys, etc.) from being
|
||||
* modified or inadvertently exposed.
|
||||
*/
|
||||
|
||||
import {
|
||||
PROTECTED_FILE_PATTERNS,
|
||||
SENSITIVE_FILE_MESSAGES,
|
||||
type ProtectedFilePattern,
|
||||
type SensitiveFileCategory,
|
||||
} from "@constants/sensitive-files";
|
||||
|
||||
/**
|
||||
* Type of file operation
|
||||
*/
|
||||
export type FileOperation = "read" | "write" | "edit" | "delete";
|
||||
|
||||
/**
|
||||
* Result of checking a file for sensitivity
|
||||
*/
|
||||
export interface SensitiveFileCheckResult {
|
||||
/** Whether the operation should be blocked */
|
||||
blocked: boolean;
|
||||
/** Whether to show a warning (for allowed reads) */
|
||||
warn: boolean;
|
||||
/** The matched pattern, if any */
|
||||
pattern?: ProtectedFilePattern;
|
||||
/** User-friendly message */
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file operation on the given path should be blocked or warned
|
||||
*/
|
||||
export const checkSensitiveFile = (
|
||||
filePath: string,
|
||||
operation: FileOperation,
|
||||
): SensitiveFileCheckResult => {
|
||||
// Normalize path for checking
|
||||
const normalizedPath = filePath.replace(/\\/g, "/");
|
||||
|
||||
for (const pattern of PROTECTED_FILE_PATTERNS) {
|
||||
if (pattern.pattern.test(normalizedPath)) {
|
||||
// For read operations on files that allow reading
|
||||
if (operation === "read" && pattern.allowRead) {
|
||||
return {
|
||||
blocked: false,
|
||||
warn: true,
|
||||
pattern,
|
||||
message: formatWarningMessage(pattern),
|
||||
};
|
||||
}
|
||||
|
||||
// For write/edit/delete or read on files that don't allow reading
|
||||
if (operation !== "read" || !pattern.allowRead) {
|
||||
return {
|
||||
blocked: true,
|
||||
warn: false,
|
||||
pattern,
|
||||
message: formatBlockedMessage(pattern, operation),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { blocked: false, warn: false };
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a warning message for sensitive file reads
|
||||
*/
|
||||
const formatWarningMessage = (pattern: ProtectedFilePattern): string => {
|
||||
const categoryDescription =
|
||||
SENSITIVE_FILE_MESSAGES.CATEGORY_DESCRIPTIONS[pattern.category];
|
||||
|
||||
return [
|
||||
`[WARNING] ${pattern.description}`,
|
||||
"",
|
||||
categoryDescription,
|
||||
SENSITIVE_FILE_MESSAGES.WARN_READ,
|
||||
].join("\n");
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a blocked message for sensitive file writes
|
||||
*/
|
||||
const formatBlockedMessage = (
|
||||
pattern: ProtectedFilePattern,
|
||||
operation: FileOperation,
|
||||
): string => {
|
||||
const categoryDescription =
|
||||
SENSITIVE_FILE_MESSAGES.CATEGORY_DESCRIPTIONS[pattern.category];
|
||||
|
||||
const operationText = operation === "read" ? "reading" : "modifying";
|
||||
const suggestion =
|
||||
operation === "read"
|
||||
? SENSITIVE_FILE_MESSAGES.READ_SUGGESTION
|
||||
: SENSITIVE_FILE_MESSAGES.WRITE_SUGGESTION;
|
||||
|
||||
return [
|
||||
`[BLOCKED] Cannot ${operation} ${pattern.description.toLowerCase()}`,
|
||||
"",
|
||||
`Category: ${formatCategoryName(pattern.category)}`,
|
||||
"",
|
||||
categoryDescription,
|
||||
SENSITIVE_FILE_MESSAGES.BLOCKED_REASON,
|
||||
"",
|
||||
suggestion,
|
||||
].join("\n");
|
||||
};
|
||||
|
||||
/**
|
||||
* Format category name for display
|
||||
*/
|
||||
const formatCategoryName = (category: SensitiveFileCategory): string => {
|
||||
const names: Record<SensitiveFileCategory, string> = {
|
||||
environment: "Environment Files",
|
||||
credentials: "Credential Files",
|
||||
ssh_keys: "SSH Keys",
|
||||
api_tokens: "API Tokens",
|
||||
certificates: "Certificates",
|
||||
cloud_config: "Cloud Configuration",
|
||||
};
|
||||
return names[category];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all protected patterns (for configuration UI)
|
||||
*/
|
||||
export const getProtectedPatterns = (): readonly ProtectedFilePattern[] => {
|
||||
return PROTECTED_FILE_PATTERNS;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a file path is sensitive
|
||||
*/
|
||||
export const isSensitiveFile = (filePath: string): boolean => {
|
||||
const result = checkSensitiveFile(filePath, "read");
|
||||
return result.blocked || result.warn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a file can be safely written
|
||||
*/
|
||||
export const canWriteFile = (filePath: string): boolean => {
|
||||
return !checkSensitiveFile(filePath, "write").blocked;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get categories of sensitive files
|
||||
*/
|
||||
export const getSensitiveFileCategories = (): SensitiveFileCategory[] => {
|
||||
return [
|
||||
"environment",
|
||||
"credentials",
|
||||
"ssh_keys",
|
||||
"api_tokens",
|
||||
"certificates",
|
||||
"cloud_config",
|
||||
];
|
||||
};
|
||||
Reference in New Issue
Block a user