07ea171af9
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 43s
New CLI tools: - manifest_element.php — extract element/type/prefix from any platform manifest - release_create.php — create/overwrite Gitea releases with proper naming - release_package.php — build ZIP+tar.gz, SHA-256, upload assets - release_promote.php — promote releases between channels (dev→RC→stable) - version_reset_dev.php — reset platform version on dev branch after release Updated CLI tools: - version_bump.php — now writes to manifests, Dolibarr mod, composer.json (not just README) - release_cascade.php — added --version for version-aware deletion of stale releases - release_validate.php — auto-detect platform, --github-output, source dir check Workflow changes (auto-release.yml): - Draft PR to main → auto-promote highest pre-release to RC - Merged PR to main → promote RC to stable (skip rebuild when RC exists) - Removed paths filter for Go/Node/generic repo compatibility - Fixed cascade --api-base parameter bug Workflow changes (pre-release.yml): - Auto-trigger development pre-release on feature branch merge to dev - Removed paths filter Infrastructure: - RepositorySynchronizer: fixed template repo names, .mokogitea/workflows path, universal workflow cascade (Template-Generic → other templates) - bulk_sync.php: syncs universal workflows to templates before repo sync - PHPDoc added to 4 classes missing class-level docs - Version bump 09.00.00 → 09.01.00 Closes #152 #153 #154 #155 #156 #157 #158 #159 #161 #162 Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
469 lines
14 KiB
PHP
469 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Enterprise Audit Library - Structured audit logging for all operations.
|
|
*
|
|
* This class provides enterprise-grade audit logging capabilities with:
|
|
* - Structured JSON logging to audit database
|
|
* - Transaction ID tracking across operations
|
|
* - Security event logging (who, what, when, where)
|
|
* - Audit log rotation and archival
|
|
* - Compliance reporting capabilities
|
|
*
|
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: MokoStandards.Enterprise.Audit
|
|
* INGROUP: MokoStandards.Enterprise
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /lib/Enterprise/AuditLogger.php
|
|
* BRIEF: Enterprise audit logging
|
|
*
|
|
* @package MokoStandards\Enterprise
|
|
* @version 04.00.04
|
|
* @author MokoStandards Team
|
|
* @license GPL-3.0-or-later
|
|
*/
|
|
|
|
namespace MokoEnterprise;
|
|
|
|
use DateTime;
|
|
use DateTimeZone;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Enterprise audit logger with transaction tracking and structured logging.
|
|
*
|
|
* Features:
|
|
* - Transaction ID tracking
|
|
* - Security event logging
|
|
* - Structured JSON output
|
|
* - Automatic log rotation
|
|
* - Context manager support
|
|
*
|
|
* Example:
|
|
* ```php
|
|
* $logger = new AuditLogger('version_bump');
|
|
* $transaction = $logger->startTransaction('bump_version');
|
|
* $transaction->logEvent('version_change', ['old' => '1.0.0', 'new' => '1.1.0']);
|
|
* $transaction->logSecurityEvent('file_modified', ['file' => 'README.md']);
|
|
* $transaction->end();
|
|
* ```
|
|
*
|
|
* @since 04.00.00
|
|
*/
|
|
class AuditLogger
|
|
{
|
|
/** @var string Service name */
|
|
private string $service;
|
|
|
|
/** @var string User performing actions */
|
|
private string $user;
|
|
|
|
/** @var string Directory for audit logs */
|
|
private string $logDir;
|
|
|
|
/** @var bool Enable console output */
|
|
private bool $enableConsole;
|
|
|
|
/** @var bool Enable file logging */
|
|
private bool $enableFile;
|
|
|
|
/** @var int Maximum log file size in MB */
|
|
private int $maxLogSizeMb;
|
|
|
|
/** @var int Days to retain audit logs */
|
|
private int $retentionDays;
|
|
|
|
/** @var string Session ID */
|
|
private string $sessionId;
|
|
|
|
/** @var array Transaction stack */
|
|
private array $transactionStack = [];
|
|
|
|
/** @var string Version constant */
|
|
public const VERSION = '04.06.00';
|
|
|
|
/**
|
|
* Initialize audit logger.
|
|
*
|
|
* @param string $service Service name (e.g., 'version_bump', 'branch_cleanup')
|
|
* @param string|null $logDir Directory for audit logs (default: var/logs/audit/)
|
|
* @param string|null $user Username for audit trail (default: from environment)
|
|
* @param bool $enableConsole Output to console (default: true)
|
|
* @param bool $enableFile Write to file (default: true)
|
|
* @param int $maxLogSizeMb Maximum log file size before rotation
|
|
* @param int $retentionDays Days to retain audit logs
|
|
*/
|
|
public function __construct(
|
|
string $service,
|
|
?string $logDir = null,
|
|
?string $user = null,
|
|
bool $enableConsole = true,
|
|
bool $enableFile = true,
|
|
int $maxLogSizeMb = 10,
|
|
int $retentionDays = 90
|
|
) {
|
|
$this->service = $service;
|
|
$this->enableConsole = $enableConsole;
|
|
$this->enableFile = $enableFile;
|
|
$this->maxLogSizeMb = $maxLogSizeMb;
|
|
$this->retentionDays = $retentionDays;
|
|
|
|
// Determine user
|
|
$this->user = $user ?? $_SERVER['USER'] ?? $_SERVER['USERNAME'] ?? posix_getpwuid(posix_geteuid())['name'] ?? 'unknown';
|
|
|
|
// Set up log directory
|
|
if ($logDir === null) {
|
|
// Default to var/logs/audit/ in repository root
|
|
$repoRoot = dirname(__DIR__, 3);
|
|
$this->logDir = $repoRoot . '/var/logs/audit';
|
|
} else {
|
|
$this->logDir = $logDir;
|
|
}
|
|
|
|
// Create log directory if it doesn't exist
|
|
if ($this->enableFile && !is_dir($this->logDir)) {
|
|
if (!mkdir($this->logDir, 0755, true) && !is_dir($this->logDir)) {
|
|
throw new RuntimeException("Failed to create log directory: {$this->logDir}");
|
|
}
|
|
}
|
|
|
|
// Session ID for this logger instance
|
|
$this->sessionId = $this->generateSessionId();
|
|
|
|
// Log session start
|
|
$this->logSystemEvent('session_start', [
|
|
'service' => $this->service,
|
|
'user' => $this->user,
|
|
'session_id' => $this->sessionId,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Generate unique session ID.
|
|
*
|
|
* @return string Session ID
|
|
*/
|
|
private function generateSessionId(): string
|
|
{
|
|
$timestamp = (new DateTime('now', new DateTimeZone('UTC')))->format('Ymd_His');
|
|
$uniqueId = substr(bin2hex(random_bytes(4)), 0, 8);
|
|
return "{$timestamp}_{$uniqueId}";
|
|
}
|
|
|
|
/**
|
|
* Generate unique transaction ID.
|
|
*
|
|
* @return string Transaction ID (UUID v4)
|
|
*/
|
|
private function generateTransactionId(): string
|
|
{
|
|
return sprintf(
|
|
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
|
mt_rand(0, 0xffff),
|
|
mt_rand(0, 0xffff),
|
|
mt_rand(0, 0xffff),
|
|
mt_rand(0, 0x0fff) | 0x4000,
|
|
mt_rand(0, 0x3fff) | 0x8000,
|
|
mt_rand(0, 0xffff),
|
|
mt_rand(0, 0xffff),
|
|
mt_rand(0, 0xffff)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get current log file path with rotation support.
|
|
*
|
|
* @return string Log file path
|
|
*/
|
|
private function getLogFilePath(): string
|
|
{
|
|
$dateStr = (new DateTime('now', new DateTimeZone('UTC')))->format('Ymd');
|
|
return "{$this->logDir}/audit_{$this->service}_{$dateStr}.jsonl";
|
|
}
|
|
|
|
/**
|
|
* Check if log file should be rotated based on size.
|
|
*
|
|
* @param string $logFile Log file path
|
|
* @return bool True if should rotate
|
|
*/
|
|
private function shouldRotateLog(string $logFile): bool
|
|
{
|
|
if (!file_exists($logFile)) {
|
|
return false;
|
|
}
|
|
|
|
$sizeMb = filesize($logFile) / (1024 * 1024);
|
|
return $sizeMb >= $this->maxLogSizeMb;
|
|
}
|
|
|
|
/**
|
|
* Rotate log file if it exceeds size limit.
|
|
*
|
|
* @param string $logFile Log file path
|
|
*/
|
|
private function rotateLogIfNeeded(string $logFile): void
|
|
{
|
|
if ($this->shouldRotateLog($logFile)) {
|
|
$timestamp = (new DateTime('now', new DateTimeZone('UTC')))->format('His');
|
|
$rotatedFile = preg_replace('/\.jsonl$/', ".{$timestamp}.jsonl", $logFile);
|
|
rename($logFile, $rotatedFile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write log entry to file and/or console.
|
|
*
|
|
* @param array $entry Log entry data
|
|
*/
|
|
private function writeLogEntry(array $entry): void
|
|
{
|
|
// Add timestamp and session info
|
|
$entry['timestamp'] = (new DateTime('now', new DateTimeZone('UTC')))->format('c');
|
|
$entry['session_id'] = $this->sessionId;
|
|
$entry['service'] = $this->service;
|
|
$entry['user'] = $this->user;
|
|
|
|
// Console output
|
|
if ($this->enableConsole) {
|
|
$jsonOutput = json_encode($entry, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
|
echo "[AUDIT] {$jsonOutput}\n";
|
|
}
|
|
|
|
// File output
|
|
if ($this->enableFile) {
|
|
$logFile = $this->getLogFilePath();
|
|
$this->rotateLogIfNeeded($logFile);
|
|
|
|
$jsonLine = json_encode($entry, JSON_UNESCAPED_SLASHES) . "\n";
|
|
file_put_contents($logFile, $jsonLine, FILE_APPEND | LOCK_EX);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log a system event.
|
|
*
|
|
* @param string $eventType Type of system event
|
|
* @param array $data Event data
|
|
*/
|
|
private function logSystemEvent(string $eventType, array $data = []): void
|
|
{
|
|
$entry = [
|
|
'event_type' => 'system',
|
|
'event_subtype' => $eventType,
|
|
'data' => $data,
|
|
];
|
|
$this->writeLogEntry($entry);
|
|
}
|
|
|
|
/**
|
|
* Start a new transaction.
|
|
*
|
|
* @param string $operation Operation name
|
|
* @param array $context Additional context data
|
|
* @return AuditTransaction Transaction object
|
|
*/
|
|
public function startTransaction(string $operation, array $context = []): AuditTransaction
|
|
{
|
|
$transactionId = $this->generateTransactionId();
|
|
$transaction = new AuditTransaction($this, $transactionId, $operation, $context);
|
|
$this->transactionStack[] = $transactionId;
|
|
return $transaction;
|
|
}
|
|
|
|
/**
|
|
* End a transaction.
|
|
*
|
|
* @param string $transactionId Transaction ID to end
|
|
*/
|
|
public function endTransaction(string $transactionId): void
|
|
{
|
|
$key = array_search($transactionId, $this->transactionStack, true);
|
|
if ($key !== false) {
|
|
unset($this->transactionStack[$key]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log an event within a transaction.
|
|
*
|
|
* @param string $transactionId Transaction ID
|
|
* @param string $eventType Event type
|
|
* @param array $data Event data
|
|
*/
|
|
public function logEvent(string $transactionId, string $eventType, array $data = []): void
|
|
{
|
|
$entry = [
|
|
'event_type' => 'audit',
|
|
'transaction_id' => $transactionId,
|
|
'event_subtype' => $eventType,
|
|
'data' => $data,
|
|
];
|
|
$this->writeLogEntry($entry);
|
|
}
|
|
|
|
/**
|
|
* Log a security event.
|
|
*
|
|
* @param string $transactionId Transaction ID
|
|
* @param string $eventType Security event type
|
|
* @param array $data Event data
|
|
*/
|
|
public function logSecurityEvent(string $transactionId, string $eventType, array $data = []): void
|
|
{
|
|
$entry = [
|
|
'event_type' => 'security',
|
|
'transaction_id' => $transactionId,
|
|
'event_subtype' => $eventType,
|
|
'severity' => $data['severity'] ?? 'medium',
|
|
'data' => $data,
|
|
];
|
|
$this->writeLogEntry($entry);
|
|
}
|
|
|
|
/**
|
|
* Log a message with specified level.
|
|
*
|
|
* @param string $level Log level (info, warning, error)
|
|
* @param string $message Message to log
|
|
* @param array $data Additional data
|
|
*/
|
|
private function logMessage(string $level, string $message, array $data = []): void
|
|
{
|
|
$entry = [
|
|
'event_type' => 'log',
|
|
'level' => $level,
|
|
'message' => $message,
|
|
'data' => $data,
|
|
];
|
|
$this->writeLogEntry($entry);
|
|
}
|
|
|
|
/**
|
|
* Log an informational message.
|
|
*
|
|
* @param string $message Message to log
|
|
* @param array $data Additional data
|
|
*/
|
|
public function logInfo(string $message, array $data = []): void
|
|
{
|
|
$this->logMessage('info', $message, $data);
|
|
}
|
|
|
|
/**
|
|
* Log a warning message.
|
|
*
|
|
* @param string $message Message to log
|
|
* @param array $data Additional data
|
|
*/
|
|
public function logWarning(string $message, array $data = []): void
|
|
{
|
|
$this->logMessage('warning', $message, $data);
|
|
}
|
|
|
|
/**
|
|
* Log an error message.
|
|
*
|
|
* @param string $message Message to log
|
|
* @param array $data Additional data
|
|
*/
|
|
public function logError(string $message, array $data = []): void
|
|
{
|
|
$this->logMessage('error', $message, $data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Audit transaction context manager.
|
|
*/
|
|
class AuditTransaction
|
|
{
|
|
private AuditLogger $logger;
|
|
private string $transactionId;
|
|
private string $operation;
|
|
private array $context;
|
|
private float $startTime;
|
|
|
|
public function __construct(
|
|
AuditLogger $logger,
|
|
string $transactionId,
|
|
string $operation,
|
|
array $context = []
|
|
) {
|
|
$this->logger = $logger;
|
|
$this->transactionId = $transactionId;
|
|
$this->operation = $operation;
|
|
$this->context = $context;
|
|
$this->startTime = microtime(true);
|
|
|
|
// Log transaction start
|
|
$this->logger->logEvent($this->transactionId, 'transaction_start', [
|
|
'operation' => $this->operation,
|
|
'context' => $this->context,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get the transaction ID.
|
|
*
|
|
* @return string Transaction ID
|
|
*/
|
|
public function getTransactionId(): string
|
|
{
|
|
return $this->transactionId;
|
|
}
|
|
|
|
/**
|
|
* Log an event within this transaction.
|
|
*
|
|
* @param string $eventType Event type
|
|
* @param array $data Event data
|
|
*/
|
|
public function logEvent(string $eventType, array $data = []): void
|
|
{
|
|
$this->logger->logEvent($this->transactionId, $eventType, $data);
|
|
}
|
|
|
|
/**
|
|
* Log a security event within this transaction.
|
|
*
|
|
* @param string $eventType Security event type
|
|
* @param array $data Event data
|
|
*/
|
|
public function logSecurityEvent(string $eventType, array $data = []): void
|
|
{
|
|
$this->logger->logSecurityEvent($this->transactionId, $eventType, $data);
|
|
}
|
|
|
|
/**
|
|
* End the transaction.
|
|
*
|
|
* @param string|null $status Transaction status (success|failure)
|
|
* @param array $result Transaction result data
|
|
*/
|
|
public function end(?string $status = 'success', array $result = []): void
|
|
{
|
|
$duration = microtime(true) - $this->startTime;
|
|
|
|
$this->logger->logEvent($this->transactionId, 'transaction_end', [
|
|
'operation' => $this->operation,
|
|
'status' => $status,
|
|
'duration_seconds' => round($duration, 3),
|
|
'result' => $result,
|
|
]);
|
|
|
|
$this->logger->endTransaction($this->transactionId);
|
|
}
|
|
}
|