Pop Rivett’s SQL Server FAQ No.5: Pop on the Audit Trail

Pop provides a cunning, trigger-based technique for auditing the activity on SQL Server tables

Pop does SQL Server auditing

 301-image001a.jpg

Good morning Pop, I see the turnips are doing well

Well, God bless you and keep you Miss, they do well, but we could do with a drop of rain. But do tell me, Miss why the long face?

Oh Pop…such a dreadful thing! I’ve just been checking through the accounts of the Womens’ Institute and someone has hacked in and removed everything we had in savings for the new Village Hall!

Lorks a’mercy, missy…but at least your audit trail will show up how it happened and who it was that did it?

What audit-tail?! [Subsiding into sobs] We haven’t got such a thing. The vicar’s son put the system together as a college project. Frankly, it’s lucky that the tables even have primary keys.

Well, when IT people talk of audit trails, they refer to keeping an auditable account of who changed the data in a table, when, from what and to what. Basically, an audit trail is a way of tracking the details of all changes to data in certain database tables – usually in order to check the integrity of financial transactions. It provides a record of who has accessed a database and what operations he or she has performed during a given period of time.

When data is financial, this is often built into the system. Records are never updated or deleted, merely superseded, or ‘terminated’, so that databases grow to become a history of all financial transactions. The application programmers are allowed access to the database only through stored procedures.

If you don’t have the luxury of a properly-designed financial system, as you don’t by the sounds of it, then you need to devise a means of logging all the transactions that you want to monitor. You might try looking in the database transaction log but the problem with this is that it’s an indiscriminate record of all transactions, whether data-feeds, cosmetic changes or data modifications – so it’s hard to use it to monitor and track down particular financial business transactions.

So what’s to be done then, Pop?!

Well, by my reckoning, an audit trigger is the way to go for you. Take a look at this…you simply create one of these triggers for every table that you need to monitor. This trigger will provide an ‘audit trail’ of all changes made to a table. In the line select @TableName = ‘trigtest’ you simply swap trigrest for the name of your table, and then the trigger will monitor all changes to that table.

It will place a record of these changes in a table called Audit. It records in Audit details of all inserted, deleted, changed columns and the old/new values of the fdata that was altered in the table that the trigger protects. Anyway, here is the code…notice that it will put out an error message if there is no primary key on the table as the routine would then be unable to identify the affected rows…so it’s lucky the vicar’s son got that right at least!

— Set up the tables
— Firstly, we create the audit table.
— There will only need to be one of these in a database

IF NOT EXISTS
(SELECT * FROM sysobjects WHERE id = OBJECT_ID(N'[dbo].[Audit]’
)
               AND OBJECTPROPERTY(id, N’IsUserTable’) = 1
)
       CREATE TABLE
Audit
               (Type CHAR(1
),
               TableName VARCHAR(128
),
               PK VARCHAR(1000
),
               FieldName VARCHAR(128
),
               OldValue VARCHAR(1000
),
               NewValue VARCHAR(1000
),
               UpdateDate datetime
,
               UserName VARCHAR(128
))
GO

— now we will illustrate the use of this tool
— by creating a dummy test table called TrigTest.

IF EXISTS
(SELECT * FROM sysobjects WHERE id = OBJECT_ID(N'[dbo].[trigtest]’
)
                AND OBJECTPROPERTY(id, N’IsUserTable’) = 1
)
DROP TABLE
[dbo].[trigtest]
GO
CREATE TABLE
trigtest
       (i INT
NOT NULL,
        j INT
NOT NULL,
        s VARCHAR(10
),
        t VARCHAR(10
))
GO

–note that for this system to work there must be a primary key
–to the table but then a table without a primary key
–isn’t really a table is it?
ALTER TABLE trigtest ADD CONSTRAINT pk PRIMARY KEY (i, j
)
GO

–and now create the trigger itself. This has to be created for every
-table you want to monitor

CREATE TRIGGER tr_trigtest ON trigtest FOR INSERT, UPDATE, DELETE
AS

DECLARE @bit INT ,
       @field INT
,
       @maxfield INT
,
       @char INT
,
       @fieldname VARCHAR(128
) ,
       @TableName VARCHAR(128
) ,
       @PKCols VARCHAR(1000
) ,
       @sql VARCHAR(2000
),
       @UpdateDate VARCHAR(21
) ,
       @UserName VARCHAR(128
) ,
       @Type CHAR(1
) ,
       @PKSelect VARCHAR(1000
)
      

–You will need to change @TableName to match the table to be audited
SELECT @TableName = ‘trigtest’

— date and user
SELECT         @UserName = SYSTEM_USER
,
       @UpdateDate = CONVERT(VARCHAR(8), GETDATE(), 112
)
               + ‘ ‘ + CONVERT(VARCHAR(12), GETDATE(), 114
)

