File Integrity Monitoring (FIM) in Java: Comprehensive Implementation Guide

File Integrity Monitoring (FIM) is a security control that detects and alerts on changes to files and directories. This guide covers comprehensive FIM implementation for Java applications.


Core Concepts

What is File Integrity Monitoring?

  • Real-time monitoring of file system changes
  • Detection of unauthorized modifications
  • Integrity verification through cryptographic hashing
  • Alerting and reporting on suspicious activities

Key Features:

  • Real-time file monitoring using watch services
  • Cryptographic hashing for integrity verification
  • Change detection and classification
  • Alerting mechanisms for suspicious activities
  • Compliance reporting (PCI DSS, HIPAA, SOX)

Dependencies and Setup

1. Maven Dependencies
<properties> <jackson.version>2.15.2</jackson.version> <commons-io.version>2.13.0</commons-io.version> <guava.version>32.1.2-jre</guava.version> <logback.version>1.4.11</logback.version> </properties> <dependencies> <!-- File Utilities --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>${commons-io.version}</version> </dependency> <!-- Hashing and Crypto --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>${guava.version}</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <!-- Logging --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <!-- Watch Service (JDK built-in) --> </dependencies>
2. Directory Structure
src/main/java/ ├── fim/ │ ├── core/ │ │ ├── FileIntegrityMonitor.java │ │ ├── FileWatcherService.java │ │ └── HashCalculator.java │ ├── model/ │ │ ├── FileEvent.java │ │ ├── FileMetadata.java │ │ └── MonitorConfig.java │ ├── storage/ │ │ ├── BaselineStorage.java │ │ └── EventStorage.java │ ├── alert/ │ │ ├── AlertManager.java │ │ └── NotificationService.java │ └── report/ │ ├── ReportGenerator.java │ └── ComplianceReporter.java

Core Implementation

