Class: Sxn::Security::SecureFileCopier
- Inherits:
-
Object
- Object
- Sxn::Security::SecureFileCopier
- Defined in:
- lib/sxn/security/secure_file_copier.rb
Overview
SecureFileCopier provides secure file copying operations with strict security controls. It validates source and destination paths, preserves/enforces file permissions, supports file encryption using OpenSSL AES-256, and maintains an audit trail.
Defined Under Namespace
Classes: CopyResult
Constant Summary collapse
- SENSITIVE_FILE_PATTERNS =
Patterns that identify sensitive files requiring special handling
[ /master\.key$/, /credentials.*\.key$/, /\.env$/, /\.env\./, /secrets\.yml$/, /\.pem$/, /\.p12$/, /\.jks$/, /\.npmrc$/, /auth_token/i, /api_key/i, /password/i, /secret/i ].freeze
- DEFAULT_PERMISSIONS =
Default secure permissions for different file types
{ sensitive: 0o600, # Owner read/write only config: 0o644, # Owner read/write, group/other read executable: 0o755 # Owner all, group/other read/execute }.freeze
- MAX_FILE_SIZE =
Maximum file size for operations (100MB)
100 * 1024 * 1024
Instance Method Summary collapse
-
#copy_file(source, destination, permissions: nil, encrypt: false, preserve_permissions: false, create_directories: true) ⇒ CopyResult
Copies a file securely with validation and optional encryption.
-
#create_symlink(source, destination, force: false) ⇒ CopyResult
Creates a symbolic link securely.
-
#decrypt_file(file_path, key) ⇒ Boolean
Decrypts a file in place using AES-256-GCM.
-
#encrypt_file(file_path, key: nil) ⇒ String
Encrypts a file in place using AES-256-GCM.
-
#initialize(project_root, logger: nil) ⇒ SecureFileCopier
constructor
A new instance of SecureFileCopier.
-
#secure_permissions?(file_path) ⇒ Boolean
Validates file permissions are secure.
-
#sensitive_file?(file_path) ⇒ Boolean
Checks if a file appears to be sensitive based on its path.
Constructor Details
#initialize(project_root, logger: nil) ⇒ SecureFileCopier
Returns a new instance of SecureFileCopier.
73 74 75 76 77 78 79 80 |
# File 'lib/sxn/security/secure_file_copier.rb', line 73 def initialize(project_root, logger: nil) @project_root = File.realpath(project_root) @path_validator = SecurePathValidator.new(@project_root) @logger = logger || Sxn.logger @encryption_key = nil rescue Errno::ENOENT raise ArgumentError, "Project root does not exist: #{project_root}" end |
Instance Method Details
#copy_file(source, destination, permissions: nil, encrypt: false, preserve_permissions: false, create_directories: true) ⇒ CopyResult
Copies a file securely with validation and optional encryption
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/sxn/security/secure_file_copier.rb', line 92 def copy_file(source, destination, permissions: nil, encrypt: false, preserve_permissions: false, create_directories: true) start_time = Time.now raw_source, raw_destination = @path_validator.validate_file_operation( source, destination, allow_creation: true ) # Normalize paths for consistent behavior in tests and cross-platform compatibility validated_source = normalize_path_for_result(raw_source) validated_destination = normalize_path_for_result(raw_destination) validate_file_operation!(raw_source, raw_destination) # Determine appropriate permissions (use normalized path for consistent method signatures) = ( validated_source, , ) # Create destination directory if needed (use raw path for actual file operations) create_destination_directory(raw_destination) if create_directories # Perform the copy operation (use normalized paths for method signatures) if encrypt copy_with_encryption(validated_source, validated_destination, ) encrypted = true else copy_without_encryption(validated_source, validated_destination, ) encrypted = false end # Generate checksum for verification (use normalized path for method signature) checksum = generate_checksum(validated_destination) duration = Time.now - start_time result = CopyResult.new( normalize_path_for_result(validated_source), normalize_path_for_result(validated_destination), :copy, encrypted: encrypted, checksum: checksum, duration: duration ) audit_log("FILE_COPY", result) result end |
#create_symlink(source, destination, force: false) ⇒ CopyResult
Creates a symbolic link securely
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/sxn/security/secure_file_copier.rb', line 145 def create_symlink(source, destination, force: false) start_time = Time.now validated_source, validated_destination = @path_validator.validate_file_operation( source, destination, allow_creation: true ) validate_file_operation!(validated_source, validated_destination) # Remove existing symlink/file if force is true File.unlink(validated_destination) if force && (File.exist?(validated_destination) || File.symlink?(validated_destination)) # Create the symlink File.symlink(validated_source, validated_destination) duration = Time.now - start_time result = CopyResult.new( normalize_path_for_result(validated_source), normalize_path_for_result(validated_destination), :symlink, duration: duration ) audit_log("SYMLINK_CREATE", result) result end |
#decrypt_file(file_path, key) ⇒ Boolean
Decrypts a file in place using AES-256-GCM
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/sxn/security/secure_file_copier.rb', line 211 def decrypt_file(file_path, key) raw_path = @path_validator.validate_path(file_path) validated_path = normalize_path_for_result(raw_path) validate_file_exists!(validated_path) encryption_key = Base64.strict_decode64(key) # Read encrypted content (use real path for file operations) real_path = denormalize_path_for_operations(validated_path) encrypted_content = File.binread(real_path) # Decrypt content original_content = decrypt_content(encrypted_content, encryption_key) # Write decrypted content atomically (use real path for file operations) temp_file = "#{real_path}.tmp" File.binwrite(temp_file, original_content) File.rename(temp_file, real_path) audit_log("FILE_DECRYPT", { file_path: validated_path }) true rescue StandardError => e raise SecurityError, "Decryption failed: #{e.message}" end |
#encrypt_file(file_path, key: nil) ⇒ String
Encrypts a file in place using AES-256-GCM
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/sxn/security/secure_file_copier.rb', line 177 def encrypt_file(file_path, key: nil) raw_path = @path_validator.validate_path(file_path) validated_path = normalize_path_for_result(raw_path) validate_file_exists!(validated_path) encryption_key = key || generate_encryption_key # Read original content (use real path for file operations) real_path = denormalize_path_for_operations(validated_path) original_content = File.binread(real_path) # Encrypt content encrypted_content = encrypt_content(original_content, encryption_key) # Write encrypted content atomically (use real path for file operations) temp_file = "#{real_path}.tmp" File.binwrite(temp_file, encrypted_content) File.rename(temp_file, real_path) # Set secure permissions (use real path for file operations) File.chmod(0o600, real_path) audit_log("FILE_ENCRYPT", { file_path: validated_path }) Base64.strict_encode64(encryption_key) rescue Sxn::PathValidationError => e raise SecurityError, "Path validation failed: #{e.message}" end |
#secure_permissions?(file_path) ⇒ Boolean
Validates file permissions are secure
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/sxn/security/secure_file_copier.rb', line 248 def (file_path) validated_path = @path_validator.validate_path(file_path, allow_creation: true) return false unless File.exist?(validated_path) stat = File.stat(validated_path) mode = stat.mode & 0o777 if sensitive_file?(file_path) # Sensitive files should not be readable by group/other mode.nobits?(0o077) else # Non-sensitive files should not be world-writable mode.nobits?(0o002) end rescue Sxn::PathValidationError false end |
#sensitive_file?(file_path) ⇒ Boolean
Checks if a file appears to be sensitive based on its path
240 241 242 |
# File 'lib/sxn/security/secure_file_copier.rb', line 240 def sensitive_file?(file_path) SENSITIVE_FILE_PATTERNS.any? { |pattern| file_path.match?(pattern) } end |