table name * * @var array */ private const TABLE_NAMES = [ self::RESOURCE_ACTION => 'actions', self::RESOURCE_AUTHENTICATION => 'config', self::RESOURCE_AUTH_TOKEN => 'token', self::RESOURCE_AUTOREGISTRATION => 'config', self::RESOURCE_CONNECTOR => 'connector', self::RESOURCE_CORRELATION => 'correlation', self::RESOURCE_DASHBOARD => 'dashboard', self::RESOURCE_LLD_RULE => 'items', self::RESOURCE_HOST => 'hosts', self::RESOURCE_HOST_GROUP => 'hstgrp', self::RESOURCE_HOST_PROTOTYPE => 'hosts', self::RESOURCE_HOUSEKEEPING => 'config', self::RESOURCE_ICON_MAP => 'icon_map', self::RESOURCE_IMAGE => 'images', self::RESOURCE_ITEM => 'items', self::RESOURCE_ITEM_PROTOTYPE => 'items', self::RESOURCE_IT_SERVICE => 'services', self::RESOURCE_MACRO => 'globalmacro', self::RESOURCE_MAINTENANCE => 'maintenances', self::RESOURCE_MEDIA_TYPE => 'media_type', self::RESOURCE_MODULE => 'module', self::RESOURCE_PROXY => 'proxy', self::RESOURCE_REGEXP => 'regexps', self::RESOURCE_SCENARIO => 'httptest', self::RESOURCE_SCHEDULED_REPORT => 'report', self::RESOURCE_SCRIPT => 'scripts', self::RESOURCE_SETTINGS => 'config', self::RESOURCE_SLA => 'sla', self::RESOURCE_TEMPLATE => 'hosts', self::RESOURCE_TEMPLATE_DASHBOARD => 'dashboard', self::RESOURCE_TEMPLATE_GROUP => 'hstgrp', self::RESOURCE_USER => 'users', self::RESOURCE_USERDIRECTORY => 'userdirectory', self::RESOURCE_USER_GROUP => 'usrgrp' ]; /** * ID field names of audit resources. * resource => ID field name * * @var array */ private const ID_FIELD_NAMES = [ self::RESOURCE_PROXY => 'proxyid', self::RESOURCE_TEMPLATE => 'templateid' ]; /** * Name field names of audit resources. * resource => name field * * @var array */ private const FIELD_NAMES = [ self::RESOURCE_ACTION => 'name', self::RESOURCE_AUTHENTICATION => null, self::RESOURCE_AUTH_TOKEN => 'name', self::RESOURCE_AUTOREGISTRATION => null, self::RESOURCE_CONNECTOR => 'name', self::RESOURCE_CORRELATION => 'name', self::RESOURCE_DASHBOARD => 'name', self::RESOURCE_LLD_RULE => 'name', self::RESOURCE_HOST => 'host', self::RESOURCE_HOST_GROUP => 'name', self::RESOURCE_HOST_PROTOTYPE => 'host', self::RESOURCE_HOUSEKEEPING => null, self::RESOURCE_ICON_MAP => 'name', self::RESOURCE_IMAGE => 'name', self::RESOURCE_ITEM => 'name', self::RESOURCE_ITEM_PROTOTYPE => 'name', self::RESOURCE_IT_SERVICE => 'name', self::RESOURCE_MACRO => 'macro', self::RESOURCE_MAINTENANCE => 'name', self::RESOURCE_MEDIA_TYPE => 'name', self::RESOURCE_MODULE => 'id', self::RESOURCE_PROXY => 'name', self::RESOURCE_REGEXP => 'name', self::RESOURCE_SCENARIO => 'name', self::RESOURCE_SCHEDULED_REPORT => 'name', self::RESOURCE_SCRIPT => 'name', self::RESOURCE_SETTINGS => null, self::RESOURCE_SLA => 'name', self::RESOURCE_TEMPLATE => 'host', self::RESOURCE_TEMPLATE_DASHBOARD => 'name', self::RESOURCE_TEMPLATE_GROUP => 'name', self::RESOURCE_USER => 'username', self::RESOURCE_USERDIRECTORY => 'name', self::RESOURCE_USER_GROUP => 'name' ]; /** * API names of audit resources. * resource => API name * * @var array */ private const API_NAMES = [ self::RESOURCE_ACTION => 'action', self::RESOURCE_AUTHENTICATION => 'authentication', self::RESOURCE_AUTH_TOKEN => 'token', self::RESOURCE_AUTOREGISTRATION => 'autoregistration', self::RESOURCE_CONNECTOR => 'connector', self::RESOURCE_CORRELATION => 'correlation', self::RESOURCE_DASHBOARD => 'dashboard', self::RESOURCE_LLD_RULE => 'discoveryrule', self::RESOURCE_HOST => 'host', self::RESOURCE_HOST_GROUP => 'hostgroup', self::RESOURCE_HOST_PROTOTYPE => 'hostprototype', self::RESOURCE_HOUSEKEEPING => 'housekeeping', self::RESOURCE_ICON_MAP => 'iconmap', self::RESOURCE_IMAGE => 'image', self::RESOURCE_ITEM => 'item', self::RESOURCE_ITEM_PROTOTYPE => 'itemprototype', self::RESOURCE_IT_SERVICE => 'service', self::RESOURCE_MACRO => 'usermacro', self::RESOURCE_MAINTENANCE => 'maintenance', self::RESOURCE_MEDIA_TYPE => 'mediatype', self::RESOURCE_MODULE => 'module', self::RESOURCE_PROXY => 'proxy', self::RESOURCE_REGEXP => 'regexp', self::RESOURCE_SCHEDULED_REPORT => 'report', self::RESOURCE_SCRIPT => 'script', self::RESOURCE_SETTINGS => 'settings', self::RESOURCE_SLA => 'sla', self::RESOURCE_TEMPLATE => 'template', self::RESOURCE_TEMPLATE_DASHBOARD => 'templatedashboard', self::RESOURCE_TEMPLATE_GROUP => 'templategroup', self::RESOURCE_USER => 'user', self::RESOURCE_USERDIRECTORY => 'userdirectory', self::RESOURCE_USER_GROUP => 'usergroup' ]; /** * Array of abstract paths that should be masked in audit details. * * @var array */ private const MASKED_PATHS = [ self::RESOURCE_AUTH_TOKEN => ['paths' => ['token.token']], self::RESOURCE_AUTOREGISTRATION => [ 'paths' => ['autoregistration.tls_psk_identity', 'autoregistration.tls_psk'] ], self::RESOURCE_CONNECTOR => [ [ 'paths' => ['connector.password'], 'conditions' => [ 'authtype' => [ZBX_HTTP_AUTH_BASIC, ZBX_HTTP_AUTH_NTLM, ZBX_HTTP_AUTH_KERBEROS, ZBX_HTTP_AUTH_DIGEST ] ] ], [ 'paths' => ['connector.token'], 'conditions' => ['authtype' => ZBX_HTTP_AUTH_BEARER] ], ['paths' => ['connector.ssl_key_password']] ], self::RESOURCE_LLD_RULE => [ [ 'paths' => ['discoveryrule.password'], 'conditions' => [ [ 'type' => [ITEM_TYPE_SIMPLE, ITEM_TYPE_DB_MONITOR, ITEM_TYPE_SSH, ITEM_TYPE_TELNET, ITEM_TYPE_JMX ] ], [ 'type' => ITEM_TYPE_HTTPAGENT, 'authtype' => [ZBX_HTTP_AUTH_BASIC, ZBX_HTTP_AUTH_NTLM, ZBX_HTTP_AUTH_KERBEROS, ZBX_HTTP_AUTH_DIGEST ] ] ] ], ['paths' => ['discoveryrule.ssl_key_password'], 'conditions' => ['type' => ITEM_TYPE_HTTPAGENT]] ], self::RESOURCE_HOST_PROTOTYPE => [ 'paths' => ['hostprototype.macros.value'], 'conditions' => ['type' => ZBX_MACRO_TYPE_SECRET] ], self::RESOURCE_ITEM => [ [ 'paths' => ['item.password'], 'conditions' => [ [ 'type' => [ITEM_TYPE_SIMPLE, ITEM_TYPE_DB_MONITOR, ITEM_TYPE_SSH, ITEM_TYPE_TELNET, ITEM_TYPE_JMX ] ], [ 'type' => ITEM_TYPE_HTTPAGENT, 'authtype' => [ZBX_HTTP_AUTH_BASIC, ZBX_HTTP_AUTH_NTLM, ZBX_HTTP_AUTH_KERBEROS, ZBX_HTTP_AUTH_DIGEST ] ] ] ], ['paths' => ['item.ssl_key_password'], 'conditions' => ['type' => ITEM_TYPE_HTTPAGENT]] ], self::RESOURCE_ITEM_PROTOTYPE => [ [ 'paths' => ['itemprototype.password'], 'conditions' => [ [ 'type' => [ITEM_TYPE_SIMPLE, ITEM_TYPE_DB_MONITOR, ITEM_TYPE_SSH, ITEM_TYPE_TELNET, ITEM_TYPE_JMX ] ], [ 'type' => ITEM_TYPE_HTTPAGENT, 'authtype' => [ZBX_HTTP_AUTH_BASIC, ZBX_HTTP_AUTH_NTLM, ZBX_HTTP_AUTH_KERBEROS, ZBX_HTTP_AUTH_DIGEST ] ] ] ], ['paths' => ['itemprototype.ssl_key_password'], 'conditions' => ['type' => ITEM_TYPE_HTTPAGENT]] ], self::RESOURCE_MACRO => [ 'paths' => ['usermacro.value'], 'conditions' => ['type' => ZBX_MACRO_TYPE_SECRET] ], self::RESOURCE_MEDIA_TYPE => ['paths' => ['mediatype.passwd']], self::RESOURCE_PROXY => ['paths' => ['proxy.tls_psk_identity', 'proxy.tls_psk']], self::RESOURCE_SCRIPT => ['paths' => ['script.password']], self::RESOURCE_TEMPLATE => [ 'paths' => ['template.macros.value'], 'conditions' => ['type' => ZBX_MACRO_TYPE_SECRET] ], self::RESOURCE_USER => ['paths' => ['user.passwd']], self::RESOURCE_USERDIRECTORY => ['paths' => ['userdirectory.bind_password']] ]; /** * Table names of nested objects to check default values. * abstract path => table name * * @var array */ private const NESTED_OBJECTS_TABLE_NAMES = [ 'action.filter' => 'actions', 'action.filter.conditions' => 'conditions', 'action.operations' => 'operations', 'action.operations.opconditions' => 'opconditions', 'action.operations.opmessage' => 'opmessage', 'action.operations.opmessage_grp' => 'opmessage_grp', 'action.operations.opmessage_usr' => 'opmessage_usr', 'action.operations.opcommand' => 'opcommand', 'action.operations.opcommand_grp' => 'opcommand_grp', 'action.operations.opcommand_hst' => 'opcommand_hst', 'action.operations.opgroup' => 'opgroup', 'action.operations.optemplate' => 'optemplate', 'action.operations.opinventory' => 'opinventory', 'action.operations.optag' => 'optag', 'action.recovery_operations' => 'operations', 'action.recovery_operations.opmessage' => 'opmessage', 'action.recovery_operations.opmessage_grp' => 'opmessage_grp', 'action.recovery_operations.opmessage_usr' => 'opmessage_usr', 'action.recovery_operations.opcommand' => 'opcommand', 'action.recovery_operations.opcommand_grp' => 'opcommand_grp', 'action.recovery_operations.opcommand_hst' => 'opcommand_hst', 'action.update_operations' => 'operations', 'action.update_operations.opmessage' => 'opmessage', 'action.update_operations.opmessage_grp' => 'opmessage_grp', 'action.update_operations.opmessage_usr' => 'opmessage_usr', 'action.update_operations.opcommand' => 'opcommand', 'action.update_operations.opcommand_grp' => 'opcommand_grp', 'action.update_operations.opcommand_hst' => 'opcommand_hst', 'connector.tags' => 'connector_tag', 'correlation.filter' => 'correlation', 'correlation.filter.conditions' => 'corr_condition', 'correlation.operations' => 'corr_operation', 'dashboard.users' => 'dashboard_user', 'dashboard.userGroups' => 'dashboard_usrgrp', 'dashboard.pages' => 'dashboard_page', 'dashboard.pages.widgets' => 'widget', 'dashboard.pages.widgets.fields' => 'widget_field', 'discoveryrule.filter' => 'items', 'discoveryrule.filter.conditions' => 'item_condition', 'discoveryrule.lld_macro_paths' => 'lld_macro_path', 'discoveryrule.overrides' => 'lld_override', 'discoveryrule.overrides.filter' => 'lld_override', 'discoveryrule.overrides.filter.conditions' => 'lld_override_condition', 'discoveryrule.overrides.operations' => 'lld_override_operation', 'discoveryrule.overrides.operations.opdiscover' => 'lld_override_opdiscover', 'discoveryrule.overrides.operations.ophistory' => 'lld_override_ophistory', 'discoveryrule.overrides.operations.opinventory' => 'lld_override_opinventory', 'discoveryrule.overrides.operations.opperiod' => 'lld_override_opperiod', 'discoveryrule.overrides.operations.opseverity' => 'lld_override_opseverity', 'discoveryrule.overrides.operations.opstatus' => 'lld_override_opstatus', 'discoveryrule.overrides.operations.optag' => 'lld_override_optag', 'discoveryrule.overrides.operations.optemplate' => 'lld_override_optemplate', 'discoveryrule.overrides.operations.optrends' => 'lld_override_optrends', 'discoveryrule.parameters' => 'item_parameter', 'discoveryrule.preprocessing' => 'item_preproc', 'hostgroup.hosts' => 'hosts_groups', 'hostprototype.groupLinks' => 'group_prototype', 'hostprototype.groupPrototypes' => 'group_prototype', 'hostprototype.interfaces' => 'interface', 'hostprototype.interfaces.details' => 'interface_snmp', 'hostprototype.macros' => 'hostmacro', 'hostprototype.tags' => 'host_tag', 'hostprototype.templates' => 'hosts_templates', 'iconmap.mappings' => 'icon_mapping', 'item.parameters' => 'item_parameter', 'item.preprocessing' => 'item_preproc', 'item.tags' => 'item_tag', 'itemprototype.parameters' => 'item_parameter', 'itemprototype.preprocessing' => 'item_preproc', 'itemprototype.tags' => 'item_tag', 'maintenance.groups' => 'maintenances_groups', 'maintenance.hosts' => 'maintenances_hosts', 'maintenance.tags' => 'maintenance_tag', 'maintenance.timeperiods' => 'timeperiods', 'mediatype.message_templates' => 'media_type_message', 'mediatype.parameters' => 'media_type_param', 'proxy.hosts' => 'hosts', 'proxy.interface' => 'interface', 'regexp.expressions' => 'expressions', 'report.users' => 'report_user', 'report.user_groups' => 'report_usrgrp', 'service.children' => 'services_links', 'service.parents' => 'services_links', 'service.problem_tags' => 'service_problem_tag', 'service.status_rules' => 'service_status_rule', 'service.tags' => 'service_tag', 'sla.service_tags' => 'sla_service_tag', 'sla.schedule' => 'sla_schedule', 'sla.excluded_downtimes' => 'sla_excluded_downtime', 'script.parameters' => 'script_param', 'template.groups' => 'hosts_groups', 'template.macros' => 'hostmacro', 'template.tags' => 'host_tag', 'template.templates' => 'hosts_templates', 'template.templates_clear' => 'hosts_templates', 'templatedashboard.pages' => 'dashboard_page', 'templatedashboard.pages.widgets' => 'widget', 'templatedashboard.pages.widgets.fields' => 'widget_field', 'templategroup.templates' => 'hosts_groups', 'user.medias' => 'media', 'user.usrgrps' => 'users_groups', 'userdirectory.provision_media' => 'userdirectory_media', 'userdirectory.provision_groups' => 'userdirectory_idpgroup', 'userdirectory.provision_groups.user_groups' => 'userdirectory_usrgrp', 'usergroup.hostgroup_rights' => 'rights', 'usergroup.templategroup_rights' => 'rights', 'usergroup.tag_filters' => 'tag_filter', 'usergroup.users' => 'users_groups' ]; /** * ID field names of nested objects that stored in a parent object properties containing an array of nested objects. * abstract path => id field name * * @var array */ private const NESTED_OBJECTS_ID_FIELD_NAMES = [ 'action.filter.conditions' => 'conditionid', 'action.operations' => 'operationid', 'action.operations.opconditions' => 'opconditionid', 'action.operations.opmessage_grp' => 'opmessage_grpid', 'action.operations.opmessage_usr' => 'opmessage_usrid', 'action.operations.opcommand_grp' => 'opcommand_grpid', 'action.operations.opcommand_hst' => 'opcommand_hstid', 'action.operations.opgroup' => 'opgroupid', 'action.operations.optemplate' => 'optemplateid', 'action.operations.optag' => 'optagid', 'action.recovery_operations' => 'operationid', 'action.recovery_operations.opmessage_grp' => 'opmessage_grpid', 'action.recovery_operations.opmessage_usr' => 'opmessage_usrid', 'action.recovery_operations.opcommand_grp' => 'opcommand_grpid', 'action.recovery_operations.opcommand_hst' => 'opcommand_hstid', 'action.update_operations' => 'operationid', 'action.update_operations.opmessage_grp' => 'opmessage_grpid', 'action.update_operations.opmessage_usr' => 'opmessage_usrid', 'action.update_operations.opcommand_grp' => 'opcommand_grpid', 'action.update_operations.opcommand_hst' => 'opcommand_hstid', 'connector.tags' => 'connector_tagid', 'correlation.filter.conditions' => 'corr_conditionid', 'correlation.operations' => 'corr_operationid', 'dashboard.users' => 'dashboard_userid', 'dashboard.userGroups' => 'dashboard_usrgrpid', 'dashboard.pages' => 'dashboard_pageid', 'dashboard.pages.widgets' => 'widgetid', 'dashboard.pages.widgets.fields' => 'widget_fieldid', 'discoveryrule.filter.conditions' => 'item_conditionid', 'discoveryrule.lld_macro_paths' => 'lld_macro_pathid', 'discoveryrule.overrides' => 'lld_overrideid', 'discoveryrule.overrides.filter.conditions' => 'lld_override_conditionid', 'discoveryrule.overrides.operations' => 'lld_override_operationid', 'discoveryrule.overrides.operations.optag' => 'lld_override_optagid', 'discoveryrule.overrides.operations.optemplate' => 'lld_override_optemplateid', 'discoveryrule.parameters' => 'item_parameterid', 'discoveryrule.preprocessing' => 'item_preprocid', 'hostgroup.hosts' => 'hostgroupid', 'hostprototype.groupLinks' => 'group_prototypeid', 'hostprototype.groupPrototypes' => 'group_prototypeid', 'hostprototype.interfaces' => 'interfaceid', 'hostprototype.macros' => 'hostmacroid', 'hostprototype.tags' => 'hosttagid', 'hostprototype.templates' => 'hosttemplateid', 'iconmap.mappings' => 'iconmappingid', 'item.parameters' => 'item_parameterid', 'item.preprocessing' => 'item_preprocid', 'item.tags' => 'itemtagid', 'itemprototype.parameters' => 'item_parameterid', 'itemprototype.preprocessing' => 'item_preprocid', 'itemprototype.tags' => 'itemtagid', 'maintenance.groups' => 'maintenance_groupid', 'maintenance.hosts' => 'maintenance_hostid', 'maintenance.tags' => 'maintenancetagid', 'maintenance.timeperiods' => 'timeperiodid', 'mediatype.message_templates' => 'mediatype_messageid', 'mediatype.parameters' => 'mediatype_paramid', 'proxy.hosts' => 'hostid', 'regexp.expressions' => 'expressionid', 'report.users' => 'reportuserid', 'report.user_groups' => 'reportusrgrpid', 'script.parameters' => 'script_paramid', 'service.children' => 'linkid', 'service.parents' => 'linkid', 'service.problem_tags' => 'service_problem_tagid', 'service.status_rules' => 'service_status_ruleid', 'service.tags' => 'servicetagid', 'sla.service_tags' => 'sla_service_tagid', 'sla.schedule' => 'sla_scheduleid', 'sla.excluded_downtimes' => 'sla_excluded_downtimeid', 'template.groups' => 'hostgroupid', 'template.macros' => 'hostmacroid', 'template.tags' => 'hosttagid', 'template.templates' => 'hosttemplateid', 'template.templates_clear' => 'hosttemplateid', 'templatedashboard.pages' => 'dashboard_pageid', 'templatedashboard.pages.widgets' => 'widgetid', 'templatedashboard.pages.widgets.fields' => 'widget_fieldid', 'templategroup.templates' => 'hostgroupid', 'user.medias' => 'mediaid', 'user.usrgrps' => 'id', 'userdirectory.provision_media' => 'userdirectory_mediaid', 'userdirectory.provision_groups' => 'userdirectory_idpgroupid', 'userdirectory.provision_groups.user_groups' => 'userdirectory_usrgrpid', 'usergroup.hostgroup_rights' => 'rightid', 'usergroup.templategroup_rights' => 'rightid', 'usergroup.tag_filters' => 'tag_filterid', 'usergroup.users' => 'id' ]; /** * Array of abstract paths that should be skipped in audit details. * * @var array */ private const SKIP_FIELDS = ['token.creator_userid', 'token.created_at']; /** * Array of abstract paths that contain blob fields. * * @var array */ private const BLOB_FIELDS = ['image.image']; /** * Array of abstract paths that can only contain a data to delete. */ private const DELETE_ONLY_FIELDS = ['template.templates_clear']; /** * Add audit records. * * @param string|null $userid * @param string $ip * @param string $username * @param int $action CAudit::ACTION_* * @param int $resource CAudit::RESOURCE_* * @param array $objects * @param array $db_objects */ public static function log(?string $userid, string $ip, string $username, int $action, int $resource, array $objects, array $db_objects): void { if (!self::isAuditEnabled() && ($resource != self::RESOURCE_SETTINGS || !array_key_exists(CSettingsHelper::AUDITLOG_ENABLED, current($objects)))) { return; } $auditlog = []; $clock = time(); $ip = substr($ip, 0, DB::getFieldLength('auditlog', 'ip')); $recordsetid = self::getRecordSetId(); switch ($action) { case self::ACTION_LOGOUT: case self::ACTION_LOGIN_SUCCESS: case self::ACTION_LOGIN_FAILED: $auditlog[] = [ 'userid' => $userid, 'username' => $username, 'clock' => $clock, 'ip' => $ip, 'action' => $action, 'resourcetype' => $resource, 'resourceid' => $userid, 'resourcename' => '', 'recordsetid' => $recordsetid, 'details' => '' ]; break; default: $table_key = array_key_exists($resource, self::ID_FIELD_NAMES) ? self::ID_FIELD_NAMES[$resource] : DB::getPk(self::TABLE_NAMES[$resource]); foreach ($objects as $object) { $resourceid = $object[$table_key]; $db_object = ($action == self::ACTION_UPDATE) ? $db_objects[$resourceid] : []; $resource_name = self::getResourceName($resource, $action, $object, $db_object); $diff = self::handleObjectDiff($resource, $action, $object, $db_object); if ($action == self::ACTION_UPDATE && count($diff) === 0) { continue; } $auditlog[] = [ 'userid' => $userid, 'username' => $username, 'clock' => $clock, 'ip' => $ip, 'action' => $action, 'resourcetype' => $resource, 'resourceid' => $resourceid, 'resourcename' => $resource_name, 'recordsetid' => $recordsetid, 'details' => (count($diff) == 0) ? '' : json_encode($diff) ]; } } DB::insertBatch('auditlog', $auditlog); } /** * Return recordsetid. Generate recordsetid if it has not been generated yet. * * @return string */ private static function getRecordSetId(): string { static $recordsetid = null; if ($recordsetid === null) { $recordsetid = CCuid::generate(); } return $recordsetid; } /** * Check audit logging is enabled. * * @return bool */ private static function isAuditEnabled(): bool { return CSettingsHelper::get(CSettingsHelper::AUDITLOG_ENABLED) == self::AUDITLOG_ENABLE; } /** * Return resource name of logging object. * * @param int $resource * @param int $action * @param array $object * @param array $db_object * * @return string */ private static function getResourceName(int $resource, int $action, array $object, array $db_object): string { $field_name = self::FIELD_NAMES[$resource]; $resource_name = ($field_name !== null) ? (($action == self::ACTION_UPDATE) ? $db_object[$field_name] : $object[$field_name]) : ''; if (mb_strlen($resource_name) > 255) { $resource_name = mb_substr($resource_name, 0, 252).'...'; } return $resource_name; } /** * Prepares the details for audit log. * * @param int $resource * @param int $action * @param array $object * @param array $db_object * * @return array */ private static function handleObjectDiff(int $resource, int $action, array $object, array $db_object): array { if (!in_array($action, [self::ACTION_ADD, self::ACTION_UPDATE])) { return []; } $api_name = self::API_NAMES[$resource]; $details = self::convertKeysToPaths($api_name, $object); switch ($action) { case self::ACTION_ADD: return self::handleAdd($resource, $details); case self::ACTION_UPDATE: $db_details = self::convertKeysToPaths($api_name, self::intersectObjects($api_name, $db_object, $object) ); return self::handleUpdate($resource, $details, $db_details); } } /** * Computes the intersection of $db_object and $object using keys for comparison. * Recursively removes $db_object properties if they are not present in $object. * * @param string $path * @param array $db_object * @param array $object * * @return array */ private static function intersectObjects(string $path, array $db_object, array $object): array { foreach ($db_object as $db_key => &$db_value) { if (is_string($db_key) && !array_key_exists($db_key, $object)) { unset($db_object[$db_key]); continue; } if (!is_array($db_value)) { continue; } $key = $db_key; $subpath = $path.'.'.$db_key; if (is_int($db_key)) { $key = null; $pk = self::NESTED_OBJECTS_ID_FIELD_NAMES[$path]; foreach ($object as $i => $nested_object) { if (bccomp($nested_object[$pk], $db_key) == 0) { $key = $i; $subpath = $path; break; } } if ($key === null) { continue; } } $db_value = self::intersectObjects($subpath, $db_value, $object[$key]); } unset($db_value); return $db_object; } /** * Checks by path, whether the value of the object should be masked. * * @param int $resource * @param string $path * @param array $object * * @return bool */ private static function isValueToMask(int $resource, string $path, array $object): bool { if (!array_key_exists($resource, self::MASKED_PATHS)) { return false; } $object_path = self::getLastObjectPath($path); $abstract_path = self::getAbstractPath($path); $rules = []; if (array_key_exists('paths', self::MASKED_PATHS[$resource])) { if (in_array($abstract_path, self::MASKED_PATHS[$resource]['paths'])) { $rules = self::MASKED_PATHS[$resource]; } } else { foreach (self::MASKED_PATHS[$resource] as $_rules) { if (in_array($abstract_path, $_rules['paths'])) { $rules = $_rules; break; } } } if (!$rules) { return false; } if (!array_key_exists('conditions', $rules)) { return true; } $or_conditions = $rules['conditions']; if (!array_key_exists(0, $or_conditions)) { $or_conditions = [$or_conditions]; } foreach ($or_conditions as $and_conditions) { $all_conditions = count($and_conditions); $true_conditions = 0; foreach ($and_conditions as $condition_key => $value) { $condition_path = $object_path.'.'.$condition_key; if (array_key_exists($condition_path, $object)) { $values = is_array($value) ? $value : [$value]; if (in_array($object[$condition_path], $values)) { $true_conditions++; } } } if ($true_conditions == $all_conditions) { return true; } } return false; } /** * Converts the object properties to the one-dimensional array where the key is a path. * * @param string $path Path to object or to array of objects. * @param array $object The object or array of objects to convert. * * @return array */ private static function convertKeysToPaths(string $path, array $object): array { $result = []; $is_field_of_another_object = strpos($path, '.') !== false && !preg_match('/\[[0-9]+\]$/', $path); $is_array_of_objects = false; if ($is_field_of_another_object) { $abstract_path = self::getAbstractPath($path); $is_array_of_objects = array_key_exists($abstract_path, self::NESTED_OBJECTS_ID_FIELD_NAMES); if ($is_array_of_objects) { $id_field_name = self::NESTED_OBJECTS_ID_FIELD_NAMES[$abstract_path]; } } if ($is_array_of_objects) { $objects = $object; foreach ($objects as $object) { $path_to_object = $path.'['.$object[$id_field_name].']'; $result += self::convertKeysToPaths($path_to_object, $object); } } else { foreach ($object as $field => $value) { $path_to_field = $path.'.'.$field; if (in_array(self::getAbstractPath($path_to_field), self::SKIP_FIELDS)) { continue; } if (is_array($value)) { $result += self::convertKeysToPaths($path_to_field, $value); } else { $result[$path_to_field] = (string) $value; } } } return $result; } /** * Checks by path, whether the value is equal to default value from the database schema. * * @param int $resource * @param string $path * @param string $value * * @return bool */ private static function isDefaultValue(int $resource, string $path, string $value): bool { $object_path = self::getLastObjectPath($path); $table_name = self::TABLE_NAMES[$resource]; if ($object_path !== self::API_NAMES[$resource]) { $table_name = self::NESTED_OBJECTS_TABLE_NAMES[self::getAbstractPath($object_path)]; } $schema_fields = DB::getSchema($table_name)['fields']; $field_name = substr($path, strrpos($path, '.') + 1); if (!array_key_exists($field_name, $schema_fields)) { return false; } if ($schema_fields[$field_name]['type'] === DB::FIELD_TYPE_ID && $schema_fields[$field_name]['null'] && $value == 0) { return true; } if (!array_key_exists('default', $schema_fields[$field_name])) { return false; } return $value == $schema_fields[$field_name]['default']; } /** * Checks whether a path is path to nested object property. * * @param string $path * * @return bool */ private static function isNestedObjectProperty(string $path): bool { return (count(explode('.', $path)) > 2); } /** * Return the path to the parent property object from the passed path. * * @param string $path * * @return string */ private static function getLastObjectPath(string $path): string { return substr($path, 0, strrpos($path, '.')); } /** * Return the abstract path (without indexes). * * @param string $path * * @return string */ private static function getAbstractPath(string $path): string { if (strpos($path, '[') !== false) { $path = preg_replace('/\[[0-9]+\]/', '', $path); } return $path; } /** * Return the paths to nested object properties from the paths of passing object. * * @param array $object * * @return array */ private static function getNestedObjectsPaths(array $object): array { $paths = []; foreach ($object as $path => $foo) { if (!self::isNestedObjectProperty($path)) { continue; } $object_path = self::getLastObjectPath($path); if (!in_array($object_path, $paths)) { $paths[] = $object_path; } } return $paths; } /** * Prepares the audit details for add action. * * @param int $resource * @param array $object * * @return array */ private static function handleAdd(int $resource, array $object): array { $result = []; foreach ($object as $path => $value) { if (self::isNestedObjectProperty($path)) { $result[self::getLastObjectPath($path)] = [self::DETAILS_ACTION_ADD]; } if (self::isValueToMask($resource, $path, $object)) { $result[$path] = [self::DETAILS_ACTION_ADD, ZBX_SECRET_MASK]; continue; } if (self::isDefaultValue($resource, $path, $value)) { continue; } if (in_array(self::getAbstractPath($path), self::BLOB_FIELDS)) { $result[$path] = [self::DETAILS_ACTION_ADD]; continue; } $result[$path] = [self::DETAILS_ACTION_ADD, $value]; } return $result; } /** * Prepares the audit details for update action. * * @param int $resource * @param array $object * @param array $db_object * * @return array */ private static function handleUpdate(int $resource, array $object, array $db_object): array { $result = []; $nested_objects_paths = self::getNestedObjectsPaths($object); $db_nested_objects_paths = self::getNestedObjectsPaths($db_object); foreach ($db_nested_objects_paths as $path) { if (!in_array($path, $nested_objects_paths)) { $result[$path] = [self::DETAILS_ACTION_DELETE]; } } foreach ($nested_objects_paths as $path) { if (!in_array($path, $db_nested_objects_paths)) { if (in_array(self::getAbstractPath($path), self::DELETE_ONLY_FIELDS)) { $result[$path] = [self::DETAILS_ACTION_DELETE]; foreach ($object as $object_path => $value) { if (substr($object_path, 0, strlen($path)) === $path) { unset($object[$object_path]); } } } else { $result[$path] = [self::DETAILS_ACTION_ADD]; } } } foreach ($object as $path => $value) { $is_value_to_mask = self::isValueToMask($resource, $path, $object); $db_value = array_key_exists($path, $db_object) ? $db_object[$path] : null; if ($db_value === null) { $is_value_to_mask = self::isValueToMask($resource, $path, $object); if ($is_value_to_mask) { $result[$path] = [self::DETAILS_ACTION_ADD, ZBX_SECRET_MASK]; continue; } if (self::isDefaultValue($resource, $path, $value)) { continue; } if (in_array(self::getAbstractPath($path), self::BLOB_FIELDS)) { $result[$path] = [self::DETAILS_ACTION_ADD]; continue; } $result[$path] = [self::DETAILS_ACTION_ADD, $value]; } else { $is_db_value_to_mask = self::isValueToMask($resource, $path, $db_object); if ($value != $db_value || $is_value_to_mask || $is_db_value_to_mask) { if (self::isNestedObjectProperty($path)) { $result[self::getLastObjectPath($path)] = [self::DETAILS_ACTION_UPDATE]; } if (in_array(self::getAbstractPath($path), self::BLOB_FIELDS)) { $result[$path] = [self::DETAILS_ACTION_UPDATE]; continue; } $result[$path] = [ self::DETAILS_ACTION_UPDATE, $is_value_to_mask ? ZBX_SECRET_MASK : $value, $is_db_value_to_mask ? ZBX_SECRET_MASK : $db_value ]; } } } return $result; } }