1. File Event Model
// FileEvent.java package com.example.fim.model; import java.io.File; import java.time.Instant; import java.util.Map; public class FileEvent { public enum EventType { CREATED, MODIFIED, DELETED, RENAMED, ATTRIBUTES_CHANGED } public enum ChangeType { CONTENT, PERMISSIONS, OWNERSHIP, TIMESTAMP, ALL } private String eventId; private EventType eventType; private ChangeType changeType; private String filePath; private String fileName; private long fileSize; private Instant timestamp; private String oldFilePath; // For rename events private String newFilePath; // For rename events private String oldHash; private String newHash; private Map<String, Object> metadata; private String user; private String process; private int severity; // 1-10 scale private boolean suspicious; // Constructors public FileEvent() { this.eventId = java.util.UUID.randomUUID().toString(); this.timestamp = Instant.now(); } public FileEvent(EventType eventType, String filePath) { this(); this.eventType = eventType; this.filePath = filePath; this.fileName = new File(filePath).getName(); } // Getters and setters public String getEventId() { return eventId; } public void setEventId(String eventId) { this.eventId = eventId; } public EventType getEventType() { return eventType; } public void setEventType(EventType eventType) { this.eventType = eventType; } public ChangeType getChangeType() { return changeType; } public void setChangeType(ChangeType changeType) { this.changeType = changeType; } public String getFilePath() { return filePath; } public void setFilePath(String filePath) { this.filePath = filePath; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public long getFileSize() { return fileSize; } public void setFileSize(long fileSize) { this.fileSize = fileSize; } public Instant getTimestamp() { return timestamp; } public void setTimestamp(Instant timestamp) { this.timestamp = timestamp; } public String getOldFilePath() { return oldFilePath; } public void setOldFilePath(String oldFilePath) { this.oldFilePath = oldFilePath; } public String getNewFilePath() { return newFilePath; } public void setNewFilePath(String newFilePath) { this.newFilePath = newFilePath; } public String getOldHash() { return oldHash; } public void setOldHash(String oldHash) { this.oldHash = oldHash; } public String getNewHash() { return newHash; } public void setNewHash(String newHash) { this.newHash = newHash; } public Map<String, Object> getMetadata() { return metadata; } public void setMetadata(Map<String, Object> metadata) { this.metadata = metadata; } public String getUser() { return user; } public void setUser(String user) { this.user = user; } public String getProcess() { return process; } public void setProcess(String process) { this.process = process; } public int getSeverity() { return severity; } public void setSeverity(int severity) { this.severity = severity; } public boolean isSuspicious() { return suspicious; } public void setSuspicious(boolean suspicious) { this.suspicious = suspicious; } @Override public String toString() { return String.format("FileEvent[type=%s, file=%s, time=%s, suspicious=%s]", eventType, filePath, timestamp, suspicious); } }
2. File Metadata Model
// FileMetadata.java package com.example.fim.model; import java.nio.file.attribute.*; import java.time.Instant; import java.util.HashMap; import java.util.Map; public class FileMetadata { private String filePath; private long size; private Instant createdTime; private Instant modifiedTime; private Instant accessedTime; private String owner; private String group; private String permissions; private String hash; private String hashAlgorithm; private Map<String, Object> additionalAttributes; public FileMetadata() { this.additionalAttributes = new HashMap<>(); } public FileMetadata(String filePath) { this(); this.filePath = filePath; } // Static factory method from BasicFileAttributes public static FileMetadata fromBasicAttributes(String filePath, BasicFileAttributes attrs) { FileMetadata metadata = new FileMetadata(filePath); metadata.setSize(attrs.size()); metadata.setCreatedTime(Instant.from(attrs.creationTime())); metadata.setModifiedTime(Instant.from(attrs.lastModifiedTime())); metadata.setAccessedTime(Instant.from(attrs.lastAccessTime())); return metadata; } // Static factory method from PosixFileAttributes public static FileMetadata fromPosixAttributes(String filePath, PosixFileAttributes attrs) { FileMetadata metadata = fromBasicAttributes(filePath, attrs); metadata.setOwner(attrs.owner().getName()); metadata.setGroup(attrs.group().getName()); metadata.setPermissions(attrs.permissions().toString()); return metadata; } // Getters and setters public String getFilePath() { return filePath; } public void setFilePath(String filePath) { this.filePath = filePath; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } public Instant getCreatedTime() { return createdTime; } public void setCreatedTime(Instant createdTime) { this.createdTime = createdTime; } public Instant getModifiedTime() { return modifiedTime; } public void setModifiedTime(Instant modifiedTime) { this.modifiedTime = modifiedTime; } public Instant getAccessedTime() { return accessedTime; } public void setAccessedTime(Instant accessedTime) { this.accessedTime = accessedTime; } public String getOwner() { return owner; } public void setOwner(String owner) { this.owner = owner; } public String getGroup() { return group; } public void setGroup(String group) { this.group = group; } public String getPermissions() { return permissions; } public void setPermissions(String permissions) { this.permissions = permissions; } public String getHash() { return hash; } public void setHash(String hash) { this.hash = hash; } public String getHashAlgorithm() { return hashAlgorithm; } public void setHashAlgorithm(String hashAlgorithm) { this.hashAlgorithm = hashAlgorithm; } public Map<String, Object> getAdditionalAttributes() { return additionalAttributes; } public void setAdditionalAttributes(Map<String, Object> additionalAttributes) { this.additionalAttributes = additionalAttributes; } public void addAttribute(String key, Object value) { this.additionalAttributes.put(key, value); } public Object getAttribute(String key) { return this.additionalAttributes.get(key); } }
3. Monitor Configuration
// MonitorConfig.java package com.example.fim.model; import java.util.*; import java.util.regex.Pattern; public class MonitorConfig { private List<String> monitoredDirectories; private List<String> excludedDirectories; private List<Pattern> includedPatterns; private List<Pattern> excludedPatterns; private boolean recursive = true; private long pollInterval = 5000; // milliseconds private int maxDepth = Integer.MAX_VALUE; private String hashAlgorithm = "SHA-256"; private boolean monitorContentChanges = true; private boolean monitorPermissionChanges = true; private boolean monitorOwnershipChanges = true; private boolean monitorTimestampChanges = false; private int alertThreshold = 7; // 1-10 scale private Map<String, Object> alertRules; private boolean realTimeMonitoring = true; private boolean baselineValidation = true; private long baselineInterval = 3600000; // 1 hour in milliseconds public MonitorConfig() { this.monitoredDirectories = new ArrayList<>(); this.excludedDirectories = new ArrayList<>(); this.includedPatterns = new ArrayList<>(); this.excludedPatterns = new ArrayList<>(); this.alertRules = new HashMap<>(); initializeDefaultRules(); } private void initializeDefaultRules() { // Default alert rules alertRules.put("system_files", true); alertRules.put("executable_changes", true); alertRules.put("config_changes", true); alertRules.put("after_hours", true); alertRules.put("multiple_changes", true); alertRules.put("suspicious_extensions", Arrays.asList(".exe", ".dll", ".bat", ".sh")); } // Builder pattern for fluent configuration public static class Builder { private MonitorConfig config; public Builder() { config = new MonitorConfig(); } public Builder addMonitoredDirectory(String directory) { config.monitoredDirectories.add(directory); return this; } public Builder addExcludedDirectory(String directory) { config.excludedDirectories.add(directory); return this; } public Builder addIncludedPattern(String pattern) { config.includedPatterns.add(Pattern.compile(pattern)); return this; } public Builder addExcludedPattern(String pattern) { config.excludedPatterns.add(Pattern.compile(pattern)); return this; } public Builder recursive(boolean recursive) { config.recursive = recursive; return this; } public Builder pollInterval(long pollInterval) { config.pollInterval = pollInterval; return this; } public Builder hashAlgorithm(String hashAlgorithm) { config.hashAlgorithm = hashAlgorithm; return this; } public Builder alertThreshold(int threshold) { config.alertThreshold = threshold; return this; } public MonitorConfig build() { return config; } } // Getters and setters public List<String> getMonitoredDirectories() { return monitoredDirectories; } public void setMonitoredDirectories(List<String> monitoredDirectories) { this.monitoredDirectories = monitoredDirectories; } public List<String> getExcludedDirectories() { return excludedDirectories; } public void setExcludedDirectories(List<String> excludedDirectories) { this.excludedDirectories = excludedDirectories; } public List<Pattern> getIncludedPatterns() { return includedPatterns; } public void setIncludedPatterns(List<Pattern> includedPatterns) { this.includedPatterns = includedPatterns; } public List<Pattern> getExcludedPatterns() { return excludedPatterns; } public void setExcludedPatterns(List<Pattern> excludedPatterns) { this.excludedPatterns = excludedPatterns; } public boolean isRecursive() { return recursive; } public void setRecursive(boolean recursive) { this.recursive = recursive; } public long getPollInterval() { return pollInterval; } public void setPollInterval(long pollInterval) { this.pollInterval = pollInterval; } public int getMaxDepth() { return maxDepth; } public void setMaxDepth(int maxDepth) { this.maxDepth = maxDepth; } public String getHashAlgorithm() { return hashAlgorithm; } public void setHashAlgorithm(String hashAlgorithm) { this.hashAlgorithm = hashAlgorithm; } public boolean isMonitorContentChanges() { return monitorContentChanges; } public void setMonitorContentChanges(boolean monitorContentChanges) { this.monitorContentChanges = monitorContentChanges; } public boolean isMonitorPermissionChanges() { return monitorPermissionChanges; } public void setMonitorPermissionChanges(boolean monitorPermissionChanges) { this.monitorPermissionChanges = monitorPermissionChanges; } public boolean isMonitorOwnershipChanges() { return monitorOwnershipChanges; } public void setMonitorOwnershipChanges(boolean monitorOwnershipChanges) { this.monitorOwnershipChanges = monitorOwnershipChanges; } public boolean isMonitorTimestampChanges() { return monitorTimestampChanges; } public void setMonitorTimestampChanges(boolean monitorTimestampChanges) { this.monitorTimestampChanges = monitorTimestampChanges; } public int getAlertThreshold() { return alertThreshold; } public void setAlertThreshold(int alertThreshold) { this.alertThreshold = alertThreshold; } public Map<String, Object> getAlertRules() { return alertRules; } public void setAlertRules(Map<String, Object> alertRules) { this.alertRules = alertRules; } public boolean isRealTimeMonitoring() { return realTimeMonitoring; } public void setRealTimeMonitoring(boolean realTimeMonitoring) { this.realTimeMonitoring = realTimeMonitoring; } public boolean isBaselineValidation() { return baselineValidation; } public void setBaselineValidation(boolean baselineValidation) { this.baselineValidation = baselineValidation; } public long getBaselineInterval() { return baselineInterval; } public void setBaselineInterval(long baselineInterval) { this.baselineInterval = baselineInterval; } // Utility methods public boolean shouldMonitorFile(String filePath) { // Check excluded patterns first for (Pattern pattern : excludedPatterns) { if (pattern.matcher(filePath).matches()) { return false; } } // Check included patterns if (!includedPatterns.isEmpty()) { for (Pattern pattern : includedPatterns) { if (pattern.matcher(filePath).matches()) { return true; } } return false; } return true; } public boolean isDirectoryExcluded(String directoryPath) { return excludedDirectories.stream() .anyMatch(excluded -> directoryPath.startsWith(excluded)); } }

Core Monitoring Engine

