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