From c7847464b1610116deeda7d371b6105a35500d23 Mon Sep 17 00:00:00 2001 From: Thibault Dockx <thibault.dockx@fusiondirectory.org> Date: Mon, 7 Apr 2025 17:10:31 +0100 Subject: [PATCH 1/5] :art: feat(audit) - enhance Audit class to support syslog audits and improve array filtering --- plugins/tasks/Audit.php | 57 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/plugins/tasks/Audit.php b/plugins/tasks/Audit.php index 9481831..ed529f3 100644 --- a/plugins/tasks/Audit.php +++ b/plugins/tasks/Audit.php @@ -2,14 +2,11 @@ class Audit implements EndpointInterface { - private TaskGateway $gateway; - private Utils $utils; public function __construct (TaskGateway $gateway) { $this->gateway = $gateway; - $this->utils = new Utils(); } /** @@ -46,15 +43,28 @@ class Audit implements EndpointInterface */ public function processEndPointPatch (array $data = NULL): array { - $result = $this->processAuditDeletion($this->gateway->getObjectTypeTask('Audit')); + // Check if audit type is specified in data + $auditType = $data['type'] ?? 'standard'; // Default to standard audit + + if ($auditType === 'syslog') { + // Process syslog audit + $result = $this->processSyslogAuditTransformation($this->gateway->getObjectTypeTask('Audit-Syslog')); + } else { + // Process standard audit + $result = $this->processAuditDeletion($this->gateway->getObjectTypeTask('Audit')); + } // Recursive function to filter out empty arrays at any depth - $nonEmptyResults = $this->utils->recursiveArrayFilter($result); + $nonEmptyResults = $this->recursiveArrayFilter($result); if (!empty($nonEmptyResults)) { return $nonEmptyResults; } else { - return ['No audit requiring removal']; + if ($auditType === 'syslog') { + return ['No syslog audit entries requiring removal']; + } else { + return ['No standard audit entries requiring removal']; + } } } @@ -85,6 +95,26 @@ class Audit implements EndpointInterface return $result; } + /** + * @param array $syslogAuditSubTasks + * @return array + * @throws Exception + */ + public function processSyslogAuditTransformation (array $syslogAuditSubTasks): array + { + $result = []; + + print_r($syslogAuditSubTasks); + exit; + + foreach ($syslogAuditSubTasks as $task) { + // Similar to processAuditDeletion but for syslog + // ... + } + + return $result; + } + /** * @param string $mainTaskDn * @return array @@ -121,4 +151,19 @@ class Audit implements EndpointInterface return $audit; } + + /** + * @param array $array + * @return array + * Note : Recursively filters out empty values and arrays at any depth. + */ + public function recursiveArrayFilter (array $array): array + { + return array_filter($array, function ($item) { + if (is_array($item)) { + $item = $this->recursiveArrayFilter($item); + } + return !empty($item); + }); + } } \ No newline at end of file -- GitLab From 1e027117cd865440c0f5e87deb9729c275e37d4a Mon Sep 17 00:00:00 2001 From: Thibault Dockx <thibault.dockx@fusiondirectory.org> Date: Mon, 7 Apr 2025 17:56:04 +0100 Subject: [PATCH 2/5] :art: feat(audit) - implement syslog transformation for audit entries and handle duplicate entries --- plugins/tasks/Audit.php | 171 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 164 insertions(+), 7 deletions(-) diff --git a/plugins/tasks/Audit.php b/plugins/tasks/Audit.php index ed529f3..85990a0 100644 --- a/plugins/tasks/Audit.php +++ b/plugins/tasks/Audit.php @@ -61,7 +61,7 @@ class Audit implements EndpointInterface return $nonEmptyResults; } else { if ($auditType === 'syslog') { - return ['No syslog audit entries requiring removal']; + return ['No audit entries requiring transformation']; } else { return ['No standard audit entries requiring removal']; } @@ -104,12 +104,153 @@ class Audit implements EndpointInterface { $result = []; - print_r($syslogAuditSubTasks); - exit; - foreach ($syslogAuditSubTasks as $task) { - // Similar to processAuditDeletion but for syslog - // ... + try { + // If the task must be treated - status and scheduled - process the sub-tasks + if ($this->gateway->statusAndScheduleCheck($task)) { + // Retrieve data from the main task + $auditMainTask = $this->getAuditMainTask($task['fdtasksgranularmaster'][0]); + + // Get all audit entries with all attributes + $auditEntries = $this->gateway->getLdapTasks('(objectClass=fdAuditEvent)', ['*'], '', ''); + $this->gateway->unsetCountKeys($auditEntries); + + if (empty($auditEntries)) { + $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], '2'); + $result[] = ["dn" => $task['dn'], "message" => "No audit entries found to transform"]; + continue; + } + + // Create syslog file + $path = '/var/log/fusiondirectory/'; + $this->ensureDirectoryExists($path); + + $date = date('Y-m-d'); + $filename = $path . 'fd-audit-' . $date . '.log'; + + // Track which audit IDs are already in the file to prevent duplicates + $existingAuditIds = []; + + // Read existing file if it exists to extract audit IDs + if (file_exists($filename)) { + $existingContent = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($existingContent as $line) { + // Extract audit ID from the line using regex + if (preg_match('/id="([^"]+)"/', $line, $matches)) { + $existingAuditIds[] = $matches[1]; + } + } + } + + // Open file for writing (append mode) + $handle = fopen($filename, 'a'); + if ($handle === false) { + throw new Exception("Could not open file: $filename"); + } + + $count = 0; + $skipped = 0; + + foreach ($auditEntries as $entry) { + // Skip entry if its ID is already in the file + $auditId = $entry['fdauditid'][0] ?? 'unknown'; + if (in_array($auditId, $existingAuditIds)) { + $skipped++; + continue; + } + + // Parse LDAP timestamp format (YYYYMMDDHHmmss.SSSSSSZ) + $timestamp = ''; + if (isset($entry['fdauditdatetime'][0])) { + // Extract date parts from LDAP format + $dateStr = $entry['fdauditdatetime'][0]; + if (preg_match('/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/', $dateStr, $matches)) { + $year = $matches[1]; + $month = $matches[2]; + $day = $matches[3]; + $hour = $matches[4]; + $min = $matches[5]; + $sec = $matches[6]; + + // Create a datetime object and format for syslog + $dt = new DateTime("$year-$month-$day $hour:$min:$sec"); + $timestamp = $dt->format('M d H:i:s'); + } else { + $timestamp = date('M d H:i:s'); + } + } else { + $timestamp = date('M d H:i:s'); + } + + // Get hostname (use IP if available, otherwise use system hostname) + $hostname = isset($entry['fdauditauthorip'][0]) ? + $entry['fdauditauthorip'][0] : gethostname(); + + // Get user information (use DN if available) + $user = isset($entry['fdauditauthordn'][0]) ? + $entry['fdauditauthordn'][0] : 'unknown'; + + // Get action + $action = isset($entry['fdauditaction'][0]) ? + $entry['fdauditaction'][0] : 'unknown'; + + // Get object type and object + $objectType = isset($entry['fdauditobjecttype'][0]) ? + $entry['fdauditobjecttype'][0] : ''; + + $object = isset($entry['fdauditobject'][0]) ? + $entry['fdauditobject'][0] : ''; + + // Get result + $auditResult = isset($entry['fdauditresult'][0]) ? + $entry['fdauditresult'][0] : ''; + + // Format the syslog message + // <priority>timestamp hostname tag: message + $syslogMessage = "<local4.info>$timestamp $hostname FusionDirectory-Audit: "; + $syslogMessage .= "id=\"" . $auditId . "\" "; + $syslogMessage .= "user=\"$user\" "; + $syslogMessage .= "action=\"$action\" "; + + if (!empty($objectType)) { + $syslogMessage .= "objectType=\"$objectType\" "; + } + + if (!empty($object)) { + $syslogMessage .= "object=\"$object\" "; + } + + if (!empty($auditResult)) { + $syslogMessage .= "result=\"$auditResult\" "; + } + + // Add attributes if available (contains changes made) + if (isset($entry['fdauditattributes'][0])) { + $syslogMessage .= "changes=\"" . $entry['fdauditattributes'][0] . "\" "; + } + + // Write the message to the file + fwrite($handle, $syslogMessage . PHP_EOL); + $count++; + } + + fclose($handle); + + // Update task status + $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], '2'); + + // Include information about skipped entries in the result message + $resultMsg = "Successfully transformed $count audit entries to syslog format in $filename"; + if ($skipped > 0) { + $resultMsg .= " (skipped $skipped duplicate entries)"; + } + + $result[] = ["dn" => $task['dn'], "message" => $resultMsg]; + } + } catch (Exception $e) { + $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], $e->getMessage()); + $result[] = ["dn" => $task['dn'], "message" => "Error transforming audit entries: " . $e->getMessage()]; + } } return $result; @@ -141,7 +282,7 @@ class Audit implements EndpointInterface /** * @return array * NOTE : simply return the list of audit entries existing in LDAP - */ + */ public function returnLdapAuditEntries () : array { // Search in LDAP for audit entries (All entries ! This can be pretty heavy. @@ -166,4 +307,20 @@ class Audit implements EndpointInterface return !empty($item); }); } + + /** + * @param string $path + * @return bool + * @throws Exception + * Note: Create directory if it doesn't exist. + */ + private function ensureDirectoryExists (string $path): bool + { + if (!is_dir($path)) { + if (!mkdir($path, 0755, true)) { + throw new Exception("Failed to create directory: $path"); + } + } + return true; + } } \ No newline at end of file -- GitLab From cff8bd81e84a27e62aac597b731d07c5975f97a4 Mon Sep 17 00:00:00 2001 From: Thibault Dockx <thibault.dockx@fusiondirectory.org> Date: Tue, 8 Apr 2025 10:05:12 +0100 Subject: [PATCH 3/5] :art: feat(audit) - enhance syslog audit processing by implementing state tracking for last processed entries and ensuring directory existence --- plugins/tasks/Audit.php | 48 +++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/plugins/tasks/Audit.php b/plugins/tasks/Audit.php index 85990a0..4e5cb52 100644 --- a/plugins/tasks/Audit.php +++ b/plugins/tasks/Audit.php @@ -103,6 +103,9 @@ class Audit implements EndpointInterface public function processSyslogAuditTransformation (array $syslogAuditSubTasks): array { $result = []; + // Define path at the beginning of the method + $path = '/var/log/fusiondirectory/'; + $this->ensureDirectoryExists($path); foreach ($syslogAuditSubTasks as $task) { try { @@ -111,8 +114,26 @@ class Audit implements EndpointInterface // Retrieve data from the main task $auditMainTask = $this->getAuditMainTask($task['fdtasksgranularmaster'][0]); - // Get all audit entries with all attributes - $auditEntries = $this->gateway->getLdapTasks('(objectClass=fdAuditEvent)', ['*'], '', ''); + // Get the most recent audit timestamp that was already processed + $lastProcessedTime = null; + + // Check if we have a state file recording last processed time + $stateFile = $path . 'fd-audit-last-processed.txt'; + if (file_exists($stateFile)) { + $fileContent = trim(file_get_contents($stateFile)); + if (!empty($fileContent)) { + $lastProcessedTime = $fileContent; + } + } + + // Only process entries newer than last processed + $filter = '(objectClass=fdAuditEvent)'; + if ($lastProcessedTime !== null) { + $filter = "(&(objectClass=fdAuditEvent)(fdauditdatetime>=$lastProcessedTime))"; + } + + // Get only new audit entries + $auditEntries = $this->gateway->getLdapTasks($filter, ['*'], '', ''); $this->gateway->unsetCountKeys($auditEntries); if (empty($auditEntries)) { @@ -121,10 +142,7 @@ class Audit implements EndpointInterface continue; } - // Create syslog file - $path = '/var/log/fusiondirectory/'; - $this->ensureDirectoryExists($path); - + // Create syslog file (path already defined at the beginning) $date = date('Y-m-d'); $filename = $path . 'fd-audit-' . $date . '.log'; @@ -236,6 +254,24 @@ class Audit implements EndpointInterface fclose($handle); + // After processing all entries, save the latest timestamp + if (!empty($auditEntries)) { + // Find the most recent timestamp + $latestTime = null; + foreach ($auditEntries as $entry) { + if (isset($entry['fdauditdatetime'][0])) { + if ($latestTime === null || $entry['fdauditdatetime'][0] > $latestTime) { + $latestTime = $entry['fdauditdatetime'][0]; + } + } + } + + // Save it to the state file + if ($latestTime !== null) { + file_put_contents($stateFile, $latestTime); + } + } + // Update task status $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], '2'); -- GitLab From e900b7fa0c6d07fb2ec105508201da72ba0d9250 Mon Sep 17 00:00:00 2001 From: Thibault Dockx <thibault.dockx@fusiondirectory.org> Date: Tue, 8 Apr 2025 10:08:39 +0100 Subject: [PATCH 4/5] :art: refactor(audit) - improve code readability by standardizing null checks and formatting adjustments --- plugins/tasks/Audit.php | 126 ++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/plugins/tasks/Audit.php b/plugins/tasks/Audit.php index 4e5cb52..4d5145d 100644 --- a/plugins/tasks/Audit.php +++ b/plugins/tasks/Audit.php @@ -45,7 +45,7 @@ class Audit implements EndpointInterface { // Check if audit type is specified in data $auditType = $data['type'] ?? 'standard'; // Default to standard audit - + if ($auditType === 'syslog') { // Process syslog audit $result = $this->processSyslogAuditTransformation($this->gateway->getObjectTypeTask('Audit-Syslog')); @@ -113,42 +113,42 @@ class Audit implements EndpointInterface if ($this->gateway->statusAndScheduleCheck($task)) { // Retrieve data from the main task $auditMainTask = $this->getAuditMainTask($task['fdtasksgranularmaster'][0]); - + // Get the most recent audit timestamp that was already processed - $lastProcessedTime = null; + $lastProcessedTime = NULL; // Check if we have a state file recording last processed time $stateFile = $path . 'fd-audit-last-processed.txt'; if (file_exists($stateFile)) { $fileContent = trim(file_get_contents($stateFile)); - if (!empty($fileContent)) { - $lastProcessedTime = $fileContent; - } + if (!empty($fileContent)) { + $lastProcessedTime = $fileContent; + } } // Only process entries newer than last processed $filter = '(objectClass=fdAuditEvent)'; - if ($lastProcessedTime !== null) { + if ($lastProcessedTime !== NULL) { $filter = "(&(objectClass=fdAuditEvent)(fdauditdatetime>=$lastProcessedTime))"; } // Get only new audit entries $auditEntries = $this->gateway->getLdapTasks($filter, ['*'], '', ''); $this->gateway->unsetCountKeys($auditEntries); - + if (empty($auditEntries)) { $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], '2'); $result[] = ["dn" => $task['dn'], "message" => "No audit entries found to transform"]; continue; } - + // Create syslog file (path already defined at the beginning) $date = date('Y-m-d'); $filename = $path . 'fd-audit-' . $date . '.log'; - + // Track which audit IDs are already in the file to prevent duplicates $existingAuditIds = []; - + // Read existing file if it exists to extract audit IDs if (file_exists($filename)) { $existingContent = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); @@ -159,16 +159,16 @@ class Audit implements EndpointInterface } } } - + // Open file for writing (append mode) $handle = fopen($filename, 'a'); - if ($handle === false) { + if ($handle === FALSE) { throw new Exception("Could not open file: $filename"); } - - $count = 0; + + $count = 0; $skipped = 0; - + foreach ($auditEntries as $entry) { // Skip entry if its ID is already in the file $auditId = $entry['fdauditid'][0] ?? 'unknown'; @@ -176,20 +176,20 @@ class Audit implements EndpointInterface $skipped++; continue; } - + // Parse LDAP timestamp format (YYYYMMDDHHmmss.SSSSSSZ) $timestamp = ''; if (isset($entry['fdauditdatetime'][0])) { // Extract date parts from LDAP format $dateStr = $entry['fdauditdatetime'][0]; if (preg_match('/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/', $dateStr, $matches)) { - $year = $matches[1]; - $month = $matches[2]; - $day = $matches[3]; - $hour = $matches[4]; - $min = $matches[5]; - $sec = $matches[6]; - + $year = $matches[1]; + $month = $matches[2]; + $day = $matches[3]; + $hour = $matches[4]; + $min = $matches[5]; + $sec = $matches[6]; + // Create a datetime object and format for syslog $dt = new DateTime("$year-$month-$day $hour:$min:$sec"); $timestamp = $dt->format('M d H:i:s'); @@ -199,88 +199,88 @@ class Audit implements EndpointInterface } else { $timestamp = date('M d H:i:s'); } - + // Get hostname (use IP if available, otherwise use system hostname) - $hostname = isset($entry['fdauditauthorip'][0]) ? + $hostname = isset($entry['fdauditauthorip'][0]) ? $entry['fdauditauthorip'][0] : gethostname(); - + // Get user information (use DN if available) - $user = isset($entry['fdauditauthordn'][0]) ? + $user = isset($entry['fdauditauthordn'][0]) ? $entry['fdauditauthordn'][0] : 'unknown'; - + // Get action - $action = isset($entry['fdauditaction'][0]) ? + $action = isset($entry['fdauditaction'][0]) ? $entry['fdauditaction'][0] : 'unknown'; - + // Get object type and object - $objectType = isset($entry['fdauditobjecttype'][0]) ? + $objectType = isset($entry['fdauditobjecttype'][0]) ? $entry['fdauditobjecttype'][0] : ''; - - $object = isset($entry['fdauditobject'][0]) ? + + $object = isset($entry['fdauditobject'][0]) ? $entry['fdauditobject'][0] : ''; - + // Get result - $auditResult = isset($entry['fdauditresult'][0]) ? + $auditResult = isset($entry['fdauditresult'][0]) ? $entry['fdauditresult'][0] : ''; - + // Format the syslog message // <priority>timestamp hostname tag: message $syslogMessage = "<local4.info>$timestamp $hostname FusionDirectory-Audit: "; $syslogMessage .= "id=\"" . $auditId . "\" "; $syslogMessage .= "user=\"$user\" "; $syslogMessage .= "action=\"$action\" "; - + if (!empty($objectType)) { $syslogMessage .= "objectType=\"$objectType\" "; } - + if (!empty($object)) { $syslogMessage .= "object=\"$object\" "; } - + if (!empty($auditResult)) { $syslogMessage .= "result=\"$auditResult\" "; } - + // Add attributes if available (contains changes made) if (isset($entry['fdauditattributes'][0])) { $syslogMessage .= "changes=\"" . $entry['fdauditattributes'][0] . "\" "; } - + // Write the message to the file fwrite($handle, $syslogMessage . PHP_EOL); $count++; } - + fclose($handle); - + // After processing all entries, save the latest timestamp if (!empty($auditEntries)) { - // Find the most recent timestamp - $latestTime = null; - foreach ($auditEntries as $entry) { - if (isset($entry['fdauditdatetime'][0])) { - if ($latestTime === null || $entry['fdauditdatetime'][0] > $latestTime) { - $latestTime = $entry['fdauditdatetime'][0]; - } - } - } - - // Save it to the state file - if ($latestTime !== null) { - file_put_contents($stateFile, $latestTime); + // Find the most recent timestamp + $latestTime = NULL; + foreach ($auditEntries as $entry) { + if (isset($entry['fdauditdatetime'][0])) { + if ($latestTime === NULL || $entry['fdauditdatetime'][0] > $latestTime) { + $latestTime = $entry['fdauditdatetime'][0]; + } } + } + + // Save it to the state file + if ($latestTime !== NULL) { + file_put_contents($stateFile, $latestTime); + } } - + // Update task status $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], '2'); - + // Include information about skipped entries in the result message $resultMsg = "Successfully transformed $count audit entries to syslog format in $filename"; if ($skipped > 0) { $resultMsg .= " (skipped $skipped duplicate entries)"; } - + $result[] = ["dn" => $task['dn'], "message" => $resultMsg]; } } catch (Exception $e) { @@ -288,7 +288,7 @@ class Audit implements EndpointInterface $result[] = ["dn" => $task['dn'], "message" => "Error transforming audit entries: " . $e->getMessage()]; } } - + return $result; } @@ -353,10 +353,10 @@ class Audit implements EndpointInterface private function ensureDirectoryExists (string $path): bool { if (!is_dir($path)) { - if (!mkdir($path, 0755, true)) { + if (!mkdir($path, 0755, TRUE)) { throw new Exception("Failed to create directory: $path"); } } - return true; + return TRUE; } } \ No newline at end of file -- GitLab From 64cb5624e6db7be2f657532319e380ffe803cd83 Mon Sep 17 00:00:00 2001 From: Thibault Dockx <thibault.dockx@fusiondirectory.org> Date: Tue, 8 Apr 2025 10:29:48 +0100 Subject: [PATCH 5/5] :art: refactor(audit) - clarify empty audit entries check by using count() for better readability --- plugins/tasks/Audit.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/tasks/Audit.php b/plugins/tasks/Audit.php index 4d5145d..fbe474b 100644 --- a/plugins/tasks/Audit.php +++ b/plugins/tasks/Audit.php @@ -136,7 +136,8 @@ class Audit implements EndpointInterface $auditEntries = $this->gateway->getLdapTasks($filter, ['*'], '', ''); $this->gateway->unsetCountKeys($auditEntries); - if (empty($auditEntries)) { + // Check if there are no audit entries + if (count($auditEntries) === 0) { $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], '2'); $result[] = ["dn" => $task['dn'], "message" => "No audit entries found to transform"]; continue; -- GitLab