1. Hash Calculator
// HashCalculator.java package com.example.fim.core; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HexFormat; public class HashCalculator { private final String algorithm; private final int bufferSize; public HashCalculator(String algorithm) { this.algorithm = algorithm; this.bufferSize = 8192; // 8KB buffer } public HashCalculator() { this("SHA-256"); } public String calculateHash(File file) throws IOException { try (InputStream inputStream = new FileInputStream(file)) { return calculateHash(inputStream); } } public String calculateHash(Path path) throws IOException { try (InputStream inputStream = Files.newInputStream(path)) { return calculateHash(inputStream); } } public String calculateHash(InputStream inputStream) throws IOException { try { MessageDigest digest = MessageDigest.getInstance(algorithm); byte[] buffer = new byte[bufferSize]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { digest.update(buffer, 0, bytesRead); } byte[] hashBytes = digest.digest(); return HexFormat.of().formatHex(hashBytes); } catch (NoSuchAlgorithmException e) { throw new IOException("Hash algorithm not supported: " + algorithm, e); } } public String calculateHash(byte[] data) { try { MessageDigest digest = MessageDigest.getInstance(algorithm); byte[] hashBytes = digest.digest(data); return HexFormat.of().formatHex(hashBytes); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Hash algorithm not supported: " + algorithm, e); } } // Quick hash for small files (in-memory) public String calculateQuickHash(File file) throws IOException { byte[] fileContent = Files.readAllBytes(file.toPath()); return calculateHash(fileContent); } public String getAlgorithm() { return algorithm; } // Verify file integrity public boolean verifyIntegrity(File file, String expectedHash) throws IOException { String actualHash = calculateHash(file); return MessageDigest.isEqual( HexFormat.of().parseHex(expectedHash), HexFormat.of().parseHex(actualHash) ); } // Compare two files public boolean compareFiles(File file1, File file2) throws IOException { String hash1 = calculateHash(file1); String hash2 = calculateHash(file2); return hash1.equals(hash2); } }
2. File Watcher Service
// FileWatcherService.java package com.example.fim.core; import com.example.fim.model.FileEvent; import com.example.fim.model.MonitorConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.file.*; import java.util.*; import java.util.concurrent.*; import static java.nio.file.StandardWatchEventKinds.*; public class FileWatcherService { private static final Logger logger = LoggerFactory.getLogger(FileWatcherService.class); private final MonitorConfig config; private final HashCalculator hashCalculator; private final WatchService watchService; private final Map<WatchKey, Path> watchKeys; private final ExecutorService executorService; private volatile boolean running = false; private final List<FileEventListener> listeners; public FileWatcherService(MonitorConfig config) throws IOException { this.config = config; this.hashCalculator = new HashCalculator(config.getHashAlgorithm()); this.watchService = FileSystems.getDefault().newWatchService(); this.watchKeys = new ConcurrentHashMap<>(); this.executorService = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r, "file-watcher"); t.setDaemon(true); return t; }); this.listeners = new CopyOnWriteArrayList<>(); } public void start() { if (running) { logger.warn("File watcher is already running"); return; } running = true; executorService.submit(this::watchLoop); registerDirectories(); logger.info("File Integrity Monitoring started for {} directories", config.getMonitoredDirectories().size()); } public void stop() { running = false; executorService.shutdown(); try { if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { executorService.shutdownNow(); } } catch (InterruptedException e) { executorService.shutdownNow(); Thread.currentThread().interrupt(); } try { watchService.close(); } catch (IOException e) { logger.error("Error closing watch service", e); } logger.info("File Integrity Monitoring stopped"); } private void registerDirectories() { for (String directoryPath : config.getMonitoredDirectories()) { try { Path directory = Paths.get(directoryPath); if (!Files.exists(directory) || !Files.isDirectory(directory)) { logger.warn("Directory does not exist or is not a directory: {}", directoryPath); continue; } if (config.isDirectoryExcluded(directoryPath)) { logger.debug("Skipping excluded directory: {}", directoryPath); continue; } registerDirectory(directory); // Register subdirectories recursively if enabled if (config.isRecursive()) { registerSubdirectories(directory, config.getMaxDepth()); } } catch (Exception e) { logger.error("Failed to register directory: {}", directoryPath, e); } } } private void registerDirectory(Path directory) throws IOException { WatchKey key = directory.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); watchKeys.put(key, directory); logger.debug("Registered directory for monitoring: {}", directory); } private void registerSubdirectories(Path directory, int maxDepth) throws IOException { if (maxDepth <= 0) return; try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) { for (Path entry : stream) { if (Files.isDirectory(entry) && !Files.isSymbolicLink(entry)) { String entryPath = entry.toString(); if (config.shouldMonitorFile(entryPath) && !config.isDirectoryExcluded(entryPath)) { registerDirectory(entry); registerSubdirectories(entry, maxDepth - 1); } } } } catch (IOException e) { logger.error("Error registering subdirectories for: {}", directory, e); } } private void watchLoop() { logger.info("Starting file watch loop"); while (running) { try { WatchKey key = watchService.poll(config.getPollInterval(), TimeUnit.MILLISECONDS); if (key == null) { continue; } Path directory = watchKeys.get(key); if (directory == null) { logger.warn("WatchKey not recognized"); key.cancel(); continue; } processWatchKeyEvents(key, directory); // Reset key and remove if no longer valid boolean valid = key.reset(); if (!valid) { watchKeys.remove(key); logger.debug("WatchKey no longer valid for: {}", directory); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.info("File watch loop interrupted"); break; } catch (ClosedWatchServiceException e) { logger.info("Watch service closed"); break; } catch (Exception e) { logger.error("Unexpected error in file watch loop", e); } } logger.info("File watch loop stopped"); } private void processWatchKeyEvents(WatchKey key, Path directory) { for (WatchEvent<?> event : key.pollEvents()) { WatchEvent.Kind<?> kind = event.kind(); // Handle OVERFLOW event if (kind == OVERFLOW) { logger.warn("File system events may have been lost for directory: {}", directory); continue; } @SuppressWarnings("unchecked") WatchEvent<Path> pathEvent = (WatchEvent<Path>) event; Path relativePath = pathEvent.context(); Path absolutePath = directory.resolve(relativePath); // Check if file should be monitored if (!config.shouldMonitorFile(absolutePath.toString())) { continue; } // Process the file event processFileEvent(kind, absolutePath); } } private void processFileEvent(WatchEvent.Kind<?> kind, Path filePath) { try { FileEvent fileEvent = createFileEvent(kind, filePath); if (fileEvent != null) { notifyListeners(fileEvent); } } catch (Exception e) { logger.error("Error processing file event for: {}", filePath, e); } } private FileEvent createFileEvent(WatchEvent.Kind<?> kind, Path filePath) { FileEvent.EventType eventType = mapEventType(kind); if (eventType == null) return null; FileEvent event = new FileEvent(eventType, filePath.toString()); try { // Collect file metadata FileMetadata metadata = collectFileMetadata(filePath); event.setFileSize(metadata.getSize()); event.setUser(metadata.getOwner()); // Calculate hash for content changes if (config.isMonitorContentChanges() && (eventType == FileEvent.EventType.CREATED || eventType == FileEvent.EventType.MODIFIED)) { String hash = hashCalculator.calculateHash(filePath.toFile()); event.setNewHash(hash); } // Determine change type event.setChangeType(determineChangeType(eventType, filePath)); // Analyze event for suspicious activity analyzeEvent(event); } catch (Exception e) { logger.debug("Could not collect complete metadata for: {}", filePath, e); } return event; } private FileEvent.EventType mapEventType(WatchEvent.Kind<?> kind) { if (kind == ENTRY_CREATE) return FileEvent.EventType.CREATED; if (kind == ENTRY_DELETE) return FileEvent.EventType.DELETED; if (kind == ENTRY_MODIFY) return FileEvent.EventType.MODIFIED; return null; } private FileEvent.ChangeType determineChangeType(FileEvent.EventType eventType, Path filePath) { if (eventType == FileEvent.EventType.CREATED || eventType == FileEvent.EventType.DELETED) { return FileEvent.ChangeType.ALL; } // For modifications, we'd need to compare with previous state // This is a simplified implementation return FileEvent.ChangeType.CONTENT; } private FileMetadata collectFileMetadata(Path filePath) throws IOException { try { BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class); FileMetadata metadata = FileMetadata.fromBasicAttributes(filePath.toString(), attrs); // Try to get POSIX attributes if available try { PosixFileAttributes posixAttrs = Files.readAttributes(filePath, PosixFileAttributes.class); metadata.setOwner(posixAttrs.owner().getName()); metadata.setGroup(posixAttrs.group().getName()); metadata.setPermissions(posixAttrs.permissions().toString()); } catch (UnsupportedOperationException e) { // POSIX attributes not supported on this system logger.debug("POSIX attributes not supported for: {}", filePath); } return metadata; } catch (IOException e) { logger.debug("Could not read file attributes for: {}", filePath, e); throw e; } } private void analyzeEvent(FileEvent event) { int severity = calculateEventSeverity(event); event.setSeverity(severity); event.setSuspicious(severity >= config.getAlertThreshold()); // Additional suspicious activity detection if (isSuspiciousFile(event.getFilePath())) { event.setSuspicious(true); event.setSeverity(Math.max(event.getSeverity(), 8)); } } private int calculateEventSeverity(FileEvent event) { int baseSeverity = 1; // Increase severity based on event type switch (event.getEventType()) { case DELETED: baseSeverity = 6; break; case MODIFIED: baseSeverity = 4; break; case CREATED: baseSeverity = 3; break; default: baseSeverity = 2; } // Increase severity for system files if (isSystemFile(event.getFilePath())) { baseSeverity += 2; } // Increase severity for executable files if (isExecutableFile(event.getFilePath())) { baseSeverity += 2; } // Increase severity for configuration files if (isConfigurationFile(event.getFilePath())) { baseSeverity += 1; } return Math.min(baseSeverity, 10); } private boolean isSuspiciousFile(String filePath) { String lowerPath = filePath.toLowerCase(); return lowerPath.contains("/etc/passwd") || lowerPath.contains("/etc/shadow") || lowerPath.contains("/bin/") || lowerPath.contains("/sbin/") || lowerPath.endsWith(".ssh/authorized_keys") || lowerPath.endsWith(".bashrc") || lowerPath.endsWith(".profile"); } private boolean isSystemFile(String filePath) { return filePath.startsWith("/etc/") || filePath.startsWith("/bin/") || filePath.startsWith("/sbin/") || filePath.startsWith("/usr/bin/") || filePath.startsWith("/usr/sbin/"); } private boolean isExecutableFile(String filePath) { return filePath.endsWith(".exe") || filePath.endsWith(".bin") || filePath.endsWith(".sh") || filePath.endsWith(".bat") || !filePath.contains("."); // No extension often indicates executable } private boolean isConfigurationFile(String filePath) { return filePath.endsWith(".conf") || filePath.endsWith(".config") || filePath.endsWith(".properties") || filePath.endsWith(".yml") || filePath.endsWith(".yaml") || filePath.endsWith(".xml") || filePath.endsWith(".json"); } // Listener management public void addListener(FileEventListener listener) { listeners.add(listener); } public void removeListener(FileEventListener listener) { listeners.remove(listener); } private void notifyListeners(FileEvent event) { for (FileEventListener listener : listeners) { try { listener.onFileEvent(event); } catch (Exception e) { logger.error("Error notifying listener", e); } } } public interface FileEventListener { void onFileEvent(FileEvent event); } }
3. File Integrity Monitor
// FileIntegrityMonitor.java package com.example.fim.core; import com.example.fim.model.*; import com.example.fim.alert.AlertManager; import com.example.fim.storage.BaselineStorage; import com.example.fim.storage.EventStorage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.time.Instant; import java.util.*; import java.util.concurrent.*; public class FileIntegrityMonitor implements FileWatcherService.FileEventListener { private static final Logger logger = LoggerFactory.getLogger(FileIntegrityMonitor.class); private final MonitorConfig config; private final FileWatcherService fileWatcher; private final HashCalculator hashCalculator; private final BaselineStorage baselineStorage; private final EventStorage eventStorage; private final AlertManager alertManager; private final ScheduledExecutorService scheduler; private final Map<String, FileMetadata> currentBaseline; public FileIntegrityMonitor(MonitorConfig config) throws IOException { this.config = config; this.fileWatcher = new FileWatcherService(config); this.hashCalculator = new HashCalculator(config.getHashAlgorithm()); this.baselineStorage = new BaselineStorage(); this.eventStorage = new EventStorage(); this.alertManager = new AlertManager(config); this.scheduler = Executors.newScheduledThreadPool(2); this.currentBaseline = new ConcurrentHashMap<>(); // Register as listener for file events fileWatcher.addListener(this); } public void start() { logger.info("Starting File Integrity Monitor"); // Establish initial baseline if (config.isBaselineValidation()) { establishBaseline(); } // Start real-time monitoring if (config.isRealTimeMonitoring()) { fileWatcher.start(); } // Schedule periodic baseline validation if (config.isBaselineValidation()) { scheduler.scheduleAtFixedRate( this::validateBaseline, config.getBaselineInterval(), config.getBaselineInterval(), TimeUnit.MILLISECONDS ); } // Schedule periodic cleanup scheduler.scheduleAtFixedRate( this::cleanupOldEvents, 24, 24, TimeUnit.HOURS ); logger.info("File Integrity Monitor started successfully"); } public void stop() { logger.info("Stopping File Integrity Monitor"); fileWatcher.stop(); scheduler.shutdown(); try { if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) { scheduler.shutdownNow(); } } catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); } logger.info("File Integrity Monitor stopped"); } private void establishBaseline() { logger.info("Establishing file integrity baseline"); int filesProcessed = 0; int errors = 0; for (String directoryPath : config.getMonitoredDirectories()) { try { filesProcessed += scanDirectoryForBaseline(directoryPath); } catch (Exception e) { logger.error("Error establishing baseline for directory: {}", directoryPath, e); errors++; } } // Save baseline to persistent storage baselineStorage.saveBaseline(currentBaseline); logger.info("Baseline established: {} files processed, {} errors", filesProcessed, errors); } private int scanDirectoryForBaseline(String directoryPath) throws IOException { return scanDirectoryForBaseline( java.nio.file.Paths.get(directoryPath), config.getMaxDepth() ); } private int scanDirectoryForBaseline(java.nio.file.Path directory, int maxDepth) throws IOException { if (maxDepth < 0) return 0; if (config.isDirectoryExcluded(directory.toString())) return 0; int filesProcessed = 0; try (var stream = java.nio.file.Files.list(directory)) { for (java.nio.file.Path path : stream.collect(java.util.stream.Collectors.toList())) { if (java.nio.file.Files.isDirectory(path)) { // Recursively scan subdirectories filesProcessed += scanDirectoryForBaseline(path, maxDepth - 1); } else if (java.nio.file.Files.isRegularFile(path)) { // Process file if (config.shouldMonitorFile(path.toString())) { try { FileMetadata metadata = createFileMetadata(path); currentBaseline.put(path.toString(), metadata); filesProcessed++; } catch (Exception e) { logger.warn("Could not process file for baseline: {}", path, e); } } } } } return filesProcessed; } private FileMetadata createFileMetadata(java.nio.file.Path filePath) throws IOException { FileMetadata metadata = FileMetadata.fromBasicAttributes( filePath.toString(), java.nio.file.Files.readAttributes(filePath, java.nio.file.attribute.BasicFileAttributes.class) ); // Calculate file hash String hash = hashCalculator.calculateHash(filePath.toFile()); metadata.setHash(hash); metadata.setHashAlgorithm(config.getHashAlgorithm()); // Add POSIX attributes if available try { java.nio.file.attribute.PosixFileAttributes posixAttrs = java.nio.file.Files.readAttributes(filePath, java.nio.file.attribute.PosixFileAttributes.class); metadata.setOwner(posixAttrs.owner().getName()); metadata.setGroup(posixAttrs.group().getName()); metadata.setPermissions(posixAttrs.permissions().toString()); } catch (UnsupportedOperationException e) { // POSIX not supported, skip } return metadata; } private void validateBaseline() { logger.info("Starting periodic baseline validation"); int changesDetected = 0; List<FileEvent> validationEvents = new ArrayList<>(); for (Map.Entry<String, FileMetadata> entry : currentBaseline.entrySet()) { String filePath = entry.getKey(); FileMetadata baselineMetadata = entry.getValue(); try { java.nio.file.Path path = java.nio.file.Paths.get(filePath); if (!java.nio.file.Files.exists(path)) { // File was deleted FileEvent event = new FileEvent(FileEvent.EventType.DELETED, filePath); event.setOldHash(baselineMetadata.getHash()); event.setSuspicious(true); event.setSeverity(8); validationEvents.add(event); changesDetected++; continue; } // Check if file content changed FileMetadata currentMetadata = createFileMetadata(path); if (!baselineMetadata.getHash().equals(currentMetadata.getHash())) { FileEvent event = new FileEvent(FileEvent.EventType.MODIFIED, filePath); event.setOldHash(baselineMetadata.getHash()); event.setNewHash(currentMetadata.getHash()); event.setChangeType(FileEvent.ChangeType.CONTENT); analyzeEvent(event); validationEvents.add(event); changesDetected++; // Update baseline currentBaseline.put(filePath, currentMetadata); } // Check permission changes if (config.isMonitorPermissionChanges() && !Objects.equals(baselineMetadata.getPermissions(), currentMetadata.getPermissions())) { FileEvent event = new FileEvent(FileEvent.EventType.MODIFIED, filePath); event.setChangeType(FileEvent.ChangeType.PERMISSIONS); analyzeEvent(event); validationEvents.add(event); changesDetected++; } } catch (Exception e) { logger.warn("Error validating file: {}", filePath, e); } } // Process validation events for (FileEvent event : validationEvents) { processFileEvent(event); } // Save updated baseline baselineStorage.saveBaseline(currentBaseline); logger.info("Baseline validation completed: {} changes detected", changesDetected); } @Override public void onFileEvent(FileEvent event) { processFileEvent(event); } private void processFileEvent(FileEvent event) { try { // Store event eventStorage.storeEvent(event); // Check against baseline for real-time events if (config.isBaselineValidation()) { checkAgainstBaseline(event); } // Send alert if suspicious if (event.isSuspicious()) { alertManager.sendAlert(event); } // Log the event if (event.getSeverity() >= 5) { logger.warn("File integrity event: {}", event); } else { logger.info("File integrity event: {}", event); } } catch (Exception e) { logger.error("Error processing file event: {}", event, e); } } private void checkAgainstBaseline(FileEvent event) { String filePath = event.getFilePath(); FileMetadata baselineMetadata = currentBaseline.get(filePath); if (baselineMetadata == null) { // New file not in baseline if (event.getEventType() == FileEvent.EventType.CREATED) { try { FileMetadata newMetadata = createFileMetadata(java.nio.file.Paths.get(filePath)); currentBaseline.put(filePath, newMetadata); baselineStorage.saveBaseline(currentBaseline); } catch (Exception e) { logger.warn("Could not add new file to baseline: {}", filePath, e); } } return; } // For modifications, verify hash if available if (event.getEventType() == FileEvent.EventType.MODIFIED && event.getNewHash() != null && config.isMonitorContentChanges()) { if (!baselineMetadata.getHash().equals(event.getNewHash())) { event.setSuspicious(true); event.setSeverity(Math.max(event.getSeverity(), 7)); } } // For deletions, mark as suspicious if file was in baseline if (event.getEventType() == FileEvent.EventType.DELETED) { event.setSuspicious(true); event.setSeverity(Math.max(event.getSeverity(), 8)); currentBaseline.remove(filePath); baselineStorage.saveBaseline(currentBaseline); } } private void analyzeEvent(FileEvent event) { // Already implemented in FileWatcherService, but we can add additional analysis here if (isAfterHours(event.getTimestamp())) { event.setSuspicious(true); event.setSeverity(Math.max(event.getSeverity(), 6)); } } private boolean isAfterHours(Instant timestamp) { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(timestamp.toEpochMilli()); int hour = cal.get(Calendar.HOUR_OF_DAY); return hour < 6 || hour > 18; // Outside 6 AM - 6 PM } private void cleanupOldEvents() { try { Instant cutoff = Instant.now().minus(java.time.Duration.ofDays(30)); eventStorage.cleanupOldEvents(cutoff); logger.info("Cleaned up events older than 30 days"); } catch (Exception e) { logger.error("Error cleaning up old events", e); } } // Public API methods public void addMonitoredDirectory(String directory) throws IOException { config.getMonitoredDirectories().add(directory); if (running) { // Re-register directories fileWatcher.stop(); fileWatcher.start(); } } public List<FileEvent> getRecentEvents(int limit) { return eventStorage.getRecentEvents(limit); } public List<FileEvent> getSuspiciousEvents(Instant from, Instant to) { return eventStorage.getEventsBySeverity(from, to, config.getAlertThreshold()); } public boolean verifyFileIntegrity(String filePath) throws IOException { FileMetadata baseline = currentBaseline.get(filePath); if (baseline == null) { throw new IOException("File not in baseline: " + filePath); } java.nio.file.Path path = java.nio.file.Paths.get(filePath); if (!java.nio.file.Files.exists(path)) { return false; } String currentHash = hashCalculator.calculateHash(path.toFile()); return baseline.getHash().equals(currentHash); } public Map<String, Object> getStatistics() { Map<String, Object> stats = new HashMap<>(); stats.put("monitoredDirectories", config.getMonitoredDirectories().size()); stats.put("baselineFiles", currentBaseline.size()); stats.put("totalEvents", eventStorage.getTotalEventCount()); stats.put("suspiciousEvents", eventStorage.getSuspiciousEventCount()); stats.put("lastValidation", Instant.now()); return stats; } private boolean running = false; public boolean isRunning() { return running; } }