— Action
IF EXISTS (SELECT * FROM inserted
)
       IF EXISTS (SELECT * FROM deleted
)
               SELECT @Type =
‘U’
      
ELSE
               SELECT @Type =
‘I’
ELSE
       SELECT @Type =
‘D’

— get list of columns
SELECT * INTO #ins FROM
inserted
SELECT * INTO #del FROM
deleted

— Get primary key columns for full outer join
SELECT @PKCols = COALESCE(@PKCols + ‘ and’, ‘ on’)
               + ‘ i.’ + c.COLUMN_NAME + ‘ = d.’ +
c.COLUMN_NAME
       FROM    INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk
,

              INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
       WHERE   pk.TABLE_NAME =
@TableName
       AND     CONSTRAINT_TYPE =
‘PRIMARY KEY’
       AND     c.TABLE_NAME =
pk.TABLE_NAME
       AND     c.CONSTRAINT_NAME =
pk.CONSTRAINT_NAME

— Get primary key select for insert
SELECT @PKSelect = COALESCE(@PKSelect+’+’,”
)
       + ”'<‘ +
COLUMN_NAME
       +
‘=”+convert(varchar(100),
coalesce(i.’ + COLUMN_NAME +’,d.’ + COLUMN_NAME +
‘))+”>”’
       FROM    INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk
,
              
INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
       WHERE   pk.TABLE_NAME =
@TableName
       AND     CONSTRAINT_TYPE =
‘PRIMARY KEY’
       AND     c.TABLE_NAME =
pk.TABLE_NAME
       AND     c.CONSTRAINT_NAME =
pk.CONSTRAINT_NAME

IF @PKCols IS NULL
BEGIN
       RAISERROR(‘no PK on table %s’, 16, -1, @TableName
)
      
RETURN
END

SELECT         @field = 0,
       @maxfield = MAX(ORDINAL_POSITION
)
       FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME =
@TableName
WHILE @field <
@maxfield
BEGIN
       SELECT @field = MIN(ORDINAL_POSITION
)
               FROM
INFORMATION_SCHEMA.COLUMNS
               WHERE TABLE_NAME =
@TableName
               AND ORDINAL_POSITION >
@field
       SELECT @bit = (@field – 1 )% 8 +
1
       SELECT @bit = POWER(2,@bit – 1
)
       SELECT @char = ((@field – 1) / 8) +
1
       IF SUBSTRING(COLUMNS_UPDATED(),@char, 1) & @bit > 0
OR @Type IN (‘I’,’D’
)
      
BEGIN
               SELECT @fieldname =
COLUMN_NAME
                       FROM
INFORMATION_SCHEMA.COLUMNS
                       WHERE TABLE_NAME =
@TableName
                       AND ORDINAL_POSITION =
@field
               SELECT @sql =

insert Audit (    Type,
               TableName,
               PK,
               FieldName,
               OldValue,
               NewValue,
               UpdateDate,
               UserName)
select ”’ + @Type +
”’,”’
       + @TableName + ”’,’ + @PKSelect
       + ‘,”’ + @fieldname + ””
       + ‘,convert(varchar(1000),d.’ + @fieldname + ‘)’
       + ‘,convert(varchar(1000),i.’ + @fieldname + ‘)’
       + ‘,”’ + @UpdateDate + ””
       + ‘,”’ + @UserName + ””
       + ‘ from #ins i full outer join #del d’
       + @PKCols
       + ‘ where i.’ + @fieldname + ‘ <> d.’ + @fieldname
       + ‘ or (i.’ + @fieldname + ‘ is null and  d.’
+ @fieldname
+ ‘ is not null)’
       + ‘ or (i.’ + @fieldname + ‘ is not null and  d.’
+ @fieldname
+ ‘ is null)’
               EXEC (@sql)
       END
END

GO

——————————————————-

–now we can test the trigger out

INSERT trigtest SELECT 1,1,’hi’, ‘bye’

INSERT trigtest SELECT 2,2,’hi’, ‘bye’

INSERT trigtest SELECT 3,3,’hi’, ‘bye’

SELECT * FROM Audit

SELECT * FROM trigtest

UPDATE trigtest SET s = ‘hibye’ WHERE i <> 1

UPDATE trigtest SET s = ‘bye’ WHERE i = 1

UPDATE trigtest SET s = ‘bye’ WHERE i = 1

UPDATE trigtest SET t = ‘hi’ WHERE i = 1

SELECT * FROM Audit

SELECT * FROM trigtest

DELETE trigtest

SELECT * FROM Audit

SELECT * FROM trigtest

GO

DROP TABLE Audit

GO

DROP TABLE trigtest

GO

Why Pop, you’re a life saver!

That’s quite all right, missy. Just don’t forget that there is, of course, an overhead to this technique. Also, bear in mind the potential problems of using the @@Identity variable when you have triggers on tables with IDENTITY columns.