image by freephotocc on pixabay.com

Why Audit Logs? (Hibernate Envers)

Patroclos Lemoniatis
7 min readJul 2, 2023

Lets start with the basics. What are Audit Logs?

Audit logging is the process of capturing user activity across an application system. Specifically event name, event date and time, user who executed the event and what was impacted from the event.

Each application has processes that are being executed by users to save and update data. Every time a change is applied on a data entity, a record is created or updated in the database.

These activities produce event/audit logs, that are required by the system administrators in order to have an overall picture of the application’s activity.

Consider, the scenario where sensitive data have been deleted or updated resulting in a “damaging” event. If no trace is left by these “invalid” events, no investigation is possible and probably no data fix can be applied to revert to the correct data.

Another, scenario, is where an unauthorized user is deleting or updating the data. Audit logs effectively can pinpoint the culprit, giving the exact date, time, and what data has the user changed.

C-I-A Confidentiality, Integrity, and Availability

The basis for securing an application system

  • Confidentiality: Access to data has to be controlled to prevent unauthorized sharing of data. Enable access control policies, encrypt data, and use multi-factor authentication.
  • Integrity: Data should be trustworthy and free of tampering. Usage of hashing, encryption, digital certificates, or digital signatures.
  • Availability: Data should be available to users at all times. Organizations can use redundant networks, servers, and applications. These can be programmed to become available when the primary system has been disrupted or broken, as to serve the data needs of the users.

To achieve the C-I-A triad we have to enforce the below principles. Lets focus on Auditing…

Loren Kohnfelder, 2021, Designing Secure Software: A Guide for Developers

Authentication: High-assurance determination of the identity of a principal

Authorization: Reliably only allowing an action by an authenticated principal

Auditing: Maintaining a reliable record of actions by principals for inspection

Auditing

In order for an organization to audit system activity, the system must produce a reliable log of all events that are critical to maintaining security. These include authentication and authorization events, system startup and shutdown, software updates, administrative accesses, and so forth. Audit logs must also be tamper-resistant, and ideally even difficult for administrators to meddle with, to be considered fully reliable records. Auditing is a critical leg of the Gold Standard, because incidents do happen, and authentication and authorization policies can be flawed. Auditing can also serve as mitigation for inside jobs in which trusted principals cause harm, providing necessary oversight.³

Why use Audit Logs?

* System Troubleshooting

Audit logs provide vital information of system processes which in turn could provide assistance to the performance enhancement or bug fixing of an application system.

* Accountability

Audit logs help an organization capture security and authentication as well as privileged activity information. This promotes user accountability for their actions across an application system. To do this effectively, audit logs have to be captured and stored regularly to show actions before and after an event has occurred.

* Reconstruction of events

Audit logs may also be used to “replay” events to help understand how a damaging event has occurred. Logs are configured to capture detailed data changes, therefore data at a certain point of time, can sometimes be reconstructed from logs.

* Providing Legal Validity

Audit logs can provide proof of a user actions.

What do Audit Logs capture?

Audit logs capture the following type of information

  • Event Name
  • Brief description of the event
  • Execution Date and Time
  • User who executed the event
  • Application/IP/Device/Entity that was impacted
  • Application/IP/Device from where the execution of event occurred
  • Custom tags like Severity of the event

Lets go though a real application example

User logs into the application …

Here is the audit log of the user log-in

User creates a new Article

Here is the audit log of the new Article which was created
Detailed info of the properties changed in relation to the event for the specific entity affected

User updates the Article. Edits the author and adds a new Comment

Here is the Audit log of the update event and the affected entities
Detailed info of the properties affected in relation to the update event for the specific entity affected
Here is the new ManyToOne (ArticleComment) child entity created from the update of the parent entity Article
Newly created ManyToOne (ArticleComment) child entity event detailed info

Changes as recorded in the database level

Here are the AUDIT Tables

Articles_AUD table
ArticleComments_AUD table

So each time an entity is created/modified or deleted, a record is created in the corresponding audit table, which has a unique revision number [similar to Subversion/Git versioning]. This makes it possible to reconstruct/checkout an entity as it was at a specific point in time.