Storage Implementations

1. Baseline Storage
// BaselineStorage.java package com.example.fim.storage; import com.example.fim.model.FileMetadata; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class BaselineStorage { private static final Logger logger = LoggerFactory.getLogger(BaselineStorage.class); private final String storagePath; private final ObjectMapper objectMapper; public BaselineStorage() { this("~/.fim/baseline.json"); } public BaselineStorage(String storagePath) { this.storagePath = storagePath.replaceFirst("^~", System.getProperty("user.home")); this.objectMapper = new ObjectMapper(); ensureStorageDirectory(); } public void saveBaseline(Map<String, FileMetadata> baseline) { try { String json = objectMapper.writerWithDefaultPrettyPrinter() .writeValueAsString(baseline); Files.writeString(Paths.get(storagePath), json); logger.debug("Baseline saved with {} entries", baseline.size()); } catch (IOException e) { logger.error("Failed to save baseline", e); } } public Map<String, FileMetadata> loadBaseline() { try { Path path = Paths.get(storagePath); if (!Files.exists(path)) { logger.info("No existing baseline found"); return new ConcurrentHashMap<>(); } String json = Files.readString(path); Map<String, FileMetadata> baseline = objectMapper.readValue(json, objectMapper.getTypeFactory().constructMapType(ConcurrentHashMap.class, String.class, FileMetadata.class)); logger.info("Baseline loaded with {} entries", baseline.size()); return baseline; } catch (IOException e) { logger.error("Failed to load baseline", e); return new ConcurrentHashMap<>(); } } public void clearBaseline() { try { Files.deleteIfExists(Paths.get(storagePath)); logger.info("Baseline cleared"); } catch (IOException e) { logger.error("Failed to clear baseline", e); } } private void ensureStorageDirectory() { try { Path directory = Paths.get(storagePath).getParent(); if (directory != null) { Files.createDirectories(directory); } } catch (IOException e) { logger.error("Failed to create storage directory", e); } } }
2. Event Storage
// EventStorage.java package com.example.fim.storage; import com.example.fim.model.FileEvent; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; public class EventStorage { private static final Logger logger = LoggerFactory.getLogger(EventStorage.class); private final String storageDirectory; private final ObjectMapper objectMapper; private final List<FileEvent> inMemoryEvents; private final int maxInMemoryEvents; public EventStorage() { this("~/.fim/events"); } public EventStorage(String storageDirectory) { this.storageDirectory = storageDirectory.replaceFirst("^~", System.getProperty("user.home")); this.objectMapper = new ObjectMapper(); this.inMemoryEvents = new CopyOnWriteArrayList<>(); this.maxInMemoryEvents = 10000; ensureStorageDirectory(); } public void storeEvent(FileEvent event) { // Store in memory synchronized (inMemoryEvents) { inMemoryEvents.add(event); // Persist to disk if we have too many in memory if (inMemoryEvents.size() >= maxInMemoryEvents) { persistEventsToDisk(); } } // For high-severity events, persist immediately if (event.getSeverity() >= 7) { persistEventToDisk(event); } } public List<FileEvent> getRecentEvents(int limit) { List<FileEvent> recentEvents = new ArrayList<>(); synchronized (inMemoryEvents) { int startIndex = Math.max(0, inMemoryEvents.size() - limit); for (int i = startIndex; i < inMemoryEvents.size(); i++) { recentEvents.add(inMemoryEvents.get(i)); } } // Sort by timestamp descending recentEvents.sort((e1, e2) -> e2.getTimestamp().compareTo(e1.getTimestamp())); return recentEvents.stream() .limit(limit) .collect(Collectors.toList()); } public List<FileEvent> getEventsBySeverity(Instant from, Instant to, int minSeverity) { return inMemoryEvents.stream() .filter(event -> event.getTimestamp().isAfter(from) && event.getTimestamp().isBefore(to) && event.getSeverity() >= minSeverity) .sorted((e1, e2) -> e2.getTimestamp().compareTo(e1.getTimestamp())) .collect(Collectors.toList()); } public List<FileEvent> getEventsByFile(String filePath, Instant from, Instant to) { return inMemoryEvents.stream() .filter(event -> event.getFilePath().equals(filePath) && event.getTimestamp().isAfter(from) && event.getTimestamp().isBefore(to)) .sorted((e1, e2) -> e2.getTimestamp().compareTo(e1.getTimestamp())) .collect(Collectors.toList()); } public int getTotalEventCount() { return inMemoryEvents.size(); } public int getSuspiciousEventCount() { return (int) inMemoryEvents.stream() .filter(FileEvent::isSuspicious) .count(); } public void cleanupOldEvents(Instant cutoff) { synchronized (inMemoryEvents) { inMemoryEvents.removeIf(event -> event.getTimestamp().isBefore(cutoff)); } // Also clean up old disk files cleanupOldDiskEvents(cutoff); } private void persistEventsToDisk() { synchronized (inMemoryEvents) { if (inMemoryEvents.isEmpty()) return; String timestamp = Instant.now().toString().replace(":", "-"); String filename = "events-" + timestamp + ".json"; Path filePath = Paths.get(storageDirectory, filename); try { String json = objectMapper.writerWithDefaultPrettyPrinter() .writeValueAsString(inMemoryEvents); Files.writeString(filePath, json); logger.debug("Persisted {} events to disk", inMemoryEvents.size()); // Clear in-memory events after persisting inMemoryEvents.clear(); } catch (IOException e) { logger.error("Failed to persist events to disk", e); } } } private void persistEventToDisk(FileEvent event) { String timestamp = event.getTimestamp().toString().replace(":", "-"); String filename = "event-" + timestamp + "-" + event.getEventId() + ".json"; Path filePath = Paths.get(storageDirectory, filename); try { String json = objectMapper.writerWithDefaultPrettyPrinter() .writeValueAsString(event); Files.writeString(filePath, json); } catch (IOException e) { logger.error("Failed to persist event to disk: {}", event.getEventId(), e); } } private void cleanupOldDiskEvents(Instant cutoff) { try { Files.list(Paths.get(storageDirectory)) .filter(path -> path.toString().endsWith(".json")) .filter(path -> { try { String filename = path.getFileName().toString(); // Extract timestamp from filename if (filename.startsWith("events-")) { String timestampStr = filename.substring(7, filename.length() - 5) .replace("-", ":"); Instant fileTime = Instant.parse(timestampStr); return fileTime.isBefore(cutoff); } } catch (Exception e) { logger.debug("Could not parse timestamp from filename: {}", path); } return false; }) .forEach(path -> { try { Files.delete(path); logger.debug("Deleted old event file: {}", path); } catch (IOException e) { logger.warn("Could not delete old event file: {}", path, e); } }); } catch (IOException e) { logger.error("Error cleaning up old disk events", e); } } private void ensureStorageDirectory() { try { Files.createDirectories(Paths.get(storageDirectory)); } catch (IOException e) { logger.error("Failed to create event storage directory", e); } } }

