Pop does SQL Server auditing
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!
1 |
— 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.
Load comments