Few words in regards to the above example

Above example is built with Java Spring Framework application [Spring MVC v6, java 17], uses Hibernate ORM v6.1.6 and Hibernate Envers v6.1.6 for Activity logging.

Hibernate Envers¹

It is a module that works in conjunction with Hibernate ORM library to help implement core audit logging functionality, to maintain detailed revision logs of creation and update events on model entities.

As per the example above, this is the code used by the model Entities to handle Audit changes. Please note for every entity table there is a corresponding *_AUD (audit table) to hold the history revisions.

CREATE TABLE ARTICLES (
id bigint NOT NULL AUTO_INCREMENT,
title varchar(200) NOT NULL,
category varchar(100) NULL,
summary varchar(200) NULL,
author varchar(100) NULL,
createdBy bigint,
createdDate timestamp NOT NULL,
lastModifiedBy bigint,
lastModifiedDate timestamp NOT NULL,
lastUpdatedByprocessId varchar(200) NULL,
isdeleted bigint NOT NULL default 0,
version bigint NOT NULL,
PRIMARY KEY (Id)
);

CREATE TABLE ARTICLES_AUD (
id bigint NOT NULL,
title varchar(200),
title_mod boolean,
category varchar(100),
category_mod boolean,
summary varchar(200),
summary_mod boolean,
author varchar(100),
author_mod boolean,
createdBy bigint,
createdBy_mod boolean,
createdDate timestamp,
createdDate_mod boolean,
lastModifiedBy bigint,
lastModifiedBy_mod boolean,
lastModifiedDate timestamp,
lastModifiedDate_mod boolean,
version bigint,
version_mod boolean,
lastUpdatedByprocessId varchar(200),
lastUpdatedByprocessId_mod boolean,
isdeleted bigint,
isdeleted_mod boolean,
REV INTEGER NOT NULL,
REVTYPE INTEGER NOT NULL,
PRIMARY KEY (Id, REV)
);

CREATE TABLE ARTICLECOMMENTS (
id bigint NOT NULL AUTO_INCREMENT,
comment varchar(max) NOT NULL,
article_id bigint NULL,
createdBy bigint,
createdDate timestamp NOT NULL,
lastModifiedBy bigint,
lastModifiedDate timestamp NOT NULL,
lastUpdatedByprocessId varchar(200) NULL,
isdeleted bigint NOT NULL default 0,
version bigint NOT NULL,
PRIMARY KEY (Id)
);

CREATE TABLE ARTICLECOMMENTS_AUD (
id bigint NOT NULL,
comment varchar(max) NULL,
comment_mod boolean,
article_id bigint NULL,
article_id_mod boolean,
createdBy bigint,
createdBy_mod boolean,
createdDate timestamp,
createdDate_mod boolean,
lastModifiedBy bigint,
lastModifiedBy_mod boolean,
lastModifiedDate timestamp,
lastModifiedDate_mod boolean,
version bigint,
version_mod boolean,
lastUpdatedByprocessId varchar(200),
lastUpdatedByprocessId_mod boolean,
isdeleted bigint,
isdeleted_mod boolean,
REV INTEGER NOT NULL,
REVTYPE INTEGER NOT NULL,
PRIMARY KEY (Id, REV)
);

CREATE TABLE CITATIONS (
id bigint NOT NULL AUTO_INCREMENT,
title varchar(200) NOT NULL,
link varchar(100) NULL,
citationtype varchar(200) NULL,
createdBy bigint,
createdDate timestamp NOT NULL,
lastModifiedBy bigint,
lastModifiedDate timestamp NOT NULL,
lastUpdatedByprocessId varchar(200) NULL,
isdeleted bigint NOT NULL default 0,
version bigint NOT NULL,
PRIMARY KEY (Id)
);