Alerting System

1. Alert Manager
// AlertManager.java package com.example.fim.alert; import com.example.fim.model.FileEvent; import com.example.fim.model.MonitorConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class AlertManager { private static final Logger logger = LoggerFactory.getLogger(AlertManager.class); private final MonitorConfig config; private final List<NotificationService> notificationServices; private final Map<String, Long> recentAlerts; private final ScheduledExecutorService alertScheduler; private final int alertCooldownMinutes; public AlertManager(MonitorConfig config) { this.config = config; this.notificationServices = new ArrayList<>(); this.recentAlerts = new ConcurrentHashMap<>(); this.alertScheduler = Executors.newSingleThreadScheduledExecutor(); this.alertCooldownMinutes = 5; // Initialize notification services initializeNotificationServices(); // Schedule alert cleanup alertScheduler.scheduleAtFixedRate( this::cleanupOldAlerts, 1, 1, TimeUnit.HOURS ); } public void sendAlert(FileEvent event) { // Check if we recently alerted for this file String alertKey = generateAlertKey(event); if (isInCooldown(alertKey)) { logger.debug("Alert for {} is in cooldown, skipping", alertKey); return; } // Create alert Alert alert = createAlertFromEvent(event); // Send through all notification services for (NotificationService service : notificationServices) { try { service.sendAlert(alert); } catch (Exception e) { logger.error("Failed to send alert via {}: {}", service.getClass().getSimpleName(), e.getMessage()); } } // Record alert time for cooldown recentAlerts.put(alertKey, System.currentTimeMillis()); logger.warn("Alert sent: {}", alert.getSummary()); } private Alert createAlertFromEvent(FileEvent event) { Alert alert = new Alert(); alert.setId(event.getEventId()); alert.setTimestamp(event.getTimestamp()); alert.setSeverity(event.getSeverity()); alert.setSource("FileIntegrityMonitor"); // Set alert message based on event type String message = String.format( "File integrity %s detected: %s\n" + "Change Type: %s\n" + "Severity: %d\n" + "User: %s\n" + "File Size: %d bytes", event.getEventType().toString().toLowerCase(), event.getFilePath(), event.getChangeType(), event.getSeverity(), event.getUser() != null ? event.getUser() : "Unknown", event.getFileSize() ); alert.setMessage(message); alert.setSummary(String.format("File %s: %s", event.getEventType().toString().toLowerCase(), event.getFileName())); // Add additional context Map<String, Object> context = new HashMap<>(); context.put("filePath", event.getFilePath()); context.put("eventType", event.getEventType()); context.put("changeType", event.getChangeType()); context.put("suspicious", event.isSuspicious()); if (event.getOldHash() != null) { context.put("oldHash", event.getOldHash()); } if (event.getNewHash() != null) { context.put("newHash", event.getNewHash()); } alert.setContext(context); return alert; } private String generateAlertKey(FileEvent event) { // Generate a key that groups similar alerts return event.getFilePath() + ":" + event.getEventType(); } private boolean isInCooldown(String alertKey) { Long lastAlertTime = recentAlerts.get(alertKey); if (lastAlertTime == null) { return false; } long cooldownMillis = alertCooldownMinutes * 60 * 1000; return (System.currentTimeMillis() - lastAlertTime) < cooldownMillis; } private void cleanupOldAlerts() { long cutoff = System.currentTimeMillis() - (alertCooldownMinutes * 60 * 1000); recentAlerts.entrySet().removeIf(entry -> entry.getValue() < cutoff); } private void initializeNotificationServices() { // Add console notification notificationServices.add(new ConsoleNotificationService()); // Add log file notification notificationServices.add(new LogFileNotificationService()); // In a real implementation, you might add: // - EmailNotificationService // - SlackNotificationService // - SNMPNotificationService // - WebhookNotificationService } public void addNotificationService(NotificationService service) { notificationServices.add(service); } public void shutdown() { alertScheduler.shutdown(); try { if (!alertScheduler.awaitTermination(5, TimeUnit.SECONDS)) { alertScheduler.shutdownNow(); } } catch (InterruptedException e) { alertScheduler.shutdownNow(); Thread.currentThread().interrupt(); } } public static class Alert { private String id; private Instant timestamp; private int severity; private String source; private String summary; private String message; private Map<String, Object> context; // Getters and setters public String getId() { return id; } public void setId(String id) { this.id = id; } public Instant getTimestamp() { return timestamp; } public void setTimestamp(Instant timestamp) { this.timestamp = timestamp; } public int getSeverity() { return severity; } public void setSeverity(int severity) { this.severity = severity; } public String getSource() { return source; } public void setSource(String source) { this.source = source; } public String getSummary() { return summary; } public void setSummary(String summary) { this.summary = summary; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Map<String, Object> getContext() { return context; } public void setContext(Map<String, Object> context) { this.context = context; } } public interface NotificationService { void sendAlert(Alert alert) throws Exception; } // Example notification services public static class ConsoleNotificationService implements NotificationService { @Override public void sendAlert(Alert alert) { System.err.printf("[ALERT] %s - %s\n", alert.getSummary(), alert.getMessage()); } } public static class LogFileNotificationService implements NotificationService { private static final Logger alertLogger = LoggerFactory.getLogger("FILE_INTEGRITY_ALERTS"); @Override public void sendAlert(Alert alert) { alertLogger.warn("ALERT - {}: {}", alert.getSummary(), alert.getMessage()); } } }

