diff --git a/ports/win32/Microsoft Visual Studio 2019/BACnet_Object_Definitions/BACnet_Object_Definitions.vcxproj b/ports/win32/Microsoft Visual Studio 2019/BACnet_Object_Definitions/BACnet_Object_Definitions.vcxproj index 569386a5..8e78629d 100644 --- a/ports/win32/Microsoft Visual Studio 2019/BACnet_Object_Definitions/BACnet_Object_Definitions.vcxproj +++ b/ports/win32/Microsoft Visual Studio 2019/BACnet_Object_Definitions/BACnet_Object_Definitions.vcxproj @@ -178,6 +178,7 @@ + @@ -207,6 +208,7 @@ + diff --git a/src/bacnet/bacaudit.c b/src/bacnet/bacaudit.c new file mode 100644 index 00000000..53fd1c3a --- /dev/null +++ b/src/bacnet/bacaudit.c @@ -0,0 +1,1095 @@ +/** + * @file + * @brief BACnetActionCommand codec used by Command objects + * @author Steve Karg + * @date November 2024 + * @copyright SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0 + */ +#include +#include +#include +/* BACnet Stack defines - first */ +#include "bacnet/bacdef.h" +/* BACnet Stack API */ +#include "bacnet/bacdcode.h" +#include "bacnet/bactimevalue.h" +#include "bacnet/timestamp.h" +/* me! */ +#include "bacnet/bacaudit.h" + +/** + * @brief Encode the BACnetAuditValue + * @param apdu - buffer of data to be encoded, or NULL for length + * @param value - value to be encoded + * @return the number of apdu bytes encoded + */ +int bacnet_audit_value_encode(uint8_t *apdu, const BACNET_AUDIT_VALUE *value) +{ + /* total length of the apdu, return value */ + int apdu_len = 0; + + if (!value) { + return 0; + } + switch (value->tag) { + case BACNET_APPLICATION_TAG_BOOLEAN: + apdu_len = + encode_application_boolean(apdu, value->type.boolean_value); + break; + case BACNET_APPLICATION_TAG_UNSIGNED_INT: + apdu_len = + encode_application_unsigned(apdu, value->type.unsigned_value); + break; + case BACNET_APPLICATION_TAG_SIGNED_INT: + apdu_len = + encode_application_signed(apdu, value->type.integer_value); + break; + case BACNET_APPLICATION_TAG_REAL: + apdu_len = encode_application_real(apdu, value->type.real_value); + break; + case BACNET_APPLICATION_TAG_ENUMERATED: + apdu_len = encode_application_enumerated( + apdu, value->type.enumerated_value); + break; + case BACNET_APPLICATION_TAG_NULL: + default: + if (apdu) { + apdu[0] = value->tag; + } + apdu_len++; + break; + } + + return apdu_len; +} + +/** + * @brief Encode the BACnetAuditValue + * @param apdu - buffer of data to be encoded, or NULL for length + * @param tag_number - context tag number to be encoded + * @param value - value to be encoded + * @return the number of apdu bytes encoded + */ +int bacnet_audit_value_context_encode( + uint8_t *apdu, uint8_t tag_number, const BACNET_AUDIT_VALUE *value) +{ + int len; + int apdu_len = 0; + + len = encode_opening_tag(apdu, tag_number); + apdu_len += len; + if (apdu) { + apdu += len; + } + len = bacnet_audit_value_encode(apdu, value); + apdu_len += len; + if (apdu) { + apdu += len; + } + len = encode_closing_tag(apdu, tag_number); + apdu_len += len; + + return apdu_len; +} + +/** + * @brief Decode the BACnetAuditValue + * @param apdu - buffer of data to be encoded, or NULL for length + * @param tag_number - context tag number to be encoded + * @param value - value to be encoded + * @return the number of apdu bytes encoded, or BACNET_STATUS_ERROR if an error + * occurs + */ +int bacnet_audit_value_decode( + const uint8_t *apdu, uint32_t apdu_size, BACNET_AUDIT_VALUE *value) +{ + int len = 0; + int apdu_len = 0; + BACNET_TAG tag = { 0 }; + bool boolean_value = false; + float real_value = 0.0f; + uint32_t enumerated_value = 0; + BACNET_UNSIGNED_INTEGER unsigned_value = 0; + int32_t integer_value = 0; + + if (!value) { + return BACNET_STATUS_ERROR; + } + if (!apdu) { + return BACNET_STATUS_ERROR; + } + len = bacnet_tag_decode(apdu, apdu_size, &tag); + if ((len > 0) && tag.application) { + if (value) { + value->tag = tag.number; + } + switch (tag.number) { + case BACNET_APPLICATION_TAG_BOOLEAN: + apdu_len = bacnet_boolean_application_decode( + apdu, apdu_size, &boolean_value); + if (apdu_len > 0) { + if (value) { + value->type.boolean_value = boolean_value; + } + } + break; + case BACNET_APPLICATION_TAG_UNSIGNED_INT: + apdu_len = bacnet_unsigned_application_decode( + apdu, apdu_size, &unsigned_value); + if (apdu_len > 0) { + if (value) { + value->type.unsigned_value = unsigned_value; + } + } + break; + case BACNET_APPLICATION_TAG_SIGNED_INT: + apdu_len = bacnet_signed_application_decode( + apdu, apdu_size, &integer_value); + if (apdu_len > 0) { + if (value) { + value->type.integer_value = integer_value; + } + } + break; + case BACNET_APPLICATION_TAG_REAL: + apdu_len = bacnet_real_application_decode( + apdu, apdu_size, &real_value); + if (apdu_len > 0) { + if (value) { + value->type.real_value = real_value; + } + } + break; + case BACNET_APPLICATION_TAG_ENUMERATED: + apdu_len = bacnet_enumerated_application_decode( + apdu, apdu_size, &enumerated_value); + if (apdu_len > 0) { + if (value) { + value->type.enumerated_value = enumerated_value; + } + } + break; + case BACNET_APPLICATION_TAG_NULL: + default: + apdu_len = len; + break; + } + } else { + apdu_len = BACNET_STATUS_ERROR; + } + + return apdu_len; +} + +/** + * @brief Decode a context encoded BACnetAuditValue from a buffer + * @param apdu - the APDU buffer + * @param apdu_size - the size of the APDU buffer + * @param tag_number - the tag number + * @param value - BACnetAuditValue to decode into + * @return number of bytes decoded, zero if tag mismatch, or + * BACNET_STATUS_ERROR on failure. + */ +int bacnet_audit_value_context_decode( + const uint8_t *apdu, + uint16_t apdu_size, + uint8_t tag_number, + BACNET_AUDIT_VALUE *value) +{ + int len = 0; + int apdu_len = 0; + + if (!apdu) { + return BACNET_STATUS_ERROR; + } + if (!bacnet_is_opening_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, tag_number, &len)) { + return 0; + } + apdu_len += len; + len = + bacnet_audit_value_decode(&apdu[apdu_len], apdu_size - apdu_len, value); + if (len > 0) { + apdu_len += len; + if (bacnet_is_closing_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, tag_number, &len)) { + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + } else { + return BACNET_STATUS_ERROR; + } + + return apdu_len; +} + +/** + * @brief Compare two BACnetActionPropertyValue complex datatypes + * @param value1 [in] The first structure to compare + * @param value2 [in] The second structure to compare + * @return true if the two structures are the same + */ +bool bacnet_audit_value_same( + const BACNET_AUDIT_VALUE *value1, const BACNET_AUDIT_VALUE *value2) +{ + bool status = false; /*return value */ + + if ((value1 == NULL) || (value2 == NULL)) { + return false; + } + if (value1->tag == value2->tag) { + switch (value1->tag) { + case BACNET_APPLICATION_TAG_NULL: + status = true; + break; + case BACNET_APPLICATION_TAG_BOOLEAN: + status = + value1->type.boolean_value == value2->type.boolean_value; + break; + case BACNET_APPLICATION_TAG_UNSIGNED_INT: + status = + value1->type.unsigned_value == value2->type.unsigned_value; + break; + case BACNET_APPLICATION_TAG_SIGNED_INT: + status = + value1->type.integer_value == value2->type.integer_value; + break; + case BACNET_APPLICATION_TAG_REAL: + status = !islessgreater( + value1->type.real_value, value2->type.real_value); + break; + case BACNET_APPLICATION_TAG_ENUMERATED: + status = value1->type.enumerated_value == + value2->type.enumerated_value; + break; + default: + break; + } + } + + return status; +} + +/** + * @brief Encode the BACnetAuditNotification + * @param apdu - buffer of data to be encoded, or NULL for length + * @param value - value to be encoded + * @return the number of apdu bytes encoded + */ +int bacnet_audit_log_notification_encode( + uint8_t *apdu, const BACNET_AUDIT_NOTIFICATION *value) +{ + int len, apdu_len = 0; /* total length of the apdu, return value */ + + if (!value) { + return 0; + } +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_TIMESTAMP_ENABLE + /* source-timestamp [0] BACnetTimeStamp OPTIONAL */ + len = bacapp_encode_context_timestamp(apdu, 0, &value->source_timestamp); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_TIMESTAMP_ENABLE + /* target-timestamp [1] BACnetTimeStamp OPTIONAL */ + len = bacapp_encode_context_timestamp(apdu, 1, &value->target_timestamp); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif + /* source-device [2] BACnetRecipient */ + len = bacnet_recipient_context_encode(apdu, 2, &value->source_device); + apdu_len += len; + if (apdu) { + apdu += len; + } +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_OBJECT_ENABLE + /* source-object [3] BACnetObjectIdentifier OPTIONAL */ + len = encode_context_object_id( + apdu, 3, value->source_object.type, value->source_object.instance); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif + /* operation [4] BACnetAuditOperation */ + len = encode_context_unsigned(apdu, 4, value->operation); + apdu_len += len; + if (apdu) { + apdu += len; + } +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_COMMENT_ENABLE + /* source-comment [5] CharacterString OPTIONAL */ + len = encode_context_character_string(apdu, 5, &value->source_comment); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_COMMENT_ENABLE + /* target-comment [6] CharacterString OPTIONAL */ + len = encode_context_character_string(apdu, 6, &value->target_comment); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_INVOKE_ID_ENABLE + /* invoke-id [7] Unsigned8 OPTIONAL */ + len = encode_context_unsigned(apdu, 7, value->invoke_id); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ID_ENABLE + /* source-user-id [8] Unsigned16 OPTIONAL */ + len = encode_context_unsigned(apdu, 8, value->source_user_id); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ROLE_ENABLE + /* source-user-role [9] Unsigned8 OPTIONAL */ + len = encode_context_unsigned(apdu, 9, value->source_user_role); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif + /* target-device [10] BACnetRecipient */ + len = bacnet_recipient_context_encode(apdu, 10, &value->target_device); + apdu_len += len; + if (apdu) { + apdu += len; + } +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_OBJECT_ENABLE + /* target-object [11] BACnetObjectIdentifier OPTIONAL */ + len = encode_context_object_id( + apdu, 11, value->target_object.type, value->target_object.instance); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PROPERTY_ENABLE + /* target-property [12] BACnetPropertyReference OPTIONAL */ + len = bacnet_property_reference_context_encode( + apdu, 12, &value->target_property); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PRIORITY_ENABLE + /* target-priority [13] Unsigned (1..16) OPTIONAL */ + len = encode_context_unsigned(apdu, 13, value->target_priority); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_VALUE_ENABLE + /* target-value [14] ABSTRACT-SYNTAX.&Type OPTIONAL */ + len = bacnet_audit_value_context_encode(apdu, 14, &value->target_value); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_CURRENT_VALUE_ENABLE + /* current-value [15] ABSTRACT-SYNTAX.&Type OPTIONAL */ + len = bacnet_audit_value_context_encode(apdu, 15, &value->current_value); + apdu_len += len; + if (apdu) { + apdu += len; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_RESULT_ENABLE + /* result [16] Error OPTIONAL */ + len = encode_context_enumerated(apdu, 16, value->result); + apdu_len += len; +#endif + + return apdu_len; +} + +/** + * @brief Decode the BACnetAuditNotification + * @param apdu - buffer of data to be decoded + * @param value - value to hold the decoded data + * @return the number of apdu bytes encoded, or BACNET_STATUS_ERROR if an error + * occurs + */ +int bacnet_audit_log_notification_decode( + const uint8_t *apdu, uint32_t apdu_size, BACNET_AUDIT_NOTIFICATION *value) +{ + int len = 0; + int apdu_len = 0; + BACNET_UNSIGNED_INTEGER unsigned_value; + BACNET_TIMESTAMP timestamp; + BACNET_OBJECT_TYPE object_type; + uint32_t object_instance; + BACNET_CHARACTER_STRING char_string; + BACNET_RECIPIENT recipient; + struct BACnetPropertyReference property_reference; + BACNET_AUDIT_VALUE audit_value; + uint32_t enumerated_value; + + if (!apdu) { + return BACNET_STATUS_ERROR; + } + /* source-timestamp [0] BACnetTimeStamp OPTIONAL */ + len = bacnet_timestamp_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 0, ×tamp); + if (len > 0) { + apdu_len += len; +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_TIMESTAMP_ENABLE + if (value) { + bacapp_timestamp_copy(&value->source_timestamp, ×tamp); + } +#endif + } else if (len < 0) { + return BACNET_STATUS_ERROR; + } + /* target-timestamp [1] BACnetTimeStamp OPTIONAL */ + len = bacnet_timestamp_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 1, ×tamp); + if (len > 0) { + apdu_len += len; +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_TIMESTAMP_ENABLE + if (value) { + bacapp_timestamp_copy(&value->target_timestamp, ×tamp); + } +#endif + } else if (len < 0) { + return BACNET_STATUS_ERROR; + } + /* source-device [2] BACnetRecipient */ + len = bacnet_recipient_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 2, &recipient); + if (len > 0) { + apdu_len += len; + if (value) { + bacnet_recipient_copy(&value->source_device, &recipient); + } + } else { + return BACNET_STATUS_ERROR; + } + /* source-object [3] BACnetObjectIdentifier OPTIONAL */ + len = bacnet_object_id_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 3, &object_type, + &object_instance); + if (len > 0) { + apdu_len += len; +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_OBJECT_ENABLE + if (value) { + value->source_object.type = object_type; + value->source_object.instance = object_instance; + } +#endif + } else if (len < 0) { + return BACNET_STATUS_ERROR; + } + /* operation [4] BACnetAuditOperation */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 4, &unsigned_value); + if (len > 0) { + if (unsigned_value < AUDIT_OPERATION_MAX) { + if (value) { + value->operation = (BACNET_AUDIT_OPERATION)unsigned_value; + } + } else { + return BACNET_STATUS_ERROR; + } + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + /* source-comment [5] CharacterString OPTIONAL */ + len = bacnet_character_string_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 5, &char_string); + if (len > 0) { + apdu_len += len; +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_COMMENT_ENABLE + if (value) { + characterstring_copy(&value->source_comment, &char_string); + } +#endif + } else if (len < 0) { + return BACNET_STATUS_ERROR; + } + /* target-comment [6] CharacterString OPTIONAL */ + len = bacnet_character_string_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 6, &char_string); + if (len > 0) { + apdu_len += len; +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_COMMENT_ENABLE + if (value) { + characterstring_copy(&value->target_comment, &char_string); + } +#endif + } else if (len < 0) { + return BACNET_STATUS_ERROR; + } + /* invoke-id [7] Unsigned8 OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 7, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (unsigned_value > UINT8_MAX) { + return BACNET_STATUS_ERROR; + } +#ifdef BACNET_AUDIT_NOTIFICATION_INVOKE_ID_ENABLE + if (value) { + value->invoke_id = (uint8_t)unsigned_value; + } +#endif + } else if (len < 0) { + return BACNET_STATUS_ERROR; + } + /* source-user-id [8] Unsigned16 OPTIONAL */ + if (bacnet_is_context_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, 8, NULL, NULL)) { + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 8, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (unsigned_value > UINT16_MAX) { + return BACNET_STATUS_ERROR; + } +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ID_ENABLE + if (value) { + value->source_user_id = (uint16_t)unsigned_value; + } +#endif + } else { + return BACNET_STATUS_ERROR; + } + } + /* source-user-role [9] Unsigned8 OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 9, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (unsigned_value > UINT8_MAX) { + return BACNET_STATUS_ERROR; + } +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ROLE_ENABLE + if (value) { + value->source_user_role = (uint8_t)unsigned_value; + } +#endif + } else if (len < 0) { + return BACNET_STATUS_ERROR; + } + /* target-device [10] BACnetRecipient */ + len = bacnet_recipient_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 10, &recipient); + if (len > 0) { + apdu_len += len; + if (value) { + bacnet_recipient_copy(&value->target_device, &recipient); + } + } else { + return BACNET_STATUS_ERROR; + } + /* target-object [11] BACnetObjectIdentifier OPTIONAL */ + len = bacnet_object_id_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 11, &object_type, + &object_instance); + if (len > 0) { + apdu_len += len; +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_OBJECT_ENABLE + if (value) { + value->target_object.type = object_type; + value->target_object.instance = object_instance; + } +#endif + } else if (len < 0) { + return BACNET_STATUS_ERROR; + } + /* target-property [12] BACnetPropertyReference OPTIONAL */ + len = bacnet_property_reference_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 12, &property_reference); + if (len > 0) { + apdu_len += len; +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PROPERTY_ENABLE + if (value) { + memcpy( + &value->target_property, &property_reference, + sizeof(property_reference)); + } +#endif + } else if (len < 0) { + return BACNET_STATUS_ERROR; + } + /* target-priority [13] Unsigned (1..16) OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 13, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (unsigned_value > UINT8_MAX) { + return BACNET_STATUS_ERROR; + } +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PRIORITY_ENABLE + if (value) { + value->target_priority = (uint8_t)unsigned_value; + } +#endif + } else if (len < 0) { + return BACNET_STATUS_ERROR; + } + /* target-value [14] ABSTRACT-SYNTAX.&Type OPTIONAL */ + len = bacnet_audit_value_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 14, &audit_value); + if (len > 0) { + apdu_len += len; +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_VALUE_ENABLE + if (value) { + memcpy(&value->target_value, &audit_value, sizeof(audit_value)); + } +#endif + } else if (len < 0) { + return BACNET_STATUS_ERROR; + } + /* current-value [15] ABSTRACT-SYNTAX.&Type OPTIONAL */ + len = bacnet_audit_value_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 15, &audit_value); + if (len > 0) { + apdu_len += len; +#ifdef BACNET_AUDIT_NOTIFICATION_CURRENT_VALUE_ENABLE + if (value) { + memcpy(&value->current_value, &audit_value, sizeof(audit_value)); + } +#endif + } + /* result [16] Error OPTIONAL */ + len = bacnet_enumerated_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 16, &enumerated_value); + if (len > 0) { + apdu_len += len; + if (enumerated_value > UINT16_MAX) { + return BACNET_STATUS_ERROR; + } +#ifdef BACNET_AUDIT_NOTIFICATION_RESULT_ENABLE + if (value) { + value->result = (BACNET_ERROR_CODE)enumerated_value; + } +#endif + } else if (len < 0) { + return BACNET_STATUS_ERROR; + } + + return apdu_len; +} + +/** + * @brief Decode a context BACnetAuditLogNotification context data. + * Check for an opening tag and a closing tag as well. + * + * @param apdu Pointer to the buffer containing the encoded value + * @param apdu_size Size of the buffer containing the encoded value + * @param tag_number Tag number + * @param value Pointer to the structure that shall be decoded into. + * + * @return number of bytes decoded, zero if tag mismatch, + * or #BACNET_STATUS_ERROR (-1) if malformed + */ +int bacnet_audit_log_notification_context_decode( + const uint8_t *apdu, + uint16_t apdu_size, + uint8_t tag_number, + BACNET_AUDIT_NOTIFICATION *value) +{ + int len = 0; + int apdu_len = 0; + + if (!apdu) { + return BACNET_STATUS_ERROR; + } + if (!bacnet_is_opening_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, tag_number, &len)) { + return 0; + } + apdu_len += len; + len = bacnet_audit_log_notification_decode( + &apdu[apdu_len], apdu_size - apdu_len, value); + if (len > 0) { + apdu_len += len; + if (bacnet_is_closing_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, tag_number, &len)) { + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + } else { + return BACNET_STATUS_ERROR; + } + + return apdu_len; +} + +/** + * @brief Compare two BACnetAuditNotification for same-ness + * @param value1 - value 1 structure + * @param value2 - value 2 structure + * @return true if the values are the same + */ +bool bacnet_audit_log_notification_same( + const BACNET_AUDIT_NOTIFICATION *value1, + const BACNET_AUDIT_NOTIFICATION *value2) +{ + if (!value1 || !value2) { + return false; + } + if (value1->operation != value2->operation) { + return false; + } + if (!bacnet_recipient_same( + &value1->source_device, &value2->source_device)) { + return false; + } + if (!bacnet_recipient_same( + &value1->target_device, &value2->target_device)) { + return false; + } +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_TIMESTAMP_ENABLE + if (!bacapp_timestamp_same( + &value1->source_timestamp, &value2->source_timestamp)) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_TIMESTAMP_ENABLE + if (!bacapp_timestamp_same( + &value1->target_timestamp, &value2->target_timestamp)) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_OBJECT_ENABLE + if (!bacnet_object_id_same( + value1->source_object.type, value1->source_object.instance, + value2->source_object.type, value2->source_object.instance)) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_COMMENT_ENABLE + if (!characterstring_same( + &value1->source_comment, &value2->source_comment)) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_COMMENT_ENABLE + if (!characterstring_same( + &value1->target_comment, &value2->target_comment)) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_INVOKE_ID_ENABLE + if (value1->invoke_id != value2->invoke_id) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ID_ENABLE + if (value1->source_user_id != value2->source_user_id) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ROLE_ENABLE + if (value1->source_user_role != value2->source_user_role) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_OBJECT_ENABLE + if (!bacnet_object_id_same( + value1->target_object.type, value1->target_object.instance, + value2->target_object.type, value2->target_object.instance)) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PROPERTY_ENABLE + if (!bacnet_property_reference_same( + &value1->target_property, &value2->target_property)) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PRIORITY_ENABLE + if (value1->target_priority != value2->target_priority) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_VALUE_ENABLE + if (!bacnet_audit_value_same( + &value1->target_value, &value2->target_value)) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_CURRENT_VALUE_ENABLE + if (!bacnet_audit_value_same( + &value1->current_value, &value2->current_value)) { + return false; + } +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_RESULT_ENABLE + if (value1->result != value2->result) { + return false; + } +#endif + + return true; +} + +/** + * @brief Encode property value according to the application tag + * + * BACnetAuditLogRecord ::= SEQUENCE { + * timestamp [0] BACnetDateTime, + * log-datum [1] CHOICE { + * log-status [0] BACnetLogStatus, + * audit-notification [1] BACnetAuditNotification, + * time-change [2] REAL + * } + * } + * + * @param apdu - Pointer to the buffer to encode to, or NULL for length + * @param value - Pointer to the property value to encode from + * @return number of bytes encoded + */ +int bacnet_audit_log_record_encode( + uint8_t *apdu, const BACNET_AUDIT_LOG_RECORD *value) +{ + int len, apdu_len = 0; /* total length of the apdu, return value */ + BACNET_BIT_STRING log_status = { 0 }; + + if (!value) { + return 0; + } + /* timestamp [0] BACnetDateTime */ + len = bacapp_encode_context_datetime(apdu, 0, &value->timestamp); + apdu_len += len; + if (apdu) { + apdu += len; + } + /* log-datum [1] CHOICE */ + len = encode_opening_tag(apdu, 1); + apdu_len += len; + if (apdu) { + apdu += len; + } + switch (value->tag) { + case AUDIT_LOG_DATUM_TAG_STATUS: + /* log-status [0] BACnetLogStatus */ + bitstring_bits_used_set(&log_status, LOG_STATUS_MAX); + bitstring_set_octet(&log_status, 0, value->log_datum.log_status); + len = encode_context_bitstring(apdu, value->tag, &log_status); + apdu_len += len; + if (apdu) { + apdu += len; + } + break; + case AUDIT_LOG_DATUM_TAG_NOTIFICATION: + /* audit-notification [1] BACnetAuditNotification */ + len = encode_opening_tag(apdu, value->tag); + apdu_len += len; + if (apdu) { + apdu += len; + } + len = bacnet_audit_log_notification_encode( + apdu, &value->log_datum.notification); + apdu_len += len; + if (apdu) { + apdu += len; + } + len = encode_closing_tag(apdu, value->tag); + apdu_len += len; + if (apdu) { + apdu += len; + } + break; + case AUDIT_LOG_DATUM_TAG_TIME_CHANGE: + /* time-change [2] REAL */ + len = encode_context_real( + apdu, value->tag, value->log_datum.time_change); + apdu_len += len; + if (apdu) { + apdu += len; + } + break; + default: + break; + } + /* log-datum [1] CHOICE */ + len = encode_closing_tag(apdu, 1); + apdu_len += len; + + return apdu_len; +} + +/** + * @brief Decode property value from the application buffer + * @param apdu - Pointer to the buffer to decode from + * @param apdu_size Size of the buffer to decode from + * @param value - Pointer to the property value to decode to + * @return number of bytes encoded, or BACNET_STATUS_ERROR if an error + * occurs + */ +int bacnet_audit_log_record_decode( + const uint8_t *apdu, uint32_t apdu_size, BACNET_AUDIT_LOG_RECORD *value) +{ + int len = 0; + int apdu_len = 0; + BACNET_TAG tag = { 0 }; + BACNET_DATE_TIME bdatetime = { 0 }; + BACNET_BIT_STRING log_status = { 0 }; + BACNET_AUDIT_NOTIFICATION notification = { 0 }; + float real_value = 0.0f; + + if (!apdu) { + return BACNET_STATUS_ERROR; + } + /* timestamp [0] BACnetDateTime */ + len = bacnet_datetime_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 0, &bdatetime); + if (len > 0) { + if (value) { + datetime_copy(&value->timestamp, &bdatetime); + } + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + /* log-datum [1] CHOICE */ + if (bacnet_is_opening_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, 1, &len)) { + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + len = bacnet_tag_decode(&apdu[apdu_len], apdu_size - apdu_len, &tag); + if (len <= 0) { + return BACNET_STATUS_ERROR; + } + if (value) { + value->tag = tag.number; + } + /* ignore the len. len is included in context decoder below. */ + switch (tag.number) { + case AUDIT_LOG_DATUM_TAG_STATUS: + /* log-status [0] BACnetLogStatus */ + len = bacnet_bitstring_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, tag.number, &log_status); + if (len > 0) { + if (value) { + value->log_datum.log_status = + bitstring_octet(&log_status, 0); + } + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + break; + case AUDIT_LOG_DATUM_TAG_NOTIFICATION: + /* audit-notification [1] BACnetAuditNotification */ + len = bacnet_audit_log_notification_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, tag.number, + ¬ification); + if (len > 0) { + if (value) { + memmove( + &value->log_datum.notification, ¬ification, + sizeof(BACNET_AUDIT_NOTIFICATION)); + } + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + break; + case AUDIT_LOG_DATUM_TAG_TIME_CHANGE: + /* time-change [2] REAL */ + len = bacnet_real_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, tag.number, &real_value); + if (len > 0) { + if (value) { + value->log_datum.time_change = real_value; + } + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + break; + default: + return BACNET_STATUS_ERROR; + } + /* log-datum [1] CHOICE */ + if (bacnet_is_closing_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, 1, &len)) { + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + + return apdu_len; +} + +/** + * @brief Compare two BACnetActionPropertyValue complex datatypes + * @param value1 [in] The first structure to compare + * @param value2 [in] The second structure to compare + * @return true if the two structures are the same + */ +bool bacnet_audit_log_record_same( + const BACNET_AUDIT_LOG_RECORD *value1, + const BACNET_AUDIT_LOG_RECORD *value2) +{ + bool status = false; /*return value */ + + if ((value1 == NULL) || (value2 == NULL)) { + return false; + } + /* does the tag match? */ + if (value1->tag == value2->tag) { + status = true; + } + if (status) { + status = false; + /* does the timestamp match? */ + if (datetime_compare(&value1->timestamp, &value2->timestamp) == 0) { + status = true; + } + } + if (status) { + status = false; + switch (value1->tag) { + case AUDIT_LOG_DATUM_TAG_STATUS: + if (value1->log_datum.log_status == + value2->log_datum.log_status) { + status = true; + } + break; + case AUDIT_LOG_DATUM_TAG_NOTIFICATION: + if (bacnet_audit_log_notification_same( + &value1->log_datum.notification, + &value2->log_datum.notification)) { + status = true; + } + break; + case AUDIT_LOG_DATUM_TAG_TIME_CHANGE: + if (!islessgreater( + value1->log_datum.time_change, + value2->log_datum.time_change)) { + status = true; + } + break; + default: + status = false; + break; + } + } + + return status; +} diff --git a/src/bacnet/bacaudit.h b/src/bacnet/bacaudit.h new file mode 100644 index 00000000..7b19da62 --- /dev/null +++ b/src/bacnet/bacaudit.h @@ -0,0 +1,258 @@ +/** + * @file + * @brief API for BACnetAuditNotification and BACnetAuditLogRecord codec used + * by Audit Log objects + * @author Mikhail Antropov + * @author Steve Karg + * @date November 2024 + * @copyright SPDX-License-Identifier: MIT + */ +#ifndef BACNET_AUDIT_H +#define BACNET_AUDIT_H +#include +#include +/* BACnet Stack defines - first */ +#include "bacnet/bacdef.h" +/* BACnet Stack API */ +#include "bacnet/bacdest.h" +#include "bacnet/cov.h" +#include "bacnet/datetime.h" +#include "bacnet/bacdevobjpropref.h" + +/** + * @brief Container to hold the target value or current value + * @note This must be a separate struct from BACapp to avoid + * recursive a recursive include. + * @note This is a union to save space. The application tag + * is used to determine which type is used. + * + * + */ +typedef struct BACnet_Audit_Value { + uint8_t tag; /* application tag data type */ + union { + /* NULL - not needed as it is encoded in the tag alone */ + bool boolean_value; + float real_value; + uint32_t enumerated_value; + uint32_t unsigned_value; + int32_t integer_value; + /* note: use ifdef for values larger than 32-bits are supported */ + } type; +} BACNET_AUDIT_VALUE; + +/** + * @brief Storage structures for Audit Log record + * + * @note I've tried to minimize the storage requirements here + * as the memory requirements for logging in embedded + * implementations are frequently a big issue. For PC or + * embedded Linux type setupz this may seem like overkill + * but if you have limited memory and need to squeeze as much + * logging capacity as possible every little byte counts! + */ +#if !( \ + defined(BACNET_AUDIT_NOTIFICATION_MINIMAL_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_OPTIONAL_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_SOURCE_TIMESTAMP_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_TARGET_TIMESTAMP_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_SOURCE_OBJECT_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_SOURCE_COMMENT_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_TARGET_COMMENT_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_INVOKE_ID_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ID_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ROLE_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_TARGET_OBJECT_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_TARGET_PROPERTY_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_TARGET_PRIORITY_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_TARGET_VALUE_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_CURRENT_VALUE_ENABLE) || \ + defined(BACNET_AUDIT_NOTIFICATION_RESULT_ENABLE)) +#define BACNET_AUDIT_NOTIFICATION_OPTIONAL_ENABLE +#endif + +#ifdef BACNET_AUDIT_NOTIFICATION_OPTIONAL_ENABLE +#define BACNET_AUDIT_NOTIFICATION_SOURCE_TIMESTAMP_ENABLE +#define BACNET_AUDIT_NOTIFICATION_TARGET_TIMESTAMP_ENABLE +#define BACNET_AUDIT_NOTIFICATION_SOURCE_OBJECT_ENABLE +#define BACNET_AUDIT_NOTIFICATION_SOURCE_COMMENT_ENABLE +#define BACNET_AUDIT_NOTIFICATION_TARGET_COMMENT_ENABLE +#define BACNET_AUDIT_NOTIFICATION_INVOKE_ID_ENABLE +#define BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ID_ENABLE +#define BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ROLE_ENABLE +#define BACNET_AUDIT_NOTIFICATION_TARGET_OBJECT_ENABLE +#define BACNET_AUDIT_NOTIFICATION_TARGET_PROPERTY_ENABLE +#define BACNET_AUDIT_NOTIFICATION_TARGET_PRIORITY_ENABLE +#define BACNET_AUDIT_NOTIFICATION_TARGET_VALUE_ENABLE +#define BACNET_AUDIT_NOTIFICATION_CURRENT_VALUE_ENABLE +#define BACNET_AUDIT_NOTIFICATION_RESULT_ENABLE +#endif + +/* + * BACnetAuditNotification ::= SEQUENCE { + * source-timestamp [0] BACnetTimeStamp OPTIONAL, + * target-timestamp [1] BACnetTimeStamp OPTIONAL, + * source-device [2] BACnetRecipient, + * source-object [3] BACnetObjectIdentifier OPTIONAL, + * operation [4] BACnetAuditOperation, + * source-comment [5] CharacterString OPTIONAL, + * target-comment [6] CharacterString OPTIONAL, + * invoke-id [7] Unsigned8 OPTIONAL, + * source-user-id [8] Unsigned16 OPTIONAL, + * source-user-role [9] Unsigned8 OPTIONAL, + * target-device [10] BACnetRecipient, + * target-object [11] BACnetObjectIdentifier OPTIONAL, + * target-property [12] BACnetPropertyReference OPTIONAL, + * target-priority [13] Unsigned (1..16) OPTIONAL, + * target-value [14] ABSTRACT-SYNTAX.&Type OPTIONAL, + * current-value [15] ABSTRACT-SYNTAX.&Type OPTIONAL, + * result [16] Error OPTIONAL + * } + */ +typedef struct BACnetAuditNotification { +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_TIMESTAMP_ENABLE + /* source-timestamp [0] BACnetTimeStamp OPTIONAL */ + BACNET_TIMESTAMP source_timestamp; +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_TIMESTAMP_ENABLE + /* target-timestamp [1] BACnetTimeStamp OPTIONAL */ + BACNET_TIMESTAMP target_timestamp; +#endif + /* source-device [2] BACnetRecipient */ + BACNET_RECIPIENT source_device; +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_OBJECT_ENABLE + /* source-object [3] BACnetObjectIdentifier OPTIONAL */ + BACNET_OBJECT_ID source_object; +#endif + /* operation [4] BACnetAuditOperation */ + uint8_t operation; +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_COMMENT_ENABLE + /* source-comment [5] CharacterString OPTIONAL */ + BACNET_CHARACTER_STRING source_comment; +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_COMMENT_ENABLE + /* target-comment [6] CharacterString OPTIONAL */ + BACNET_CHARACTER_STRING target_comment; +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_INVOKE_ID_ENABLE + /* invoke-id [7] Unsigned8 OPTIONAL */ + uint8_t invoke_id; +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ID_ENABLE + /* source-user-id [8] Unsigned16 OPTIONAL */ + uint16_t source_user_id; +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ROLE_ENABLE + /* source-user-role [9] Unsigned8 OPTIONAL */ + uint8_t source_user_role; +#endif + /* target-device [10] BACnetRecipient */ + BACNET_RECIPIENT target_device; +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_OBJECT_ENABLE + /* target-object [11] BACnetObjectIdentifier OPTIONAL */ + BACNET_OBJECT_ID target_object; +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PROPERTY_ENABLE + /* target-property [12] BACnetPropertyReference OPTIONAL */ + struct BACnetPropertyReference target_property; +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PRIORITY_ENABLE + /* target-priority [13] Unsigned (1..16) OPTIONAL */ + uint8_t target_priority; +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_VALUE_ENABLE + /* target-value [14] ABSTRACT-SYNTAX.&Type OPTIONAL */ + BACNET_AUDIT_VALUE target_value; +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_CURRENT_VALUE_ENABLE + /* current-value [15] ABSTRACT-SYNTAX.&Type OPTIONAL */ + BACNET_AUDIT_VALUE current_value; +#endif +#ifdef BACNET_AUDIT_NOTIFICATION_RESULT_ENABLE + /* result [16] Error OPTIONAL */ + BACNET_ERROR_CODE result; +#endif +} BACNET_AUDIT_NOTIFICATION; + +/** + * @brief Datum types associated with a BACnetAuditLogRecord. + * The tag numbers are used when encoding or decoding the log_datum field. + */ +#define AUDIT_LOG_DATUM_TAG_STATUS 0 +#define AUDIT_LOG_DATUM_TAG_NOTIFICATION 1 +#define AUDIT_LOG_DATUM_TAG_TIME_CHANGE 2 + +/* + * BACnetAuditLogRecord ::= SEQUENCE { + * timestamp [0] BACnetDateTime, + * log-datum [1] CHOICE { + * log-status [0] BACnetLogStatus, + * audit-notification [1] BACnetAuditNotification, + * time-change [2] REAL + * } + * } + */ +typedef struct BACnetAuditLogRecord { + BACNET_DATE_TIME timestamp; + uint8_t tag; + union { + uint8_t log_status; + BACNET_AUDIT_NOTIFICATION notification; + float time_change; + } log_datum; +} BACNET_AUDIT_LOG_RECORD; + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +BACNET_STACK_EXPORT +int bacnet_audit_log_record_encode( + uint8_t *apdu, const BACNET_AUDIT_LOG_RECORD *value); +BACNET_STACK_EXPORT +int bacnet_audit_log_record_decode( + const uint8_t *apdu, uint32_t apdu_size, BACNET_AUDIT_LOG_RECORD *value); +BACNET_STACK_EXPORT +bool bacnet_audit_log_record_same( + const BACNET_AUDIT_LOG_RECORD *value1, + const BACNET_AUDIT_LOG_RECORD *value2); + +BACNET_STACK_EXPORT +int bacnet_audit_log_notification_encode( + uint8_t *apdu, const BACNET_AUDIT_NOTIFICATION *value); +BACNET_STACK_EXPORT +int bacnet_audit_log_notification_decode( + const uint8_t *apdu, uint32_t apdu_size, BACNET_AUDIT_NOTIFICATION *value); +BACNET_STACK_EXPORT +int bacnet_audit_log_notification_context_decode( + const uint8_t *apdu, + uint16_t apdu_size, + uint8_t tag_number, + BACNET_AUDIT_NOTIFICATION *value); +BACNET_STACK_EXPORT +bool bacnet_audit_log_notification_same( + const BACNET_AUDIT_NOTIFICATION *value1, + const BACNET_AUDIT_NOTIFICATION *value2); + +BACNET_STACK_EXPORT +int bacnet_audit_value_encode(uint8_t *apdu, const BACNET_AUDIT_VALUE *value); +BACNET_STACK_EXPORT +int bacnet_audit_value_context_encode( + uint8_t *apdu, uint8_t tag_number, const BACNET_AUDIT_VALUE *value); +BACNET_STACK_EXPORT +int bacnet_audit_value_decode( + const uint8_t *apdu, uint32_t apdu_size, BACNET_AUDIT_VALUE *value); +BACNET_STACK_EXPORT +int bacnet_audit_value_context_decode( + const uint8_t *apdu, + uint16_t apdu_size, + uint8_t tag_number, + BACNET_AUDIT_VALUE *value); +BACNET_STACK_EXPORT +bool bacnet_audit_value_same( + const BACNET_AUDIT_VALUE *value1, const BACNET_AUDIT_VALUE *value2); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif diff --git a/src/bacnet/bacenum.h b/src/bacnet/bacenum.h index 97e96380..5eedbd54 100644 --- a/src/bacnet/bacenum.h +++ b/src/bacnet/bacenum.h @@ -1861,7 +1861,8 @@ typedef enum BACnetStatusFlags { typedef enum BACnetLogStatus { LOG_STATUS_LOG_DISABLED = 0, LOG_STATUS_BUFFER_PURGED = 1, - LOG_STATUS_LOG_INTERRUPTED = 2 + LOG_STATUS_LOG_INTERRUPTED = 2, + LOG_STATUS_MAX = 3 } BACNET_LOG_STATUS; typedef enum BACnetLoggingType { @@ -3123,7 +3124,8 @@ typedef enum BACnetAuditOperation { The enumerated values match the bit positions in BACnetAuditOperationFlags. */ AUDIT_OPERATION_PROPRIETARY_MIN = 32, - AUDIT_OPERATION_PROPRIETARY_MAX = 63 + AUDIT_OPERATION_PROPRIETARY_MAX = 63, + AUDIT_OPERATION_MAX = 64 } BACNET_AUDIT_OPERATION; typedef enum BACnetSCHubConnectorState { diff --git a/src/bacnet/basic/object/auditlog.c b/src/bacnet/basic/object/auditlog.c new file mode 100644 index 00000000..dd49d6ac --- /dev/null +++ b/src/bacnet/basic/object/auditlog.c @@ -0,0 +1,1486 @@ +/** + * @file + * @brief Audit Log object, customize for your use + * @details An Audit Log object combines audit notifications + * from operation sources and operation targets and stores the + * combined record in an internal buffer for subsequent retrieval. + * Each timestamped buffer entry is called an audit log "record." + * + * Each Audit Log object maintains an internal, persistent, optionally + * fixed-size log buffer. This log buffer fills or grows as audit + * log records are added. If the log buffer becomes full, the least recent + * log records are overwritten when new log records are added. + * Log buffers are transferred as a list of BACnetAuditLogRecord values + * using the ReadRange and AuditLogQuery services. Each log record in the + * log buffer has an implied sequence number that is equal to the value + * of the Total_Record_Count property immediately after the record is added. + * See Clause 19.6 for a full description of how audit notifications are + * added to audit logs. + * + * As records are added into the log, the Audit Log object will scan + * existing entries for a matching record. A record is a match if: + * (a) the record contains the timestamp for the opposite actor (the + * record contains the operation source timestamp when merging + * in an operation target notification and vice-versa); + * (b) the operation-source, operation, invoke-id, target-device, + * target-property, are all equal; + * (c) if the user-id, user-role, target-value fields are provided + * in both notifications then they are equal; and + * (d) if the source-timestamp and target-timestamp values are + * approximately equal (+/- APDU_Timeout * 2). + * + * If a match is found, the existing log record is updated. + * Otherwise, a new record is created. If a match is found, + * and it already contains both an operation source and an + * operation target portion, then the notification is dropped. + * When creating a new record, those fields which are not supplied + * in the notification (such as the 'Source Timestamp' when a + * server notification is received) shall be absent from the record. + * When updating an existing record, those fields not supplied in the + * original notification are updated from the new notification, if present. + * For the 'Current Value' field, a value provided by the operation target + * device shall always take precedence over a value provided by an operation + * source device. As such, if the values provided in the peer notifications + * differ, the operation target value shall be the one used in the record. + * + * Logging may be enabled and disabled through the Enable property. + * Audit Log enabling and disabling is recorded in the audit log buffer. + * + * Unlike other log objects, Audit Log objects do not use the BUFFER_READY + * event algorithm. + * + * The acquisition of log records by remote devices has + * no effect upon the state of the Audit Log object itself. This allows + * completely independent, but properly sequential, access to its log records + * by all remote devices. Any remote device can independently update its + * log records at any time. + * + * Audit Log objects may optionally support forwarding of audit notifications + * to “parent” audit logs. This functionality improves the reliability of the + * audit system by allowing intermediaries to buffer audit notifications + * in the case where the ultimate audit logger is offline for a short period + * of time. It is expected that intermediaries be capable of storing a larger + * number of records than devices which report auditable actions. It is also + * useful for buffering of audit notifications so they can be sent in bulk to + * the parent audit log. When operating in this mode, with the + * Delete_On_Forward property set to TRUE, the object is not required + * to perform audit notification matching and combining. + * + * @author Mikhail Antropov + * @author Steve Karg + * @date July 2023 + * @copyright SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +/* BACnet Stack defines - first */ +#include "bacnet/bacdef.h" +/* BACnet Stack API */ +#include "bacnet/bacdcode.h" +#include "bacnet/bacapp.h" +#include "bacnet/bacaudit.h" +#include "bacnet/apdu.h" +#include "bacnet/bacdevobjpropref.h" +#include "bacnet/datetime.h" +#include "bacnet/rp.h" +#include "bacnet/wp.h" +#include "bacnet/basic/services.h" +#include "bacnet/basic/sys/keylist.h" +/* me! */ +#include "auditlog.h" + +#ifndef BACNET_AUDIT_LOG_RECORDS_MAX +#define BACNET_AUDIT_LOG_RECORDS_MAX 128 +#endif + +struct object_data { + bool Enable; + bool Out_Of_Service; + int Buffer_Size; + OS_Keylist Records; + int Record_Count_Total; + const char *Object_Name; + const char *Description; +}; +/* Key List for storing the object data sorted by instance number */ +static OS_Keylist Object_List; + +static const int Properties_Required[] = { + /* required properties that are supported for this object */ + PROP_OBJECT_IDENTIFIER, + PROP_OBJECT_NAME, + PROP_OBJECT_TYPE, + PROP_STATUS_FLAGS, + PROP_EVENT_STATE, + PROP_ENABLE, + PROP_BUFFER_SIZE, + PROP_LOG_BUFFER, + PROP_RECORD_COUNT, + PROP_TOTAL_RECORD_COUNT, + -1 +}; + +static const int Properties_Optional[] = { PROP_DESCRIPTION, -1 }; + +static const int Properties_Proprietary[] = { -1 }; + +static const int BACnetARRAY_Properties[] = { + /* standard properties that are arrays for this object */ + PROP_LOG_BUFFER, + PROP_EVENT_TIME_STAMPS, + PROP_EVENT_MESSAGE_TEXTS, + PROP_EVENT_MESSAGE_TEXTS_CONFIG, + PROP_TAGS, + -1 +}; + +/** + * @brief Determine if the object property is a BACnetARRAY property + * @param object_property - object-property to be checked + * @return true if the property is a BACnetARRAY property + */ +static bool BACnetARRAY_Property(int object_property) +{ + return property_list_member(BACnetARRAY_Properties, object_property); +} + +/** + * Returns the list of required, optional, and proprietary properties. + * Used by ReadPropertyMultiple service. + * + * @param pRequired - pointer to list of int terminated by -1, of + * BACnet required properties for this object. + * @param pOptional - pointer to list of int terminated by -1, of + * BACnet optkional properties for this object. + * @param pProprietary - pointer to list of int terminated by -1, of + * BACnet proprietary properties for this object. + */ +void Audit_Log_Property_Lists( + const int **pRequired, const int **pOptional, const int **pProprietary) +{ + if (pRequired) { + *pRequired = Properties_Required; + } + if (pOptional) { + *pOptional = Properties_Optional; + } + if (pProprietary) { + *pProprietary = Properties_Proprietary; + } + + return; +} + +/** + * @brief Gets an object from the list using an instance number as the key + * @param object_instance - object-instance number of the object + * @return object found in the list, or NULL if not found + */ +static struct object_data *Object_Data(uint32_t object_instance) +{ + return Keylist_Data(Object_List, object_instance); +} + +/** + * @brief Determines if a given object instance is valid + * @param object_instance - object-instance number of the object + * @return true if the instance is valid, and false if not + */ +bool Audit_Log_Valid_Instance(uint32_t object_instance) +{ + struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (pObject) { + return true; + } + + return false; +} + +/** + * @brief Determines the number of objects + * @return Number of objects + */ +unsigned Audit_Log_Count(void) +{ + return Keylist_Count(Object_List); +} + +/** + * @brief Determines the object instance-number for a given 0..N index + * of objects where N is the count. + * @param index - 0..N value + * @return object instance-number for a valid given index, or UINT32_MAX + */ +uint32_t Audit_Log_Index_To_Instance(unsigned index) +{ + uint32_t instance = UINT32_MAX; + + (void)Keylist_Index_Key(Object_List, index, &instance); + + return instance; +} + +/** + * @brief For a given object instance-number, determines a 0..N index + * of objects where N is the count. + * @param object_instance - object-instance number of the object + * @return index for the given instance-number, or count if not valid. + */ +unsigned Audit_Log_Instance_To_Index(uint32_t object_instance) +{ + return Keylist_Index(Object_List, object_instance); +} + +/** + * For a given object instance-number, returns the Audit Log entity by index. + * + * @param object_instance - object-instance number of the object + * @param index - index of entity + * + * @return Audit Log entity, or NULL if not found. + */ +BACNET_AUDIT_LOG_RECORD * +Audit_Log_Record_Entry(uint32_t object_instance, uint32_t index) +{ + BACNET_AUDIT_LOG_RECORD *entry = NULL; + struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (pObject) { + entry = Keylist_Data_Index(pObject->Records, index); + } + + return entry; +} + +/** + * @brief Delete a record entry from the log buffer + * @param object_instance - object-instance number of the object + * @param index - record index of entity + */ +void Audit_Log_Record_Entry_Delete(uint32_t object_instance, uint32_t index) +{ + BACNET_AUDIT_LOG_RECORD *entry = NULL; + struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (pObject) { + entry = Keylist_Data_Delete(pObject->Records, index); + free(entry); + } +} + +/** + * For a given object instance-number, adds a Audit Log entity to entities list. + * + * @note If the log buffer becomes full, the least recent log records are + * overwritten when new log records are added. + * + * @param object_instance - object-instance number of the object + * @param entity - Audit Log entity + * + * @return true if the entity is add successfully. + */ +bool Audit_Log_Record_Entry_Add( + uint32_t object_instance, const BACNET_AUDIT_LOG_RECORD *value) +{ + BACNET_AUDIT_LOG_RECORD *entry = NULL; + struct object_data *pObject; + int index; + + pObject = Object_Data(object_instance); + if (!pObject) { + return false; + } + if (Keylist_Count(pObject->Records) >= pObject->Buffer_Size) { + /* log is full, so delete oldest record before adding a new record */ + entry = Keylist_Data_Delete_By_Index(pObject->Records, 0); + } + if (!entry) { + entry = calloc(1, sizeof(BACNET_AUDIT_LOG_RECORD)); + if (!entry) { + return false; + } + } + memcpy(entry, value, sizeof(BACNET_AUDIT_LOG_RECORD)); + index = + Keylist_Data_Add(pObject->Records, pObject->Record_Count_Total, entry); + if (index < 0) { + free(entry); + return false; + } + pObject->Record_Count_Total++; + + return true; +} + +/** + * @brief Get the log record maximum length for this object instance + * @param object_instance - object-instance number of the object + * @return maximum number of log records + * @note For products that support very large log objects, + * the value 2^32-1 is reserved to indicate that the buffer size is + * unknown and is constrained solely by currently available resources. + */ +uint32_t Audit_Log_Buffer_Size(uint32_t object_instance) +{ + struct object_data *pObject; + pObject = Object_Data(object_instance); + if (!pObject) { + return 0; + } + + return pObject->Buffer_Size; +} + +/** + * @brief Set the log record maximum length for this object instance + * @param object_instance - object-instance number of the object + * @param buffer_size - maximum number of log records + * @return true if the maximum number of log records is set + */ +bool Audit_Log_Buffer_Size_Set(uint32_t object_instance, uint32_t buffer_size) +{ + struct object_data *pObject; + BACNET_AUDIT_LOG_RECORD *entry = NULL; + int i; + + pObject = Object_Data(object_instance); + if (!pObject) { + return false; + } + if (buffer_size > INT_MAX) { + return false; + } + if (buffer_size < pObject->Buffer_Size) { + /* The disposition of existing log records when Buffer_Size is written + is a local matter. We can shrink the log buffer. */ + for (i = buffer_size; i < pObject->Buffer_Size; i++) { + entry = Keylist_Data_Delete_By_Index(pObject->Records, i); + free(entry); + } + } + pObject->Buffer_Size = buffer_size; + + return true; +} + +/** + * For a given object instance-number, loads the object-name into + * a characterstring. Note that the object name must be unique + * within this device. + * + * @param object_instance - object-instance number of the object + * @param object_name - holds the object-name retrieved + * + * @return true if object-name was retrieved + */ +bool Audit_Log_Object_Name( + uint32_t object_instance, BACNET_CHARACTER_STRING *object_name) +{ + bool status = false; + struct object_data *pObject; + char name_text[32] = "AUDIT-LOG-4194303"; + + pObject = Object_Data(object_instance); + if (pObject) { + if (pObject->Object_Name) { + status = + characterstring_init_ansi(object_name, pObject->Object_Name); + } else { + snprintf( + name_text, sizeof(name_text), "AUDIT-LOG-%lu", + (unsigned long)object_instance); + status = characterstring_init_ansi(object_name, name_text); + } + } + + return status; +} + +/** + * For a given object instance-number, sets the object-name + * Note that the object name must be unique within this device. + * + * @param object_instance - object-instance number of the object + * @param new_name - holds the object-name to be set + * + * @return true if object-name was set + */ +bool Audit_Log_Name_Set(uint32_t object_instance, const char *new_name) +{ + bool status = false; /* return value */ + struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (pObject) { + status = true; + pObject->Object_Name = new_name; + } + + return status; +} + +/** + * @brief Return the object name C string + * @param object_instance [in] BACnet object instance number + * @return object name or NULL if not found + */ +const char *Audit_Log_Name_ASCII(uint32_t object_instance) +{ + const char *name = NULL; + struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (pObject) { + name = pObject->Object_Name; + } + + return name; +} + +/** + * For a given object instance-number, returns the description + * + * @param object_instance - object-instance number of the object + * + * @return description text or NULL if not found + */ +const char *Audit_Log_Description(uint32_t object_instance) +{ + const char *name = NULL; + const struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (pObject) { + if (pObject->Description) { + name = pObject->Description; + } + } + + return name; +} + +/** + * For a given object instance-number, sets the description + * + * @param object_instance - object-instance number of the object + * @param new_name - holds the description to be set + * + * @return true if object-name was set + */ +bool Audit_Log_Description_Set(uint32_t object_instance, const char *new_name) +{ + bool status = false; /* return value */ + struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (pObject) { + status = true; + pObject->Description = new_name; + } + + return status; +} + +/** + * @brief Determines a object enabled flag state + * + * @note Logging occurs if and only if Enable is TRUE. + * Log_Buffer records of type log-status are recorded + * without regard to the value of the Enable property. + * + * @param object_instance - object-instance number of the object + * @return enabled status flag + */ +bool Audit_Log_Enable(uint32_t object_instance) +{ + bool value = false; + struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (pObject) { + value = pObject->Enable; + } + + return value; +} + +/** + * @brief Apply the log enabled algormithm + * @param pObject - object data + * @param enable log enable flag + * @return true if the log enable flag is applied + */ +bool Audit_Log_Enable_Set(uint32_t object_instance, bool enable) +{ + bool status = false; + struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (pObject) { + if (pObject->Enable != enable) { + /* Only trigger this validation on a potential change of state */ + pObject->Enable = enable; + if (enable == false) { + Audit_Log_Record_Status_Insert( + object_instance, LOG_STATUS_LOG_DISABLED, true); + } else { + Audit_Log_Record_Status_Insert( + object_instance, LOG_STATUS_LOG_DISABLED, false); + } + } + status = true; + } + + return status; +} + +/** + * @brief For a given object instance-number, sets the object enabled flag + * @param object_instance - object-instance number of the object + * @param enable - holds the value to be set + * @param error_class - BACnet error class + * @param error_code - BACnet error code + * @return true if set + */ +static bool Audit_Log_Enable_Write( + uint32_t object_instance, + bool enable, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + + status = Audit_Log_Enable_Set(object_instance, enable); + if (!status) { + *error_class = ERROR_CLASS_OBJECT; + *error_code = ERROR_CODE_LOG_BUFFER_FULL; + } + + return status; +} + +/** + * @brief For a given object instance-number, determines the property value + * @param object_instance - object-instance number of the object + * @return the property value + */ +uint32_t Audit_Log_Record_Count(uint32_t object_instance) +{ + uint32_t record_count = 0; + struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (pObject) { + record_count = Keylist_Count(pObject->Records); + } + + return record_count; +} + +/** + * @brief For a given object instance-number, determines the property value + * @param object_instance - object-instance number of the object + * @return the property value + */ +uint32_t Audit_Log_Total_Record_Count(uint32_t object_instance) +{ + uint32_t total_count = 0; + struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (pObject) { + total_count = pObject->Record_Count_Total; + } + + return total_count; +} + +/** + * @brief For a given object instance-number, writes the property value + * @details If writable, it may not be written when Enable is TRUE. + * @param object_instance - object-instance number of the object + * @param buffer_size - holds the value to be set + * @param error_class - BACnet error class + * @param error_code - BACnet error code + * @return true if set + */ +bool Audit_Log_Buffer_Size_Write( + uint32_t object_instance, + uint32_t buffer_size, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (pObject) { + if (pObject->Enable) { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } else if (buffer_size > INT_MAX) { + /* keylist library uses 'int' so that is our limit */ + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } else { + status = Audit_Log_Buffer_Size_Set(object_instance, buffer_size); + } + } + + return status; +} + +/** + * ReadProperty handler for this object. For the given ReadProperty + * data, the application_data is loaded or the error flags are set. + * + * @param rpdata - BACNET_READ_PROPERTY_DATA data, including + * requested data and space for the reply, or error response. + * + * @return number of APDU bytes in the response, or + * BACNET_STATUS_ERROR on error. + */ +int Audit_Log_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) +{ + int apdu_len = 0; /* return value */ + BACNET_CHARACTER_STRING char_string = { 0 }; + BACNET_BIT_STRING bit_string = { 0 }; + uint8_t *apdu = NULL; + int apdu_max = 0; + + if ((rpdata == NULL) || (rpdata->application_data == NULL) || + (rpdata->application_data_len == 0)) { + return 0; + } + apdu = rpdata->application_data; + apdu_max = rpdata->application_data_len; + switch (rpdata->object_property) { + case PROP_OBJECT_IDENTIFIER: + apdu_len = encode_application_object_id( + &apdu[0], rpdata->object_type, rpdata->object_instance); + break; + case PROP_OBJECT_NAME: + Audit_Log_Object_Name(rpdata->object_instance, &char_string); + apdu_len = + encode_application_character_string(&apdu[0], &char_string); + break; + case PROP_OBJECT_TYPE: + apdu_len = + encode_application_enumerated(&apdu[0], rpdata->object_type); + break; + case PROP_ENABLE: + apdu_len = encode_application_boolean( + &apdu[0], Audit_Log_Enable(rpdata->object_instance)); + break; + case PROP_BUFFER_SIZE: + apdu_len = encode_application_unsigned( + &apdu[0], Audit_Log_Buffer_Size(rpdata->object_instance)); + break; + case PROP_LOG_BUFFER: + /* You can only read the buffer via the ReadRange service */ + rpdata->error_class = ERROR_CLASS_PROPERTY; + rpdata->error_code = ERROR_CODE_READ_ACCESS_DENIED; + apdu_len = BACNET_STATUS_ERROR; + break; + case PROP_RECORD_COUNT: + apdu_len = encode_application_unsigned( + &apdu[0], Audit_Log_Record_Count(rpdata->object_instance)); + break; + case PROP_TOTAL_RECORD_COUNT: + apdu_len = encode_application_unsigned( + &apdu[0], + Audit_Log_Total_Record_Count(rpdata->object_instance)); + break; + case PROP_EVENT_STATE: + /* note: see the details in the standard on how to use this */ + apdu_len = + encode_application_enumerated(&apdu[0], EVENT_STATE_NORMAL); + break; + case PROP_STATUS_FLAGS: + /* note: see the details in the standard on how to use these */ + bitstring_init(&bit_string); + bitstring_set_bit(&bit_string, STATUS_FLAG_IN_ALARM, false); + bitstring_set_bit(&bit_string, STATUS_FLAG_FAULT, false); + /* OVERRIDDEN The value of this flag shall be Logical FALSE */ + bitstring_set_bit(&bit_string, STATUS_FLAG_OVERRIDDEN, false); + /* OUT_OF_SERVICE The value of this flag shall be Logical FALSE */ + bitstring_set_bit(&bit_string, STATUS_FLAG_OUT_OF_SERVICE, false); + apdu_len = encode_application_bitstring(&apdu[0], &bit_string); + break; + case PROP_DESCRIPTION: + characterstring_init_ansi( + &char_string, Audit_Log_Description(rpdata->object_instance)); + apdu_len = + encode_application_character_string(&apdu[0], &char_string); + break; + default: + rpdata->error_class = ERROR_CLASS_PROPERTY; + rpdata->error_code = ERROR_CODE_UNKNOWN_PROPERTY; + apdu_len = BACNET_STATUS_ERROR; + break; + } + /* only array properties can have array options */ + if ((apdu_len >= 0) && (!BACnetARRAY_Property(rpdata->object_property)) && + (rpdata->array_index != BACNET_ARRAY_ALL)) { + rpdata->error_class = ERROR_CLASS_PROPERTY; + rpdata->error_code = ERROR_CODE_PROPERTY_IS_NOT_AN_ARRAY; + apdu_len = BACNET_STATUS_ERROR; + } + + return apdu_len; +} + +/** + * WriteProperty handler for this object. For the given WriteProperty + * data, the application_data is loaded or the error flags are set. + * + * @param wp_data - BACNET_WRITE_PROPERTY_DATA data, including + * requested data and space for the reply, or error response. + * + * @return false if an error is loaded, true if no errors + */ +bool Audit_Log_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) +{ + bool status = false; /* return value */ + int len = 0; + BACNET_APPLICATION_DATA_VALUE value; + + /* decode the some of the request */ + len = bacapp_decode_application_data( + wp_data->application_data, wp_data->application_data_len, &value); + /* FIXME: len < application_data_len: more data? */ + if (len < 0) { + /* error while decoding - a value larger than we can handle */ + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + return false; + } + if ((!BACnetARRAY_Property(wp_data->object_property)) && + (wp_data->array_index != BACNET_ARRAY_ALL)) { + /* only array properties can have array options */ + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_PROPERTY_IS_NOT_AN_ARRAY; + return false; + } + switch (wp_data->object_property) { + case PROP_ENABLE: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_BOOLEAN); + if (status) { + status = Audit_Log_Enable_Write( + wp_data->object_instance, value.type.Boolean, + &wp_data->error_class, &wp_data->error_code); + } + break; + case PROP_BUFFER_SIZE: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT); + if (status) { + status = Audit_Log_Buffer_Size_Write( + wp_data->object_instance, value.type.Unsigned_Int, + &wp_data->error_class, &wp_data->error_code); + } + break; + default: + if (property_lists_member( + Properties_Required, Properties_Optional, + Properties_Proprietary, wp_data->object_property)) { + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; + } else { + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_UNKNOWN_PROPERTY; + } + break; + } + + return status; +} + +/** + * Inserts a status notification into a audit log + * + * @param instance - object-instance number of the object + * @param log_status - log status + * @param state - notificate state + */ +void Audit_Log_Record_Status_Insert( + uint32_t object_instance, BACNET_LOG_STATUS log_status, bool state) +{ + BACNET_AUDIT_LOG_RECORD record; + struct object_data *pObject; + + pObject = Object_Data(object_instance); + if (!pObject) { + return; + } + datetime_local(&record.timestamp.date, &record.timestamp.time, NULL, NULL); + record.tag = AUDIT_LOG_DATUM_TAG_STATUS; + record.log_datum.log_status = 0; + /* Note we set the bits in correct order so that we can place them directly + * into the bitstring structure later on when we have to encode them */ + switch (log_status) { + case LOG_STATUS_LOG_DISABLED: + if (state) { + record.log_datum.log_status = 1 << LOG_STATUS_LOG_DISABLED; + } + break; + case LOG_STATUS_BUFFER_PURGED: + if (state) { + record.log_datum.log_status = 1 << LOG_STATUS_BUFFER_PURGED; + } + break; + case LOG_STATUS_LOG_INTERRUPTED: + if (state) { + record.log_datum.log_status = 1 << LOG_STATUS_LOG_INTERRUPTED; + } + break; + default: + break; + } + Audit_Log_Record_Entry_Add(object_instance, &record); +} + +/** + * @brief Determines if a given log notification is the same as another + * @param object_instance - object-instance number of the object + * @param record - log record + * @return the index of the found log record, or -1 if not found + */ +static int Audit_Log_Record_Search( + uint32_t object_instance, BACNET_AUDIT_LOG_RECORD *record) +{ + int i; + BACNET_AUDIT_LOG_RECORD *entry; + struct object_data *pObject; + + if (!record) { + return -1; + } + pObject = Object_Data(object_instance); + if (!pObject) { + return -1; + } + for (i = 0; i < pObject->Buffer_Size; i++) { + entry = Audit_Log_Record_Entry(object_instance, i); + if (!entry) { + break; + } + if (entry->tag == record->tag) { + if (entry->tag == AUDIT_LOG_DATUM_TAG_STATUS) { + if (entry->log_datum.log_status == + record->log_datum.log_status) { + return i; + } + } else if (entry->tag == AUDIT_LOG_DATUM_TAG_NOTIFICATION) { + if (bacnet_audit_log_notification_same( + &entry->log_datum.notification, + &record->log_datum.notification)) { + return i; + } + } + } + } + + return -1; +} + +/** + * Insert a notification record into a audit log. + * + * @param object_instance - object-instance number of the object + * @param notif - notificatione + */ +void Audit_Log_Record_Notification_Insert( + uint32_t object_instance, BACNET_AUDIT_NOTIFICATION *notification) +{ + BACNET_AUDIT_LOG_RECORD seek_entry = { 0 }; + struct object_data *pObject; + int index; + + pObject = Object_Data(object_instance); + if (!pObject) { + return; + } + if (!Audit_Log_Enable(object_instance)) { + return; + } + /* As records are added into the log, the Audit Log object will scan + existing entries for a matching record. */ + datetime_local( + &seek_entry.timestamp.date, &seek_entry.timestamp.time, NULL, NULL); + seek_entry.tag = AUDIT_LOG_DATUM_TAG_NOTIFICATION; + memcpy( + &seek_entry.log_datum.notification, notification, + sizeof(BACNET_AUDIT_NOTIFICATION)); + index = Audit_Log_Record_Search(object_instance, &seek_entry); + if (index >= 0) { + /* If a match is found, the existing record is updated with the new + time stamp and the record is moved to the end of the list. + i.e. delete the old entry and add the new entry */ + Audit_Log_Record_Entry_Delete(object_instance, index); + } + Audit_Log_Record_Entry_Add(object_instance, &seek_entry); +} + +/** + * For a given read range request, encodes log records + * + * @param apdu - buffer to hold the bytes + * @param pRequest - the read range request + * + * @return number of bytes encoded, or 0 if unable to encode. + */ +int Audit_Log_Read_Range(uint8_t *apdu, BACNET_READ_RANGE_DATA *pRequest) +{ + int apdu_len = 0; + + /* buffer and buffer size is passed in BACNET_READ_RANGE_DATA */ + (void)apdu; + /* Initialise result flags to all false */ + bitstring_init(&pRequest->ResultFlags); + bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_FIRST_ITEM, false); + bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_LAST_ITEM, false); + bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_MORE_ITEMS, false); + pRequest->ItemCount = 0; /* Start out with nothing */ + if ((pRequest->RequestType == RR_BY_POSITION) || + (pRequest->RequestType == RR_READ_ALL)) { + apdu_len = Audit_Log_Read_Range_By_Position(pRequest); + } else if (pRequest->RequestType == RR_BY_SEQUENCE) { + apdu_len = Audit_Log_Read_Range_By_Sequence(pRequest); + } else { + apdu_len = Audit_Log_Read_Range_By_Time(pRequest); + } + + return apdu_len; +} + +/** + * @brief Handle encoding for the By Position and All options. + * Does All option by converting to a By Position request starting at index + * 1 and of maximum log size length. + * @param pRequest - the read range request + * @return number of bytes encoded, or 0 if unable to encode. + */ +int Audit_Log_Read_Range_By_Position(BACNET_READ_RANGE_DATA *pRequest) +{ + int log_index = 0; + int apdu_len = 0; + size_t apdu_size; + int len; + uint8_t *apdu; + uint32_t record_count = 0; + BACNET_AUDIT_LOG_RECORD *entry = NULL; + int32_t iTemp = 0; + uint32_t uiIndex = 0; /* Current entry number */ + uint32_t uiFirst = 0; /* Entry number we started encoding from */ + uint32_t uiLast = 0; /* Entry number we finished encoding on */ + uint32_t uiTarget = 0; /* Last entry we are required to encode */ + + record_count = Audit_Log_Record_Count(pRequest->object_instance); + /* See how much space we have */ + apdu_size = pRequest->application_data_len - pRequest->Overhead; + if (pRequest->RequestType == RR_READ_ALL) { + /* + * Read all the list or as much as will fit in the buffer by selecting + * a range that covers the whole list and falling through to the next + * section of code + */ + pRequest->Count = record_count; + /* Starting at the beginning */ + pRequest->Range.RefIndex = 1; + } + if (pRequest->Count < 0) { + /* + * negative count means work from index backwards + * + * Convert from end index/negative count to + * start index/positive count and then process as + * normal. This assumes that the order to return items + * is always first to last, if this is not true we will + * have to handle this differently. + * + * Note: We need to be careful about how we convert these + * values due to the mix of signed and unsigned types - don't + * try to optimise the code unless you understand all the + * implications of the data type conversions! + */ + /* pull out and convert to signed */ + iTemp = pRequest->Range.RefIndex; + /* Adjust backwards, remember count is -ve */ + iTemp += pRequest->Count + 1; + if (iTemp < 1) { + /* if count is too much, return from 1 to start index */ + pRequest->Count = pRequest->Range.RefIndex; + pRequest->Range.RefIndex = 1; + } else { + /* Otherwise adjust the start index and make count +ve */ + pRequest->Range.RefIndex = iTemp; + pRequest->Count = -pRequest->Count; + } + } + /* From here on in we only have a starting point and a positive count */ + if (pRequest->Range.RefIndex > record_count) { + /* Nothing to return as we are past the end of the list */ + return 0; + } + /* Index of last required entry */ + uiTarget = pRequest->Range.RefIndex + pRequest->Count - 1; + if (uiTarget > record_count) { + /* Capped at end of list if necessary */ + uiTarget = record_count; + } + uiIndex = pRequest->Range.RefIndex; + /* Record where we started from */ + uiFirst = uiIndex; + apdu = pRequest->application_data; + while (uiIndex <= uiTarget) { + log_index = uiIndex - 1; + entry = Audit_Log_Record_Entry(pRequest->object_instance, log_index); + len = bacnet_audit_log_record_encode(NULL, entry); + if (len > (apdu_size - apdu_len)) { + /* + * Can't fit any more in! We just set the result flag to say there + * was more and drop out of the loop early + */ + bitstring_set_bit( + &pRequest->ResultFlags, RESULT_FLAG_MORE_ITEMS, true); + break; + } + len = bacnet_audit_log_record_encode(apdu, entry); + apdu += len; + apdu_len += len; + /* Record the last entry encoded */ + uiLast = uiIndex; + /* and get ready for next one */ + uiIndex++; + /* Chalk up another one for the response count */ + pRequest->ItemCount++; + } + /* Set remaining result flags if necessary */ + if (uiFirst == 1) { + bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_FIRST_ITEM, true); + } + if (uiLast == record_count) { + bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_LAST_ITEM, true); + } + + return apdu_len; +} + +/** + * Handle encoding for the By Sequence option. + * The fact that the buffer always has at least a single entry is used + * implicetly in the following as we don't have to handle the case of an + * empty buffer. + * + * @param apdu - buffer to hold the bytes + * @param pRequest - the read range request + * + * @return number of bytes encoded, or 0 if unable to encode. + */ +int Audit_Log_Read_Range_By_Sequence(BACNET_READ_RANGE_DATA *pRequest) +{ + int apdu_len = 0; + size_t apdu_size; + int len; + uint8_t *apdu; + int record_index = 0; + uint32_t record_count; + uint32_t total_record_count; + BACNET_AUDIT_LOG_RECORD *entry = NULL; + uint32_t uiIndex = 0; /* Current entry number */ + uint32_t uiFirst = 0; /* Entry number we started encoding from */ + uint32_t uiLast = 0; /* Entry number we finished encoding on */ + uint32_t uiSequence = 0; /* Tracking sequence number when encoding */ + uint32_t uiFirstSeq = 0; /* Sequence number for 1st record in log */ + uint32_t uiBegin = 0; /* Starting Sequence number for request */ + uint32_t uiEnd = 0; /* Ending Sequence number for request */ + bool bWrapReq = + false; /* Has request sequence range spanned the max for uint32_t? */ + bool bWrapLog = + false; /* Has log sequence range spanned the max for uint32_t? */ + + /* See how much space we have */ + apdu = pRequest->application_data; + apdu_size = pRequest->application_data_len - pRequest->Overhead; + record_count = Audit_Log_Record_Count(pRequest->object_instance); + total_record_count = + Audit_Log_Total_Record_Count(pRequest->object_instance); + /* Figure out the sequence number for the first record, last is + * ulTotalRecordCount */ + uiFirstSeq = total_record_count - record_count - 1; + /* Calculate start and end sequence numbers from request */ + if (pRequest->Count < 0) { + uiBegin = pRequest->Range.RefSeqNum + pRequest->Count + 1; + uiEnd = pRequest->Range.RefSeqNum; + } else { + uiBegin = pRequest->Range.RefSeqNum; + uiEnd = pRequest->Range.RefSeqNum + pRequest->Count - 1; + } + /* See if we have any wrap around situations */ + if (uiBegin > uiEnd) { + bWrapReq = true; + } + if (uiFirstSeq > total_record_count) { + bWrapLog = true; + } + + if ((bWrapReq == false) && (bWrapLog == false)) { + /* Simple case no wraps */ + /* If no overlap between request range and buffer contents bail out */ + if ((uiEnd < uiFirstSeq) || (uiBegin > total_record_count)) { + return (0); + } + /* Truncate range if necessary so it is guaranteed to lie + * between the first and last sequence numbers in the buffer + * inclusive. + */ + if (uiBegin < uiFirstSeq) { + uiBegin = uiFirstSeq; + } + + if (uiEnd > total_record_count) { + uiEnd = total_record_count; + } + } else { + /* There are wrap arounds to contend with */ + /* First check for non overlap condition as it is common to all */ + if ((uiBegin > total_record_count) && (uiEnd < uiFirstSeq)) { + return (0); + } + + if (bWrapLog == false) { + /* Only request range wraps */ + if (uiEnd < uiFirstSeq) { + uiEnd = total_record_count; + if (uiBegin < uiFirstSeq) { + uiBegin = uiFirstSeq; + } + } else { + uiBegin = uiFirstSeq; + if (uiEnd > total_record_count) { + uiEnd = total_record_count; + } + } + } else if (bWrapReq == false) { + /* Only log wraps */ + if (uiBegin > total_record_count) { + if (uiBegin > uiFirstSeq) { + uiBegin = uiFirstSeq; + } + } else { + if (uiEnd > total_record_count) { + uiEnd = total_record_count; + } + } + } else { /* Both wrap */ + if (uiBegin < uiFirstSeq) { + uiBegin = uiFirstSeq; + } + + if (uiEnd > total_record_count) { + uiEnd = total_record_count; + } + } + } + /* We now have a range that lies completely within the log buffer + * and we need to figure out where that starts in the buffer. + */ + uiIndex = uiBegin - uiFirstSeq + 1; + uiSequence = uiBegin; + uiFirst = uiIndex; /* Record where we started from */ + while (uiSequence != uiEnd + 1) { + record_index = uiIndex - 1; + entry = Audit_Log_Record_Entry(pRequest->object_instance, record_index); + len = bacnet_audit_log_record_encode(NULL, entry); + if (len > (apdu_size - apdu_len)) { + /* + * Can't fit any more in! We just set the result flag to say there + * was more and drop out of the loop early + */ + bitstring_set_bit( + &pRequest->ResultFlags, RESULT_FLAG_MORE_ITEMS, true); + break; + } + len = bacnet_audit_log_record_encode(apdu, entry); + apdu += len; + apdu_len += len; + uiLast = uiIndex; /* Record the last entry encoded */ + uiIndex++; /* and get ready for next one */ + uiSequence++; + pRequest->ItemCount++; /* Chalk up another one for the response count */ + } + + /* Set remaining result flags if necessary */ + if (uiFirst == 1) { + bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_FIRST_ITEM, true); + } + + if (uiLast == record_count) { + bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_LAST_ITEM, true); + } + pRequest->FirstSequence = uiBegin; + + return apdu_len; +} + +/** + * Handle encoding for the By Time option. + * The fact that the buffer always has at least a single entry is used + * implicetly in the following as we don't have to handle the case of an + * empty buffer. + * + * @param apdu - buffer to hold the bytes + * @param pRequest - the read range request + * + * @return number of bytes encoded, or 0 if unable to encode. + */ +int Audit_Log_Read_Range_By_Time(BACNET_READ_RANGE_DATA *pRequest) +{ + int apdu_len = 0; + size_t apdu_size; + int len; + uint8_t *apdu; + int record_index = 0; + uint32_t record_count; + uint32_t total_record_count; + BACNET_AUDIT_LOG_RECORD *entry = NULL; + int diff; + + int32_t iTemp = 0; + int iCount = 0; + uint32_t uiIndex = 0; /* Current entry number */ + uint32_t uiFirst = 0; /* Entry number we started encoding from */ + uint32_t uiLast = 0; /* Entry number we finished encoding on */ + uint32_t uiFirstSeq = 0; /* Sequence number for 1st record in log */ + + /* See how much space we have */ + apdu = pRequest->application_data; + apdu_size = pRequest->application_data_len - pRequest->Overhead; + record_count = Audit_Log_Record_Count(pRequest->object_instance); + total_record_count = + Audit_Log_Total_Record_Count(pRequest->object_instance); + if (pRequest->Count < 0) { + /* Start at end of log and look for record which has + * timestamp greater than or equal to the reference. + */ + iCount = record_count - 1; + /* Start out with the sequence number for the last record */ + uiFirstSeq = total_record_count; + for (;;) { + record_index = iCount; + entry = + Audit_Log_Record_Entry(pRequest->object_instance, record_index); + diff = + datetime_compare(&entry->timestamp, &pRequest->Range.RefTime); + if (diff < 0) { + /* If datetime1 is before datetime2, returns negative.*/ + break; + } + uiFirstSeq--; + if (iCount) { + iCount--; + } else { + /* end of records, not found */ + return 0; + } + } + /* We have an and point for our request, + * now work backwards to find where we should start from + */ + pRequest->Count = -pRequest->Count; /* Convert to +ve count */ + /* If count would bring us back beyond the limits + * Of the buffer then pin it to the start of the buffer + * otherwise adjust starting point and sequence number + * appropriately. + */ + iTemp = pRequest->Count - 1; + if (iTemp > iCount) { + uiFirstSeq -= iCount; + pRequest->Count = iCount + 1; + iCount = 0; + } else { + uiFirstSeq -= iTemp; + iCount -= iTemp; + } + } else { + /* Start at beginning of log and look for 1st record which has + * timestamp greater than the reference time. + */ + iCount = 0; + /* Figure out the sequence number for the first record, last is + * ulTotalRecordCount */ + uiFirstSeq = total_record_count - record_count - 1; + for (;;) { + record_index = iCount; + entry = + Audit_Log_Record_Entry(pRequest->object_instance, record_index); + diff = + datetime_compare(&entry->timestamp, &pRequest->Range.RefTime); + if (diff > 0) { + /* If datetime1 is after datetime2, returns positive.*/ + break; + } + uiFirstSeq++; + iCount++; + if ((uint32_t)iCount == record_count) { + return (0); + } + } + } + + /* We now have a starting point for the operation and a +ve count */ + uiIndex = iCount + 1; /* Convert to BACnet 1 based reference */ + uiFirst = uiIndex; /* Record where we started from */ + iCount = pRequest->Count; + while (iCount != 0) { + record_index = uiIndex - 1; + entry = Audit_Log_Record_Entry(pRequest->object_instance, record_index); + len = bacnet_audit_log_record_encode(NULL, entry); + if (len > (apdu_size - apdu_len)) { + /* + * Can't fit any more in! We just set the result flag to say there + * was more and drop out of the loop early + */ + bitstring_set_bit( + &pRequest->ResultFlags, RESULT_FLAG_MORE_ITEMS, true); + break; + } + len = bacnet_audit_log_record_encode(apdu, entry); + apdu += len; + apdu_len += len; + uiLast = uiIndex; /* Record the last entry encoded */ + uiIndex++; /* and get ready for next one */ + pRequest->ItemCount++; /* Chalk up another one for the response count */ + iCount--; /* And finally cross another one off the requested count */ + + if (uiIndex > record_count) { + /* Finish up if we hit the end of the log */ + break; + } + } + /* Set remaining result flags if necessary */ + if (uiFirst == 1) { + bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_FIRST_ITEM, true); + } + if (uiLast == record_count) { + bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_LAST_ITEM, true); + } + pRequest->FirstSequence = uiFirstSeq; + + return apdu_len; +} + +/** + * @brief Creates a Audit Log object + * @param object_instance - object-instance number of the object + * @return object_instance if the object is created, else BACNET_MAX_INSTANCE + */ +uint32_t Audit_Log_Create(uint32_t object_instance) +{ + struct object_data *pObject = NULL; + int index = 0; + + if (object_instance > BACNET_MAX_INSTANCE) { + return BACNET_MAX_INSTANCE; + } else if (object_instance == BACNET_MAX_INSTANCE) { + /* wildcard instance */ + /* the Object_Identifier property of the newly created object + shall be initialized to a value that is unique within the + responding BACnet-user device. The method used to generate + the object identifier is a local matter.*/ + object_instance = Keylist_Next_Empty_Key(Object_List, 1); + } + pObject = Keylist_Data(Object_List, object_instance); + if (!pObject) { + pObject = calloc(1, sizeof(struct object_data)); + if (!pObject) { + return BACNET_MAX_INSTANCE; + } + pObject->Object_Name = NULL; + pObject->Description = NULL; + pObject->Records = Keylist_Create(); + pObject->Buffer_Size = BACNET_AUDIT_LOG_RECORDS_MAX; + pObject->Enable = false; + pObject->Out_Of_Service = false; + /* add to list */ + index = Keylist_Data_Add(Object_List, object_instance, pObject); + if (index < 0) { + free(pObject); + return BACNET_MAX_INSTANCE; + } + } + + return object_instance; +} + +/** + * @brief Deletes all the Audit Logs and their data + */ +static void Audit_Log_Records_Cleanup(OS_Keylist list) +{ + BACNET_AUDIT_LOG_RECORD *entry; + + while (Keylist_Count(list) > 0) { + entry = Keylist_Data_Pop(list); + free(entry); + } + Keylist_Delete(list); +} + +/** + * @brief Deletes an Audit Log object + * @param object_instance - object-instance number of the object + * @return true if the object is deleted + */ +bool Audit_Log_Delete(uint32_t object_instance) +{ + bool status = false; + struct object_data *pObject = NULL; + + pObject = Keylist_Data_Delete(Object_List, object_instance); + if (pObject) { + Audit_Log_Records_Cleanup(pObject->Records); + free(pObject); + status = true; + } + + return status; +} + +/** + * @brief Deletes all the Audit Logs and their data + */ +void Audit_Log_Cleanup(void) +{ + struct object_data *pObject; + + if (Object_List) { + do { + pObject = Keylist_Data_Pop(Object_List); + if (pObject) { + Audit_Log_Records_Cleanup(pObject->Records); + free(pObject); + } + } while (pObject); + Keylist_Delete(Object_List); + Object_List = NULL; + } +} + +/** + * @brief Initializes the Audit Log object data + */ +void Audit_Log_Init(void) +{ + if (!Object_List) { + Object_List = Keylist_Create(); + } +} diff --git a/src/bacnet/basic/object/auditlog.h b/src/bacnet/basic/object/auditlog.h new file mode 100644 index 00000000..a07ac14b --- /dev/null +++ b/src/bacnet/basic/object/auditlog.h @@ -0,0 +1,124 @@ +/** + * @file + * @author Mikhail Antropov + * @date Jul 2023 + * @brief Auditlog object, customize for your use + * + * @section DESCRIPTION + * + * An Audit Log object combines audit notifications from operation sources and + * operation targets and stores the combined record in an internal buffer for + * subsequent retrieval. Each timestamped buffer entry is called an audit log + * "record." + * + * @section LICENSE + * + * Copyright (C) 2023 Steve Karg + * + * SPDX-License-Identifier: MIT + */ +#ifndef AUDITLOG_H +#define AUDITLOG_H + +#include +#include +/* BACnet Stack defines - first */ +#include "bacnet/bacdef.h" +/* BACnet Stack API */ +#include "bacnet/bacaudit.h" +#include "bacnet/bacdest.h" +#include "bacnet/cov.h" +#include "bacnet/datetime.h" +#include "bacnet/readrange.h" +#include "bacnet/rp.h" +#include "bacnet/wp.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +BACNET_STACK_EXPORT +void Audit_Log_Property_Lists( + const int **pRequired, const int **pOptional, const int **pProprietary); + +BACNET_STACK_EXPORT +bool Audit_Log_Valid_Instance(uint32_t object_instance); +BACNET_STACK_EXPORT +unsigned Audit_Log_Count(void); +BACNET_STACK_EXPORT +uint32_t Audit_Log_Index_To_Instance(unsigned index); +BACNET_STACK_EXPORT +unsigned Audit_Log_Instance_To_Index(uint32_t instance); +BACNET_STACK_EXPORT +bool Audit_Log_Object_Instance_Add(uint32_t instance); + +BACNET_STACK_EXPORT +bool Audit_Log_Object_Name( + uint32_t object_instance, BACNET_CHARACTER_STRING *object_name); +BACNET_STACK_EXPORT +bool Audit_Log_Name_Set(uint32_t object_instance, const char *new_name); +BACNET_STACK_EXPORT +const char *Audit_Log_Name_ASCII(uint32_t object_instance); + +BACNET_STACK_EXPORT +const char *Audit_Log_Description(uint32_t instance); +BACNET_STACK_EXPORT +bool Audit_Log_Description_Set(uint32_t instance, const char *new_name); + +BACNET_STACK_EXPORT +int Audit_Log_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata); +BACNET_STACK_EXPORT +bool Audit_Log_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data); +BACNET_STACK_EXPORT +uint32_t Audit_Log_Create(uint32_t object_instance); +BACNET_STACK_EXPORT +bool Audit_Log_Delete(uint32_t object_instance); + +BACNET_STACK_EXPORT +void Audit_Log_Cleanup(void); +BACNET_STACK_EXPORT +void Audit_Log_Init(void); + +BACNET_STACK_EXPORT +uint32_t Audit_Log_Buffer_Size(uint32_t object_instance); +BACNET_STACK_EXPORT +bool Audit_Log_Buffer_Size_Set(uint32_t object_instance, uint32_t buffer_size); + +BACNET_STACK_EXPORT +uint32_t Audit_Log_Record_Count(uint32_t object_instance); +BACNET_STACK_EXPORT +uint32_t Audit_Log_Total_Record_Count(uint32_t object_instance); + +BACNET_STACK_EXPORT +void Audit_Log_Record_Status_Insert( + uint32_t instance, BACNET_LOG_STATUS eStatus, bool bState); +BACNET_STACK_EXPORT +void Audit_Log_Record_Notification_Insert( + uint32_t instance, BACNET_AUDIT_NOTIFICATION *notification); +BACNET_STACK_EXPORT +BACNET_AUDIT_LOG_RECORD * +Audit_Log_Record_Entry(uint32_t object_instance, uint32_t index); +BACNET_STACK_EXPORT +void Audit_Log_Record_Entry_Delete(uint32_t object_instance, uint32_t index); +BACNET_STACK_EXPORT +bool Audit_Log_Record_Entry_Add( + uint32_t object_instance, const BACNET_AUDIT_LOG_RECORD *value); + +BACNET_STACK_EXPORT +bool Audit_Log_Enable(uint32_t instance); +BACNET_STACK_EXPORT +bool Audit_Log_Enable_Set(uint32_t object_instance, bool enable); + +BACNET_STACK_EXPORT +int Audit_Log_Read_Range_By_Position(BACNET_READ_RANGE_DATA *pRequest); +BACNET_STACK_EXPORT +int Audit_Log_Read_Range_By_Sequence(BACNET_READ_RANGE_DATA *pRequest); +BACNET_STACK_EXPORT +int Audit_Log_Read_Range_By_Time(BACNET_READ_RANGE_DATA *pRequest); +BACNET_STACK_EXPORT +int Audit_Log_Read_Range(uint8_t *apdu, BACNET_READ_RANGE_DATA *pRequest); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif diff --git a/src/bacnet/property.c b/src/bacnet/property.c index 426a0a5f..d5571d06 100644 --- a/src/bacnet/property.c +++ b/src/bacnet/property.c @@ -2688,6 +2688,45 @@ static const int Trend_Log_Multiple_Properties_Optional[] = { -1 }; +static const int Audit_Log_Properties_Required[] = { + /* unordered list of properties */ + PROP_OBJECT_IDENTIFIER, + PROP_OBJECT_NAME, + PROP_OBJECT_TYPE, + PROP_STATUS_FLAGS, + PROP_EVENT_STATE, + PROP_ENABLE, + PROP_BUFFER_SIZE, + PROP_LOG_BUFFER, + PROP_RECORD_COUNT, + PROP_TOTAL_RECORD_COUNT, + -1 +}; + +static const int Audit_Log_Properties_Optional[] = { + /* unordered list of properties */ + PROP_DESCRIPTION, + PROP_RELIABILITY, + PROP_MEMBER_OF, + PROP_DELETE_ON_FORWARD, + PROP_ISSUE_CONFIRMED_NOTIFICATIONS, + PROP_EVENT_DETECTION_ENABLE, + PROP_NOTIFICATION_CLASS, + PROP_EVENT_ENABLE, + PROP_ACKED_TRANSITIONS, + PROP_NOTIFY_TYPE, + PROP_EVENT_TIME_STAMPS, + PROP_EVENT_MESSAGE_TEXTS, + PROP_EVENT_MESSAGE_TEXTS_CONFIG, + PROP_RELIABILITY_EVALUATION_INHIBIT, + PROP_AUDIT_LEVEL, + PROP_AUDITABLE_OPERATIONS, + PROP_TAGS, + PROP_PROFILE_LOCATION, + PROP_PROFILE_NAME, + -1 +}; + /** * Function that returns the list of all Optional properties * of known standard objects. @@ -2891,6 +2930,9 @@ const int *property_list_optional(BACNET_OBJECT_TYPE object_type) case OBJECT_TREND_LOG_MULTIPLE: pList = Trend_Log_Multiple_Properties_Optional; break; + case OBJECT_AUDIT_LOG: + pList = Audit_Log_Properties_Optional; + break; default: pList = Default_Properties_Optional; break; @@ -3102,6 +3144,9 @@ const int *property_list_required(BACNET_OBJECT_TYPE object_type) case OBJECT_TREND_LOG_MULTIPLE: pList = Trend_Log_Multiple_Properties_Required; break; + case OBJECT_AUDIT_LOG: + pList = Audit_Log_Properties_Required; + break; default: pList = Default_Properties_Required; break; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 292975f1..57986297 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -78,6 +78,7 @@ list(APPEND testdirs bacnet/awf bacnet/bacaddr bacnet/bacapp + bacnet/bacaudit bacnet/bacdcode bacnet/bacdevobjpropref bacnet/bacdest @@ -143,6 +144,7 @@ list(APPEND testdirs bacnet/basic/object/ai bacnet/basic/object/ao bacnet/basic/object/av + bacnet/basic/object/auditlog bacnet/basic/object/bacfile bacnet/basic/object/bi bacnet/basic/object/bitstring_value diff --git a/test/bacnet/bacaudit/CMakeLists.txt b/test/bacnet/bacaudit/CMakeLists.txt new file mode 100644 index 00000000..99204cb6 --- /dev/null +++ b/test/bacnet/bacaudit/CMakeLists.txt @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: MIT + +cmake_minimum_required(VERSION 3.10 FATAL_ERROR) + +get_filename_component(basename ${CMAKE_CURRENT_SOURCE_DIR} NAME) +project(test_${basename} + VERSION 1.0.0 + LANGUAGES C) + + +string(REGEX REPLACE + "/test/bacnet/[a-zA-Z_/-]*$" + "/src" + SRC_DIR + ${CMAKE_CURRENT_SOURCE_DIR}) +string(REGEX REPLACE + "/test/bacnet/[a-zA-Z_/-]*$" + "/test" + TST_DIR + ${CMAKE_CURRENT_SOURCE_DIR}) +set(ZTST_DIR "${TST_DIR}/ztest/src") + +add_compile_definitions( + BIG_ENDIAN=0 + CONFIG_ZTEST=1 + BACAPP_MINIMAL=1 + BACAPP_DESTINATION=1 + ) + +include_directories( + ${SRC_DIR} + ${TST_DIR}/ztest/include + ) + +add_executable(${PROJECT_NAME} + # File(s) under test + ${SRC_DIR}/bacnet/bacaudit.c + # Support files and stubs (pathname alphabetical) + ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/bacaction.c + ${SRC_DIR}/bacnet/bacaddr.c + ${SRC_DIR}/bacnet/bacapp.c + ${SRC_DIR}/bacnet/bacdcode.c + ${SRC_DIR}/bacnet/bacdest.c + ${SRC_DIR}/bacnet/bacdevobjpropref.c + ${SRC_DIR}/bacnet/bacerror.c + ${SRC_DIR}/bacnet/bacint.c + ${SRC_DIR}/bacnet/bacreal.c + ${SRC_DIR}/bacnet/bacstr.c + ${SRC_DIR}/bacnet/bactext.c + ${SRC_DIR}/bacnet/basic/sys/bigend.c + ${SRC_DIR}/bacnet/datetime.c + ${SRC_DIR}/bacnet/basic/sys/days.c + ${SRC_DIR}/bacnet/indtext.c + ${SRC_DIR}/bacnet/hostnport.c + ${SRC_DIR}/bacnet/lighting.c + ${SRC_DIR}/bacnet/timestamp.c + ${SRC_DIR}/bacnet/timesync.c + ${SRC_DIR}/bacnet/weeklyschedule.c + ${SRC_DIR}/bacnet/bactimevalue.c + ${SRC_DIR}/bacnet/dailyschedule.c + ${SRC_DIR}/bacnet/calendar_entry.c + ${SRC_DIR}/bacnet/special_event.c + ${SRC_DIR}/bacnet/channel_value.c + ${SRC_DIR}/bacnet/secure_connect.c + # Test and test library files + ./src/main.c + ${ZTST_DIR}/ztest_mock.c + ${ZTST_DIR}/ztest.c + ) diff --git a/test/bacnet/bacaudit/src/main.c b/test/bacnet/bacaudit/src/main.c new file mode 100644 index 00000000..2c0bb173 --- /dev/null +++ b/test/bacnet/bacaudit/src/main.c @@ -0,0 +1,259 @@ +/** + * @file + * @brief Unit test for for BACnetAuditNotification and BACnetAuditLogRecord + * @author Steve Karg + * @date November 2024 + * @copyright SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include +#include + +/** + * @addtogroup bacnet_tests + * @{ + */ + +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bacnet_audit_tests, test_bacnet_audit_value) +#else +static void test_bacnet_audit_value(void) +#endif +{ + uint8_t apdu[MAX_APDU] = { 0 }; + BACNET_AUDIT_VALUE value = { 0 }, test_value = { 0 }; + int apdu_len = 0, null_len = 0, test_len = 0, tag_len = 0, value_len = 0; + uint8_t tag_number = 1; + bool status = false; + + null_len = bacnet_audit_value_encode(NULL, &value); + apdu_len = bacnet_audit_value_encode(apdu, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_audit_value_decode(apdu, apdu_len, &value); + zassert_equal(apdu_len, test_len, NULL); + zassert_true(bacnet_audit_value_same(&value, &test_value), NULL); + + /* decoding, some negative tests */ + test_len = bacnet_audit_value_decode(NULL, apdu_len, &test_value); + zassert_equal(test_len, BACNET_STATUS_ERROR, NULL); + test_len = bacnet_audit_value_decode(apdu, 0, &test_value); + zassert_equal(test_len, BACNET_STATUS_ERROR, NULL); + test_len = bacnet_audit_value_decode(apdu, apdu_len, NULL); + zassert_equal(test_len, BACNET_STATUS_ERROR, NULL); + /* out of range tag */ + value.tag = 255; + apdu_len = bacnet_audit_value_encode(apdu, &value); + test_len = bacnet_audit_value_decode(apdu, apdu_len, &value); + zassert_equal(test_len, BACNET_STATUS_ERROR, NULL); + + /* value type = boolean */ + value.tag = BACNET_APPLICATION_TAG_BOOLEAN; + value.type.boolean_value = true; + null_len = bacnet_audit_value_encode(NULL, &value); + apdu_len = bacnet_audit_value_encode(apdu, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_audit_value_decode(apdu, apdu_len, &test_value); + zassert_equal(apdu_len, test_len, NULL); + zassert_true(bacnet_audit_value_same(&value, &test_value), NULL); + + /* value type = unsigned */ + value.tag = BACNET_APPLICATION_TAG_UNSIGNED_INT; + value.type.unsigned_value = 1234; + null_len = bacnet_audit_value_encode(NULL, &value); + apdu_len = bacnet_audit_value_encode(apdu, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_audit_value_decode(apdu, apdu_len, &test_value); + zassert_equal(apdu_len, test_len, NULL); + zassert_true(bacnet_audit_value_same(&value, &test_value), NULL); + + /* value type = signed */ + value.tag = BACNET_APPLICATION_TAG_SIGNED_INT; + value.type.integer_value = -1234; + null_len = bacnet_audit_value_encode(NULL, &value); + apdu_len = bacnet_audit_value_encode(apdu, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_audit_value_decode(apdu, apdu_len, &test_value); + zassert_equal(apdu_len, test_len, NULL); + zassert_true(bacnet_audit_value_same(&value, &test_value), NULL); + + /* value type = REAL */ + value.tag = BACNET_APPLICATION_TAG_REAL; + value.type.real_value = 3.14159f; + null_len = bacnet_audit_value_encode(NULL, &value); + apdu_len = bacnet_audit_value_encode(apdu, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_audit_value_decode(apdu, apdu_len, &test_value); + zassert_equal(apdu_len, test_len, NULL); + zassert_true(bacnet_audit_value_same(&value, &test_value), NULL); + + /* value type = ENUMERATED */ + value.tag = BACNET_APPLICATION_TAG_ENUMERATED; + value.type.enumerated_value = 1234; + null_len = bacnet_audit_value_encode(NULL, &value); + apdu_len = bacnet_audit_value_encode(apdu, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_audit_value_decode(apdu, apdu_len, &test_value); + zassert_equal(apdu_len, test_len, NULL); + zassert_true(bacnet_audit_value_same(&value, &test_value), NULL); + + /* negative tests */ + null_len = bacnet_audit_value_encode(NULL, NULL); + zassert_equal(null_len, 0, NULL); + value.tag = 255; + null_len = bacnet_audit_value_encode(NULL, &value); + zassert_equal(null_len, 1, NULL); + + /* context encoded */ + value.tag = BACNET_APPLICATION_TAG_BOOLEAN; + value.type.boolean_value = true; + null_len = bacnet_audit_value_context_encode(NULL, tag_number, &value); + apdu_len = bacnet_audit_value_context_encode(apdu, tag_number, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = 0; + status = bacnet_is_opening_tag_number(apdu, apdu_len, tag_number, &tag_len); + zassert_true(status, NULL); + test_len += tag_len; + value_len = bacnet_audit_value_decode( + &apdu[test_len], apdu_len - test_len, &test_value); + test_len += value_len; + status = bacnet_is_closing_tag_number( + &apdu[test_len], apdu_len - test_len, tag_number, &tag_len); + zassert_true(status, NULL); + test_len += tag_len; + zassert_equal(apdu_len, test_len, NULL); +} + +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bacnet_audit_tests, test_bacnet_audit_log_notification) +#else +static void test_bacnet_audit_log_notification(void) +#endif +{ + uint8_t apdu[MAX_APDU] = { 0 }; + BACNET_AUDIT_NOTIFICATION value = { 0 }, test_value = { 0 }; + int apdu_len = 0, null_len = 0, test_len = 0; + + null_len = bacnet_audit_log_notification_encode(NULL, &value); + apdu_len = bacnet_audit_log_notification_encode(apdu, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_audit_log_notification_decode(apdu, apdu_len, &value); + zassert_equal(apdu_len, test_len, NULL); + zassert_true(bacnet_audit_log_notification_same(&value, &test_value), NULL); + + /* decoding, some negative tests */ + test_len = + bacnet_audit_log_notification_decode(NULL, apdu_len, &test_value); + zassert_equal(test_len, BACNET_STATUS_ERROR, NULL); + test_len = bacnet_audit_log_notification_decode(apdu, 0, &test_value); + zassert_equal(test_len, BACNET_STATUS_ERROR, NULL); + test_len = bacnet_audit_log_notification_decode(apdu, apdu_len, NULL); + zassert_equal(test_len, apdu_len, NULL); +} + +uint8_t Test_APDU[MAX_APDU]; +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bacnet_audit_tests, test_bacnet_audit_log_record) +#else +static void test_bacnet_audit_log_record(void) +#endif +{ + uint8_t *apdu = Test_APDU; + BACNET_AUDIT_LOG_RECORD value = { 0 }, test_value = { 0 }; + int apdu_len = 0, null_len = 0, test_len = 0; + bool status = false; + BACNET_AUDIT_NOTIFICATION *notification = NULL; + + value.tag = AUDIT_LOG_DATUM_TAG_STATUS; + datetime_date_init_ascii(&value.timestamp.date, "2024/11/30"); + datetime_time_init_ascii(&value.timestamp.time, "23:59:59.99"); + null_len = bacnet_audit_log_record_encode(NULL, &value); + apdu_len = bacnet_audit_log_record_encode(apdu, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_audit_log_record_decode(apdu, apdu_len, &test_value); + zassert_equal(apdu_len, test_len, NULL); + zassert_true(bacnet_audit_log_record_same(&value, &test_value), NULL); + + /* decoding, some negative tests */ + test_len = bacnet_audit_log_record_decode(NULL, apdu_len, &test_value); + zassert_equal(test_len, BACNET_STATUS_ERROR, NULL); + test_len = bacnet_audit_log_record_decode(apdu, 0, &test_value); + zassert_equal(test_len, BACNET_STATUS_ERROR, NULL); + test_len = bacnet_audit_log_record_decode(apdu, apdu_len, NULL); + zassert_equal(test_len, apdu_len, NULL); + status = bacnet_audit_log_record_same(&value, NULL); + zassert_false(status, NULL); + status = bacnet_audit_log_record_same(NULL, &value); + zassert_false(status, NULL); + value.tag = 255; + test_value.tag = 255; + status = bacnet_audit_log_record_same(&value, &test_value); + zassert_false(status, NULL); + + /* record type = notification */ + value.tag = AUDIT_LOG_DATUM_TAG_NOTIFICATION; + notification = &value.log_datum.notification; + + bacapp_timestamp_sequence_set(¬ification->source_timestamp, 1234); + bacapp_timestamp_sequence_set(¬ification->target_timestamp, 5678); + notification->source_device.tag = BACNET_RECIPIENT_TAG_DEVICE; + notification->source_device.type.device.instance = 1234; + notification->source_device.type.device.type = OBJECT_DEVICE; + notification->source_object.type = OBJECT_ANALOG_INPUT; + notification->source_object.instance = 5678; + notification->operation = AUDIT_OPERATION_DEVICE_RESET; + characterstring_init_ansi(¬ification->source_comment, "Hello, World!"); + characterstring_init_ansi(¬ification->target_comment, "Goodbye, World!"); + notification->invoke_id = 123; + notification->source_user_id = 456; + notification->source_user_role = 7; + notification->target_device.tag = BACNET_RECIPIENT_TAG_DEVICE; + notification->target_device.type.device.instance = 5678; + notification->target_device.type.device.type = OBJECT_DEVICE; + notification->target_object.type = OBJECT_ANALOG_INPUT; + notification->target_object.instance = 1234; + notification->target_property.property_identifier = PROP_PRESENT_VALUE; + notification->target_property.property_array_index = BACNET_ARRAY_ALL; + notification->target_priority = 8; + notification->target_value.tag = BACNET_APPLICATION_TAG_REAL; + notification->target_value.type.real_value = 3.14159f; + notification->current_value.tag = BACNET_APPLICATION_TAG_REAL; + notification->current_value.type.real_value = 2.71828f; + notification->result = ERROR_CODE_OTHER; + null_len = bacnet_audit_log_record_encode(NULL, &value); + apdu_len = bacnet_audit_log_record_encode(apdu, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_audit_log_record_decode(apdu, apdu_len, &test_value); + zassert_equal(apdu_len, test_len, NULL); + zassert_true(bacnet_audit_log_record_same(&value, &test_value), NULL); + + /* record type = time-change */ + value.tag = AUDIT_LOG_DATUM_TAG_TIME_CHANGE; + value.log_datum.time_change = 3.14159; + null_len = bacnet_audit_log_record_encode(NULL, &value); + apdu_len = bacnet_audit_log_record_encode(apdu, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_audit_log_record_decode(apdu, apdu_len, &test_value); + zassert_equal(apdu_len, test_len, NULL); + zassert_true(bacnet_audit_log_record_same(&value, &test_value), NULL); +} +/** + * @} + */ + +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST_SUITE(bacnet_audit_tests, NULL, NULL, NULL, NULL, NULL); +#else +void test_main(void) +{ + ztest_test_suite( + bacnet_audit_tests, ztest_unit_test(test_bacnet_audit_log_record), + ztest_unit_test(test_bacnet_audit_log_notification), + ztest_unit_test(test_bacnet_audit_value)); + + ztest_run_test_suite(bacnet_audit_tests); +} +#endif diff --git a/test/bacnet/basic/object/auditlog/CMakeLists.txt b/test/bacnet/basic/object/auditlog/CMakeLists.txt new file mode 100644 index 00000000..aad98616 --- /dev/null +++ b/test/bacnet/basic/object/auditlog/CMakeLists.txt @@ -0,0 +1,73 @@ +# SPDX-License-Identifier: MIT + +cmake_minimum_required(VERSION 3.10 FATAL_ERROR) + +get_filename_component(basename ${CMAKE_CURRENT_SOURCE_DIR} NAME) +project(test_${basename} + VERSION 1.0.0 + LANGUAGES C) + + +string(REGEX REPLACE + "/test/bacnet/[a-zA-Z_/-]*$" + "/src" + SRC_DIR + ${CMAKE_CURRENT_SOURCE_DIR}) +string(REGEX REPLACE + "/test/bacnet/[a-zA-Z_/-]*$" + "/test" + TST_DIR + ${CMAKE_CURRENT_SOURCE_DIR}) +set(ZTST_DIR "${TST_DIR}/ztest/src") + +add_compile_definitions( + BIG_ENDIAN=0 + CONFIG_ZTEST=1 + BACAPP_MINIMAL=1 + BACAPP_DATETIME=1 + ) + +include_directories( + ${SRC_DIR} + ${TST_DIR}/ztest/include + ${TST_DIR}/bacnet/basic/object/test + ) + +add_executable(${PROJECT_NAME} + # File(s) under test + ${SRC_DIR}/bacnet/basic/object/auditlog.c + # Support files and stubs (pathname alphabetical) + ${SRC_DIR}/bacnet/bacaddr.c + ${SRC_DIR}/bacnet/bacaudit.c + ${SRC_DIR}/bacnet/bacapp.c + ${SRC_DIR}/bacnet/bacdcode.c + ${SRC_DIR}/bacnet/bacdest.c + ${SRC_DIR}/bacnet/bacdevobjpropref.c + ${SRC_DIR}/bacnet/bacerror.c + ${SRC_DIR}/bacnet/bacint.c + ${SRC_DIR}/bacnet/bacreal.c + ${SRC_DIR}/bacnet/bacstr.c + ${SRC_DIR}/bacnet/bactext.c + ${SRC_DIR}/bacnet/basic/sys/bigend.c + ${SRC_DIR}/bacnet/basic/sys/debug.c + ${SRC_DIR}/bacnet/basic/sys/keylist.c + ${SRC_DIR}/bacnet/basic/sys/days.c + ${SRC_DIR}/bacnet/datetime.c + ${SRC_DIR}/bacnet/dcc.c + ${SRC_DIR}/bacnet/indtext.c + ${SRC_DIR}/bacnet/hostnport.c + ${SRC_DIR}/bacnet/lighting.c + ${SRC_DIR}/bacnet/timestamp.c + ${SRC_DIR}/bacnet/wp.c + ${SRC_DIR}/bacnet/weeklyschedule.c + ${SRC_DIR}/bacnet/bactimevalue.c + ${SRC_DIR}/bacnet/dailyschedule.c + ${SRC_DIR}/bacnet/proplist.c + # Test and test library files + ./src/main.c + ${TST_DIR}/bacnet/basic/object/test/property_test.c + ${TST_DIR}/bacnet/basic/object/test/device_mock.c + ${TST_DIR}/bacnet/basic/object/test/datetime_local.c + ${ZTST_DIR}/ztest_mock.c + ${ZTST_DIR}/ztest.c + ) diff --git a/test/bacnet/basic/object/auditlog/src/main.c b/test/bacnet/basic/object/auditlog/src/main.c new file mode 100644 index 00000000..f225de3d --- /dev/null +++ b/test/bacnet/basic/object/auditlog/src/main.c @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2023 Legrand North America, LLC. + * + * SPDX-License-Identifier: MIT + */ + +/* @file + * @brief test BACnet auditlog APIs + */ + +#include +#include +#include +#include +#include + +/** + * @addtogroup bacnet_tests + * @{ + */ + +/** + * @brief Test Auditlog handling + + */ +static void testAuditlog(void) +{ + int len = 0, index = 0; + const uint32_t instance = 1; + uint32_t test_instance = 0; + const int skip_fail_property_list[] = { -1 }; + bool status = false; + BACNET_WRITE_PROPERTY_DATA wp_data = { 0 }; + uint32_t buffer_size = 0; + + Audit_Log_Init(); + len = Audit_Log_Count(); + zassert_true(len == 0, NULL); + status = Audit_Log_Valid_Instance(instance); + zassert_false(status, NULL); + test_instance = Audit_Log_Create(instance); + zassert_true(test_instance == instance, NULL); + len = Audit_Log_Count(); + zassert_true(len == 1, NULL); + test_instance = Audit_Log_Index_To_Instance(0); + zassert_true(test_instance == instance, NULL); + index = Audit_Log_Instance_To_Index(instance); + zassert_true(index == 0, NULL); + status = Audit_Log_Valid_Instance(instance); + zassert_true(status, NULL); + + /* configure ReadProperty test */ + /* perform a general test for RP/WP */ + bacnet_object_properties_read_write_test( + OBJECT_AUDIT_LOG, instance, Audit_Log_Property_Lists, + Audit_Log_Read_Property, Audit_Log_Write_Property, + skip_fail_property_list); + + wp_data.object_type = OBJECT_AUDIT_LOG; + wp_data.object_instance = instance; + wp_data.priority = BACNET_NO_PRIORITY; + wp_data.array_index = BACNET_ARRAY_ALL; + + /* test buffer size */ + buffer_size = 512; + wp_data.object_property = PROP_BUFFER_SIZE; + wp_data.application_data_len = + encode_application_unsigned(wp_data.application_data, buffer_size); + status = Audit_Log_Write_Property(&wp_data); + zassert_true(status, NULL); + buffer_size = INT_MAX; + buffer_size++; + wp_data.application_data_len = + encode_application_unsigned(wp_data.application_data, buffer_size); + status = Audit_Log_Write_Property(&wp_data); + /* error out of range */ + zassert_false(status, NULL); + zassert_equal(wp_data.error_class, ERROR_CLASS_PROPERTY, NULL); + zassert_equal(wp_data.error_code, ERROR_CODE_VALUE_OUT_OF_RANGE, NULL); + + wp_data.object_property = PROP_ENABLE; + wp_data.application_data_len = + encode_application_boolean(wp_data.application_data, true); + status = Audit_Log_Write_Property(&wp_data); + zassert_true(status, NULL); + + wp_data.object_property = PROP_BUFFER_SIZE; + wp_data.application_data_len = + encode_application_unsigned(wp_data.application_data, buffer_size); + status = Audit_Log_Write_Property(&wp_data); + /* error when enabled=true */ + zassert_false(status, NULL); + zassert_equal(wp_data.error_class, ERROR_CLASS_PROPERTY, NULL); + zassert_equal(wp_data.error_code, ERROR_CODE_WRITE_ACCESS_DENIED, NULL); + + /* test object name */ + bacnet_object_name_ascii_test( + instance, Audit_Log_Name_Set, Audit_Log_Name_ASCII); + bacnet_object_name_ascii_test( + instance, Audit_Log_Description_Set, Audit_Log_Description); + + status = Audit_Log_Delete(instance); + zassert_true(status, NULL); + Audit_Log_Cleanup(); + len = Audit_Log_Count(); + zassert_true(len == 0, NULL); + status = Audit_Log_Valid_Instance(instance); + zassert_false(status, NULL); +} + +static void testLogs(void) +{ + const uint32_t instance = 1; + uint32_t test_instance = 0; + int len = 0; + uint32_t buffer_size = 0, test_buffer_size = 0, original_buffer_size = 0; + uint32_t record_count = 0; + uint32_t total_record_count = 0; + bool status = false; + BACNET_AUDIT_LOG_RECORD *record; + + Audit_Log_Init(); + test_instance = Audit_Log_Create(instance); + zassert_true(test_instance == instance, NULL); + len = Audit_Log_Count(); + zassert_true(len > 0, NULL); + /* Log Buffer settings */ + buffer_size = Audit_Log_Buffer_Size(instance); + zassert_true(buffer_size > 0, NULL); + buffer_size = INT_MAX; + buffer_size++; + status = Audit_Log_Buffer_Size_Set(instance, buffer_size); + zassert_false(status, NULL); + original_buffer_size = Audit_Log_Buffer_Size(instance); + buffer_size = original_buffer_size / 2; + status = Audit_Log_Buffer_Size_Set(instance, buffer_size); + zassert_true(status, NULL); + test_buffer_size = Audit_Log_Buffer_Size(instance); + zassert_true(test_buffer_size == buffer_size, NULL); + status = Audit_Log_Buffer_Size_Set(instance, original_buffer_size); + zassert_true(status, NULL); + /* Log Buffer records manipulation */ + record_count = Audit_Log_Record_Count(instance); + zassert_true(record_count == 0, NULL); + total_record_count = Audit_Log_Total_Record_Count(instance); + zassert_true(total_record_count == 0, NULL); + status = Audit_Log_Enable(instance); + zassert_false(status, NULL); + status = Audit_Log_Enable_Set(instance, false); + zassert_true(status, NULL); + /* start logging */ + status = Audit_Log_Enable_Set(instance, true); + zassert_true(status, NULL); + record_count = Audit_Log_Record_Count(instance); + zassert_true(record_count == 1, NULL); + total_record_count = Audit_Log_Total_Record_Count(instance); + zassert_true(total_record_count == 1, NULL); + record = Audit_Log_Record_Entry(instance, 0); + zassert_not_null(record, NULL); + zassert_true(record->tag == AUDIT_LOG_DATUM_TAG_STATUS, NULL); + zassert_true( + BIT_CHECK(record->log_datum.log_status, LOG_STATUS_LOG_DISABLED) == 0, + NULL); + Audit_Log_Record_Status_Insert(instance, LOG_STATUS_LOG_INTERRUPTED, true); + record_count = Audit_Log_Record_Count(instance); + zassert_true(record_count == 2, NULL); + total_record_count = Audit_Log_Total_Record_Count(instance); + zassert_true(total_record_count == 2, NULL); + record = Audit_Log_Record_Entry(instance, 1); + zassert_not_null(record, NULL); + zassert_true(record->tag == AUDIT_LOG_DATUM_TAG_STATUS, NULL); + zassert_true( + BIT_CHECK(record->log_datum.log_status, LOG_STATUS_LOG_INTERRUPTED), + NULL); + Audit_Log_Record_Entry_Delete(instance, 1); + record_count = Audit_Log_Record_Count(instance); + zassert_true(record_count == 1, "record_count: %d", record_count); + total_record_count = Audit_Log_Total_Record_Count(instance); + zassert_true(total_record_count == 2, NULL); + record = Audit_Log_Record_Entry(instance, 0); + zassert_not_null(record, NULL); + zassert_true(record->tag == AUDIT_LOG_DATUM_TAG_STATUS, NULL); + zassert_true( + BIT_CHECK(record->log_datum.log_status, LOG_STATUS_LOG_DISABLED) == 0, + NULL); + + Audit_Log_Cleanup(); +} +/** + * @} + */ + +void test_main(void) +{ + ztest_test_suite( + auditlog_tests, ztest_unit_test(testAuditlog), + ztest_unit_test(testLogs)); + + ztest_run_test_suite(auditlog_tests); +}