CREATE TABLE CITATIONS_AUD (
id bigint NOT NULL,
title varchar(200),
title_mod boolean,
link varchar(100) NULL,
link_mod boolean,
citationtype varchar(200) NULL,
citationtype_mod boolean,
createdBy bigint,
createdBy_mod boolean,
createdDate timestamp,
createdDate_mod boolean,
lastModifiedBy bigint,
lastModifiedBy_mod boolean,
lastModifiedDate timestamp,
lastModifiedDate_mod boolean,
version bigint,
version_mod boolean,
lastUpdatedByprocessId varchar(200),
lastUpdatedByprocessId_mod boolean,
isdeleted bigint,
isdeleted_mod boolean,
REV INTEGER NOT NULL,
REVTYPE INTEGER NOT NULL,
PRIMARY KEY (Id, REV)
);

CREATE TABLE ARTICLE_CITATION (
article_id bigint not null,
citation_id bigint not null,
constraint fk_article_citation_article foreign key(article_id) references articles(id),
constraint fk_article_citation_citation foreign key(citation_id) references citations(id)
);

CREATE TABLE ARTICLE_CITATION_AUD (
article_id bigint not null,
citation_id bigint not null,
REV INTEGER NOT NULL,
REVTYPE INTEGER NOT NULL,
constraint fk_article_citation_article2 foreign key(article_id) references articles(id),
constraint fk_article_citation_citation2 foreign key(citation_id) references citations(id)
);

CREATE TABLE REVINFO (
REV INTEGER GENERATED BY DEFAULT AS IDENTITY,
REVTSTMP BIGINT,
PRIMARY KEY (REV)
);
@Audited
@Entity
@Table(name="articles")
public class Article extends BaseO {

private static final long serialVersionUID = 1L;

@Id
@NotAudited
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "articles_sequence")
@SequenceGenerator(name = "articles_sequence", sequenceName = "articles_sequence", allocationSize = 1)
@Column(name="id", updatable = false)
private long id;

@Audited(withModifiedFlag = true)
@Column(name="title")
private String title;

@Audited(withModifiedFlag = true)
@Column(name="category")
private String category;

@Audited(withModifiedFlag = true)
@Column(name="summary")
private String summary;

@Audited(withModifiedFlag = true)
@Column(name="author")
private String author;

@Audited
@AuditJoinTable(name = "article_citation_aud")
@ManyToMany(cascade=CascadeType.MERGE, fetch=FetchType.EAGER)
@JoinTable(
name="article_citation",
joinColumns={@JoinColumn(name="article_id")},
inverseJoinColumns={@JoinColumn(name="citation_id")})
private List<Citation> citations;

@Audited
@AuditJoinTable(name = "articlecomments_aud")
@OneToMany(mappedBy="article", fetch=FetchType.EAGER)
private List<ArticleComment> comments;

}

@Audited
@Entity
@Table(name="articlecomments")
public class ArticleComment extends BaseO {

private static final long serialVersionUID = 1L;

@Id
@NotAudited
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "articlecomments_sequence")
@SequenceGenerator(name = "articlecomments_sequence", sequenceName = "articlecomments_sequence", allocationSize = 1)
@Column(name="id", updatable = false)
private long id;

@Audited(withModifiedFlag = true)
@Column(name="comment")
private String comment;

@Audited(withModifiedFlag = true, modifiedColumnName = "article_id_mod")
@ManyToOne
@JoinColumn(name="article_id")
private Article article;

}
public class AuditEnversRepository {

@jakarta.persistence.PersistenceContext
protected EntityManager em;

public List<Object[]> getEntityRevisionsByProcessId(Class<? extends BaseO> o, Long id, String processId) {
AuditReader auditReader = AuditReaderFactory.get(em);
List<Object[]> revResults = auditReader.createQuery()
.forRevisionsOfEntityWithChanges(o, false)
.add(AuditEntity.property("lastUpdatedByprocessId").eq(processId))
.add(AuditEntity.property("id").eq(id))
.getResultList();
return revResults;
}

}

Hibernate Envers provides a convenient AuditReader² API class to retrieve entity changes.

[1] https://hibernate.org/orm/envers/

[2]Hibernate Envers Tutorial

[3] Loren Kohnfelder, 2021, Designing Secure Software: A Guide for Developers

--

--