|
| 1 | +-- The MIT License (MIT) |
| 2 | +-- |
| 3 | +-- Copyright (c) 2015 Philip Doxakis |
| 4 | +-- |
| 5 | +-- Permission is hereby granted, free of charge, to any person obtaining a copy |
| 6 | +-- of this software and associated documentation files (the "Software"), to deal |
| 7 | +-- in the Software without restriction, including without limitation the rights |
| 8 | +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 9 | +-- copies of the Software, and to permit persons to whom the Software is |
| 10 | +-- furnished to do so, subject to the following conditions: |
| 11 | +-- |
| 12 | +-- The above copyright notice and this permission notice shall be included in all |
| 13 | +-- copies or substantial portions of the Software. |
| 14 | +-- |
| 15 | +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 16 | +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 17 | +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 18 | +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 19 | +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 20 | +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 21 | +-- SOFTWARE. |
| 22 | + |
| 23 | +/* |
| 24 | +Script Name: |
| 25 | + Audit trail script for SQL Server Database |
| 26 | +Author: |
| 27 | + Philip Doxakis |
| 28 | +Customize: |
| 29 | +You can exclude table in the script. |
| 30 | +Follow comments in the script: "Specify table to exclude here:" |
| 31 | +Description: |
| 32 | +Install a complete audit trail on selected database. |
| 33 | +Optional: Add "USE [DatabaseName];" at the top of the script. |
| 34 | +Step: |
| 35 | +- Remove all triggers starting with "tr_audit_" |
| 36 | +- Add "Audit" table if not found on the database |
| 37 | +- Add triggers for almost all tables (this can be customized). |
| 38 | +Limitations: |
| 39 | +- Audit table is "Audit" |
| 40 | +- Audit trigger start with "tr_audit_" |
| 41 | +- Do not support datatype: image |
| 42 | +(Based on https://msdn.microsoft.com/en-us/library/ms187928.aspx) |
| 43 | +*/ |
| 44 | + |
| 45 | +DECLARE @DatabaseName VARCHAR(255); |
| 46 | +SELECT @DatabaseName = TABLE_CATALOG FROM information_schema.columns |
| 47 | + |
| 48 | +PRINT 'Starting script...' |
| 49 | +PRINT '' |
| 50 | +PRINT 'Environnement:' |
| 51 | +PRINT ' Server:' |
| 52 | +PRINT ' ' + CAST(SERVERPROPERTY('ServerName') AS VARCHAR(255)) |
| 53 | +PRINT ' Edition:' |
| 54 | +PRINT ' ' + CAST(SERVERPROPERTY('Edition') AS VARCHAR(255)) |
| 55 | +PRINT ' Database name:' |
| 56 | +PRINT ' ' + @DatabaseName |
| 57 | +PRINT '' |
| 58 | + |
| 59 | +PRINT 'Starting: Removing all triggers starting with tr_audit_' |
| 60 | +DECLARE @TriggerName VARCHAR(255); |
| 61 | +DECLARE MY_CURSOR_FOR_TRIGGER CURSOR |
| 62 | +LOCAL STATIC READ_ONLY FORWARD_ONLY |
| 63 | +FOR |
| 64 | +-- Get list of trigger in current database |
| 65 | +SELECT |
| 66 | + sysobjects.name AS trigger_name |
| 67 | +FROM sysobjects |
| 68 | +WHERE |
| 69 | +sysobjects.type = 'TR' AND |
| 70 | +sysobjects.name LIKE 'tr_audit_%' |
| 71 | +OPEN MY_CURSOR_FOR_TRIGGER |
| 72 | +FETCH NEXT FROM MY_CURSOR_FOR_TRIGGER INTO @TriggerName |
| 73 | +WHILE @@FETCH_STATUS = 0 |
| 74 | +BEGIN |
| 75 | + DECLARE @sql VARCHAR(250) |
| 76 | + |
| 77 | + -- Remove current trigger |
| 78 | + SET @sql = 'DROP TRIGGER ' + @TriggerName |
| 79 | + PRINT 'Removing trigger: ' + @TriggerName |
| 80 | + EXEC (@sql) |
| 81 | + |
| 82 | + FETCH NEXT FROM MY_CURSOR_FOR_TRIGGER INTO @TriggerName |
| 83 | +END |
| 84 | +CLOSE MY_CURSOR_FOR_TRIGGER |
| 85 | +DEALLOCATE MY_CURSOR_FOR_TRIGGER |
| 86 | +PRINT 'Finished: Removing all triggers starting with tr_audit_' |
| 87 | +PRINT '' |
| 88 | + |
| 89 | +PRINT 'Starting: Make sure Audit table exists' |
| 90 | +IF NOT EXISTS (SELECT * FROM sysobjects WHERE id = OBJECT_ID(N'[dbo].[Audit]')) |
| 91 | +BEGIN |
| 92 | +PRINT 'Adding Audit table in the database' |
| 93 | +CREATE TABLE Audit |
| 94 | + (Type CHAR(1), |
| 95 | + TableName VARCHAR(128), |
| 96 | + PK VARCHAR(1000), |
| 97 | + FieldName VARCHAR(128), |
| 98 | + OldValue VARCHAR(MAX), |
| 99 | + NewValue VARCHAR(MAX), |
| 100 | + UpdateDate datetime) |
| 101 | +END |
| 102 | +GO |
| 103 | +PRINT 'Finished: Make sure Audit table exists' |
| 104 | +PRINT '' |
| 105 | + |
| 106 | +PRINT 'Starting: Create audit trigger for all tables' |
| 107 | +DECLARE @TableName VARCHAR(255); |
| 108 | +DECLARE MY_CURSOR_FOR_TABLE CURSOR |
| 109 | +LOCAL STATIC READ_ONLY FORWARD_ONLY |
| 110 | +FOR |
| 111 | +SELECT DISTINCT TABLE_NAME |
| 112 | +FROM information_schema.columns |
| 113 | +WHERE OBJECTPROPERTY(OBJECT_ID(TABLE_CATALOG + '.' + TABLE_SCHEMA + '.' + TABLE_NAME), 'IsView') = 0 |
| 114 | +OPEN MY_CURSOR_FOR_TABLE |
| 115 | +FETCH NEXT FROM MY_CURSOR_FOR_TABLE INTO @TableName |
| 116 | +WHILE @@FETCH_STATUS = 0 |
| 117 | +BEGIN |
| 118 | + If @TableName != 'Audit' -- Table used by audit trigger |
| 119 | +AND LEFT(@TableName, 7) <> 'aspnet_' |
| 120 | +AND LEFT(@TableName, 9) <> 'webpages_' |
| 121 | +-- Specify table to exclude here: |
| 122 | +-- Copy paste line bellow to specify table to exclude more table: |
| 123 | +--AND @TableName != 'VersionInfo' -- Table used by FluentMigrator |
| 124 | + BEGIN |
| 125 | + PRINT 'Adding trigger for table: ' + @TableName |
| 126 | + DECLARE @sql VARCHAR(8000) |
| 127 | + SET @sql = 'CREATE TRIGGER tr_audit_' + @TableName + ' |
| 128 | +ON [' + @TableName + '] FOR INSERT, UPDATE, DELETE |
| 129 | +AS |
| 130 | +DECLARE @field INT, |
| 131 | + @maxfield INT, |
| 132 | + @char INT, |
| 133 | + @mask INT, |
| 134 | + @fieldname VARCHAR(128), |
| 135 | + @TableName VARCHAR(128), |
| 136 | + @PKCols VARCHAR(1000), |
| 137 | + @sql VARCHAR(8000), |
| 138 | + @UpdateDate VARCHAR(21), |
| 139 | + @UserName VARCHAR(128), |
| 140 | + @Type CHAR(1), |
| 141 | + @PKSelect VARCHAR(1000) |
| 142 | +
|
| 143 | +SET NOCOUNT ON |
| 144 | +
|
| 145 | +--You will need to change @TableName to match the table to be audited |
| 146 | +SELECT @TableName = ''' + @TableName + ''' |
| 147 | +
|
| 148 | +-- date and user |
| 149 | +SELECT @UserName = SYSTEM_USER, |
| 150 | + @UpdateDate = CONVERT(VARCHAR(8), GETDATE(), 112) |
| 151 | + + '' '' + CONVERT(VARCHAR(12), GETDATE(), 114) |
| 152 | +
|
| 153 | +-- Action |
| 154 | +IF EXISTS (SELECT * FROM inserted) |
| 155 | + IF EXISTS (SELECT * FROM deleted) |
| 156 | + SELECT @Type = ''U'' |
| 157 | + ELSE |
| 158 | + SELECT @Type = ''I'' |
| 159 | +ELSE |
| 160 | + SELECT @Type = ''D'' |
| 161 | +
|
| 162 | +-- get list of columns |
| 163 | +SELECT * INTO #ins FROM inserted |
| 164 | +SELECT * INTO #del FROM deleted |
| 165 | +
|
| 166 | +-- Get primary key columns for full outer join |
| 167 | +SELECT @PKCols = COALESCE(@PKCols + '' and'', '' on'') |
| 168 | + + '' i.'' + c.COLUMN_NAME + '' = d.'' + c.COLUMN_NAME |
| 169 | + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk , |
| 170 | +
|
| 171 | + INFORMATION_SCHEMA.KEY_COLUMN_USAGE c |
| 172 | + WHERE pk.TABLE_NAME = @TableName |
| 173 | + AND CONSTRAINT_TYPE = ''PRIMARY KEY'' |
| 174 | + AND c.TABLE_NAME = pk.TABLE_NAME |
| 175 | + AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME |
| 176 | +
|
| 177 | +-- Get primary key select for insert |
| 178 | +SELECT @PKSelect = COALESCE(@PKSelect+''+'','''') |
| 179 | + + ''''''<'' + COLUMN_NAME |
| 180 | + + ''=''''+convert(varchar(100), |
| 181 | +coalesce(i.'' + COLUMN_NAME +'',d.'' + COLUMN_NAME + ''))+''''>'''''' |
| 182 | + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk , |
| 183 | + INFORMATION_SCHEMA.KEY_COLUMN_USAGE c |
| 184 | + WHERE pk.TABLE_NAME = @TableName |
| 185 | + AND CONSTRAINT_TYPE = ''PRIMARY KEY'' |
| 186 | + AND c.TABLE_NAME = pk.TABLE_NAME |
| 187 | + AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME |
| 188 | +
|
| 189 | +IF @PKCols IS NULL |
| 190 | +BEGIN |
| 191 | + RAISERROR(''no PK on table %s'', 16, -1, @TableName) |
| 192 | + RETURN |
| 193 | +END |
| 194 | +
|
| 195 | +SELECT @field = 0, |
| 196 | + @maxfield = MAX(ORDINAL_POSITION) |
| 197 | +FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName |
| 198 | +WHILE @field < @maxfield |
| 199 | +BEGIN |
| 200 | +SELECT @field = MIN(ORDINAL_POSITION) |
| 201 | + FROM INFORMATION_SCHEMA.COLUMNS |
| 202 | + WHERE TABLE_NAME = @TableName |
| 203 | + AND ORDINAL_POSITION > @field |
| 204 | + IF @field IS NOT NULL |
| 205 | + BEGIN |
| 206 | + SELECT |
| 207 | +@field = MIN(ORDINAL_POSITION), |
| 208 | +@char = (column_id - 1) / 8 + 1, |
| 209 | +@mask = POWER(2, (column_id - 1) % 8), |
| 210 | +@fieldname = name |
| 211 | +FROM SYS.COLUMNS SC |
| 212 | +INNER JOIN INFORMATION_SCHEMA.COLUMNS ISC |
| 213 | +ON SC.name = ISC.COLUMN_NAME |
| 214 | +WHERE object_id = OBJECT_ID(@TableName) |
| 215 | +AND TABLE_NAME = @TableName |
| 216 | +AND ORDINAL_POSITION = @field |
| 217 | +GROUP BY column_id, name |
| 218 | + |
| 219 | + IF (SUBSTRING(COLUMNS_UPDATED(), @char, 1) & @mask) > 0 |
| 220 | + OR @Type IN (''I'',''D'') |
| 221 | + BEGIN |
| 222 | + SELECT @sql = '' |
| 223 | +INSERT Audit ( Type, |
| 224 | + TableName, |
| 225 | + PK, |
| 226 | + FieldName, |
| 227 | + OldValue, |
| 228 | + NewValue, |
| 229 | + UpdateDate) |
| 230 | +SELECT '''''' + @Type + '''''','''''' |
| 231 | + + @TableName + '''''','' + @PKSelect |
| 232 | + + '','''''' + @fieldname + '''''''' |
| 233 | + + '',convert(varchar(MAX),d.'' + @fieldname + '')'' |
| 234 | + + '',convert(varchar(MAX),i.'' + @fieldname + '')'' |
| 235 | + + '','''''' + @UpdateDate + '''''''' |
| 236 | + + '' from #ins i full outer join #del d'' |
| 237 | + + @PKCols |
| 238 | + + '' where i.'' + @fieldname + '' <> d.'' + @fieldname |
| 239 | + + '' or (i.'' + @fieldname + '' is null and d.'' |
| 240 | ++ @fieldname |
| 241 | ++ '' is not null)'' |
| 242 | + + '' or (i.'' + @fieldname + '' is not null and d.'' |
| 243 | ++ @fieldname |
| 244 | ++ '' is null)'' |
| 245 | + EXEC (@sql) |
| 246 | +END |
| 247 | +END |
| 248 | +END' |
| 249 | + |
| 250 | + EXEC(@sql) |
| 251 | + END |
| 252 | + ELSE |
| 253 | + BEGIN |
| 254 | +PRINT 'Trigger not added for table: ' + @TableName |
| 255 | +END |
| 256 | + FETCH NEXT FROM MY_CURSOR_FOR_TABLE INTO @TableName |
| 257 | +END |
| 258 | +CLOSE MY_CURSOR_FOR_TABLE |
| 259 | +DEALLOCATE MY_CURSOR_FOR_TABLE |
| 260 | +PRINT 'Finished: Create audit trigger for all tables' |
| 261 | + |
| 262 | +PRINT '' |
| 263 | +PRINT 'Finished!' |
0 commit comments