Usage Examples

1. Basic FIM Setup
// FIMDemo.java package com.example.fim.demo; import com.example.fim.core.FileIntegrityMonitor; import com.example.fim.model.MonitorConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FIMDemo { private static final Logger logger = LoggerFactory.getLogger(FIMDemo.class); public static void main(String[] args) { try { // Configure FIM MonitorConfig config = new MonitorConfig.Builder() .addMonitoredDirectory("/etc") .addMonitoredDirectory("/bin") .addMonitoredDirectory("/usr/bin") .addExcludedDirectory("/etc/ssl/certs") // Too many changes .addIncludedPattern(".*\\.conf$") .addExcludedPattern(".*\\.log$") .recursive(true) .pollInterval(3000) .hashAlgorithm("SHA-256") .alertThreshold(5) .build(); // Create and start monitor FileIntegrityMonitor fim = new FileIntegrityMonitor(config); fim.start(); // Add shutdown hook for graceful shutdown Runtime.getRuntime().addShutdownHook(new Thread(() -> { logger.info("Shutting down File Integrity Monitor..."); fim.stop(); })); // Keep the application running Thread.sleep(Long.MAX_VALUE); } catch (Exception e) { logger.error("FIM demo failed", e); System.exit(1); } } }
2. Spring Boot Integration
// FIMConfiguration.java package com.example.fim.config; import com.example.fim.core.FileIntegrityMonitor; import com.example.fim.model.MonitorConfig; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Arrays; @Configuration public class FIMConfiguration { @Value("${fim.monitored.directories:/etc,/bin,/usr/bin}") private String[] monitoredDirectories; @Value("${fim.excluded.directories:/etc/ssl/certs,/tmp}") private String[] excludedDirectories; @Value("${fim.alert.threshold:5}") private int alertThreshold; @Value("${fim.poll.interval:5000}") private long pollInterval; @Bean public MonitorConfig monitorConfig() { MonitorConfig.Builder builder = new MonitorConfig.Builder(); // Add monitored directories Arrays.stream(monitoredDirectories) .forEach(builder::addMonitoredDirectory); // Add excluded directories Arrays.stream(excludedDirectories) .forEach(builder::addExcludedDirectory); // Configure other settings builder.alertThreshold(alertThreshold) .pollInterval(pollInterval) .recursive(true) .hashAlgorithm("SHA-256"); return builder.build(); } @Bean(initMethod = "start", destroyMethod = "stop") public FileIntegrityMonitor fileIntegrityMonitor(MonitorConfig config) { try { return new FileIntegrityMonitor(config); } catch (Exception e) { throw new RuntimeException("Failed to create File Integrity Monitor", e); } } } // FIMController.java package com.example.fim.controller; import com.example.fim.core.FileIntegrityMonitor; import com.example.fim.model.FileEvent; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/fim") public class FIMController { private final FileIntegrityMonitor fim; public FIMController(FileIntegrityMonitor fim) { this.fim = fim; } @GetMapping("/status") public Map<String, Object> getStatus() { return fim.getStatistics(); } @GetMapping("/events/recent") public List<FileEvent> getRecentEvents(@RequestParam(defaultValue = "50") int limit) { return fim.getRecentEvents(limit); } @GetMapping("/events/suspicious") public List<FileEvent> getSuspiciousEvents() { return fim.getSuspiciousEvents( java.time.Instant.now().minus(java.time.Duration.ofHours(24)), java.time.Instant.now() ); } @PostMapping("/verify/{filePath:.*}") public Map<String, Object> verifyFile(@PathVariable String filePath) { try { boolean integrityOk = fim.verifyFileIntegrity(filePath); return Map.of( "file", filePath, "integrity", integrityOk ? "OK" : "COMPROMISED", "timestamp", java.time.Instant.now() ); } catch (Exception e) { return Map.of( "file", filePath, "integrity", "ERROR", "error", e.getMessage(), "timestamp", java.time.Instant.now() ); } } }
3. Application Properties
# application.properties fim.monitored.directories=/etc,/bin,/usr/bin,/opt/app/config fim.excluded.directories=/etc/ssl/certs,/tmp,/var/log fim.alert.threshold=5 fim.poll.interval=5000 fim.baseline.validation=true fim.baseline.interval=3600000 # Logging logging.level.com.example.fim=INFO logging.level.FILE_INTEGRITY_ALERTS=WARN # Custom pattern for FIM alerts logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %logger{36} - %msg%n

Best Practices

1. Performance Optimization
// OptimizedFIMService.java package com.example.fim.optimized; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; public class OptimizedFIMService { private final ExecutorService eventProcessor; private final ScheduledExecutorService maintenanceScheduler; private final AtomicLong eventsProcessed; private final int maxQueueSize; private final BlockingQueue<Runnable> eventQueue; public OptimizedFIMService() { this.maxQueueSize = 10000; this.eventQueue = new LinkedBlockingQueue<>(maxQueueSize); this.eventsProcessed = new AtomicLong(0); // Create thread pool with custom rejection policy this.eventProcessor = new ThreadPoolExecutor( 2, // core pool size 10, // max pool size 60L, TimeUnit.SECONDS, eventQueue, new ThreadPoolExecutor.CallerRunsPolicy() // Handle backpressure ); this.maintenanceScheduler = Executors.newScheduledThreadPool(1); } public void submitEventProcessing(Runnable task) { eventProcessor.submit(() -> { try { task.run(); eventsProcessed.incrementAndGet(); } catch (Exception e) { // Log error but don't propagate to avoid breaking the monitor System.err.println("Error processing file event: " + e.getMessage()); } }); } public void scheduleMaintenance(Runnable task, long interval, TimeUnit unit) { maintenanceScheduler.scheduleAtFixedRate(task, interval, interval, unit); } public Map<String, Object> getPerformanceStats() { ThreadPoolExecutor executor = (ThreadPoolExecutor) eventProcessor; return Map.of( "eventsProcessed", eventsProcessed.get(), "activeThreads", executor.getActiveCount(), "queueSize", eventQueue.size(), "completedTasks", executor.getCompletedTaskCount(), "poolSize", executor.getPoolSize() ); } public void shutdown() { eventProcessor.shutdown(); maintenanceScheduler.shutdown(); try { if (!eventProcessor.awaitTermination(30, TimeUnit.SECONDS)) { eventProcessor.shutdownNow(); } if (!maintenanceScheduler.awaitTermination(10, TimeUnit.SECONDS)) { maintenanceScheduler.shutdownNow(); } } catch (InterruptedException e) { eventProcessor.shutdownNow(); maintenanceScheduler.shutdownNow(); Thread.currentThread().interrupt(); } } }
2. Security Considerations
// SecureFIMService.java package com.example.fim.secure; import java.security.Permission; import java.security.Policy; import java.security.ProtectionDomain; public class SecureFIMService { public static void enableSecurityManager() { // Set a security policy for FIM operations Policy.setPolicy(new FIMSecurityPolicy()); System.setSecurityManager(new SecurityManager()); } private static class FIMSecurityPolicy extends Policy { @Override public boolean implies(ProtectionDomain domain, Permission permission) { // Allow necessary permissions for FIM operations if (permission instanceof java.io.FilePermission) { String actions = permission.getActions(); String name = permission.getName(); // Allow read access to monitored directories if (actions.equals("read") && (name.startsWith("/etc/") || name.startsWith("/bin/") || name.startsWith("/usr/bin/"))) { return true; } } // Allow necessary runtime permissions if (permission instanceof RuntimePermission) { String name = permission.getName(); if (name.startsWith("accessClassInPackage.") || name.equals("modifyThread") || name.equals("stopThread")) { return true; } } return false; } } }

Conclusion

File Integrity Monitoring in Java provides:

  • Real-time file system monitoring using WatchService
  • Cryptographic integrity verification through hashing
  • Comprehensive change detection and classification
  • Flexible alerting system with multiple notification channels
  • Baseline management for integrity validation
  • Performance optimization for high-volume environments

By implementing the patterns shown above, you can create a robust FIM system that detects unauthorized file changes, maintains file integrity, and provides comprehensive security monitoring for critical system and application files.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper