Problem
In a perfect world, only the DBA would have sa privileges, F5 would only ever be hit on purpose, every change would go through rigorous source control procedures, and we would have full backups of all databases every minute. Of course, in reality, we deal with much different circumstances, and we can find ourselves (or overhear someone else) saying, "Oops... how do I fix that?" One of the more common scenarios I've seen involves someone editing a stored procedure multiple times between backups or within some kind of cycle, and then wishing they had version (current - 1) available. It's not in the backup yet, so can't be restored; and the user, of course, has closed his or her window without saving.Solution
There are a lot of solutions to this issue, of course. They include tightening down server access, adopting a reliable source control system, and implementing a rigorous and well-documented deployment process. These things do not happen overnight, so in the meantime, DDL Triggers can provide a short-term fix that is both easy to implement and simple to manage. The approach is to take a snapshot of the current objects in the database, and then log all DDL changes from that point forward. With a well-managed log, you could easily see the state of an object at any point in time (assuming, of course, the objects are not encrypted).So where do we start? First, I like to keep housekeeping items (monitoring, administration etc.) in their own database. This allows me to query things centrally and also to control growth separately. For this task, let's use a database called AuditDB:
CREATE DATABASE AuditDB; GO
In addition to the data specific to the actions taken on stored procedures, we can also think of several other pieces of information we would want to store about each event. For example:
- database name
- schema / object name
- login information
- host name / IP address (useful with SQL auth)
USE AuditDB; GO CREATE TABLE dbo.DDLEvents ( EventDate DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, EventType NVARCHAR(64), EventDDL NVARCHAR(MAX), EventXML XML, DatabaseName NVARCHAR(255), SchemaName NVARCHAR(255), ObjectName NVARCHAR(255), HostName VARCHAR(64), IPAddress VARCHAR(32), ProgramName NVARCHAR(255), LoginName NVARCHAR(255) );
Now that the table exists, we can easily grab a snapshot of our existing stored procedure definitions, leaving out some of the irrelevant auditing data, as follows (replacing 'my name' with whatever you want to display for the initial rows):
USE YourDatabase; GO INSERT AuditDB.dbo.DDLEvents ( EventType, EventDDL, DatabaseName, SchemaName, ObjectName, LoginName ) SELECT 'CREATE_PROCEDURE', OBJECT_DEFINITION([object_id]), DB_NAME(), OBJECT_SCHEMA_NAME([object_id]), OBJECT_NAME([object_id]), 'my name' FROM sys.procedures;
USE YourDatabase; GO CREATE TRIGGER DDLTrigger_Sample ON DATABASE FOR CREATE_PROCEDURE, ALTER_PROCEDURE, DROP_PROCEDURE AS BEGIN SET NOCOUNT ON; DECLARE @EventData XML = EVENTDATA(); DECLARE @ip VARCHAR(32) = ( SELECT client_net_address FROM sys.dm_exec_connections WHERE session_id = @@SPID ); INSERT AuditDB.dbo.DDLEvents ( EventType, EventDDL, EventXML, DatabaseName, SchemaName, ObjectName, HostName, IPAddress, ProgramName, LoginName ) SELECT @EventData.value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)'), @EventData.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'NVARCHAR(MAX)'), @EventData, DB_NAME(), @EventData.value('(/EVENT_INSTANCE/SchemaName)[1]', 'NVARCHAR(255)'), @EventData.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(255)'), HOST_NAME(), @ip, PROGRAM_NAME(), SUSER_SNAME(); END GO
USE YourDatabase; GO DISABLE TRIGGER [DDLTrigger_Sample] ON DATABASE;
USE YourDatabase; GO ENABLE TRIGGER [DDLTrigger_Sample] ON DATABASE;
-- testing audit
SELECT * FROM AuditDB.dbo.DDLEvents WHERE EventType = 'ALTER_PROCEDURE';
;WITH [Events] AS ( SELECT EventDate, DatabaseName, SchemaName, ObjectName, EventDDL, rnLatest = ROW_NUMBER() OVER ( PARTITION BY DatabaseName, SchemaName, ObjectName ORDER BY EventDate DESC ), rnEarliest = ROW_NUMBER() OVER ( PARTITION BY DatabaseName, SchemaName, ObjectName ORDER BY EventDate ) FROM AuditDB.dbo.DDLEvents ) SELECT Original.DatabaseName, Original.SchemaName, Original.ObjectName, OriginalCode = Original.EventDDL, NewestCode = COALESCE(Newest.EventDDL, ''), LastModified = COALESCE(Newest.EventDate, Original.EventDate) FROM [Events] AS Original LEFT OUTER JOIN [Events] AS Newest ON Original.DatabaseName = Newest.DatabaseName AND Original.SchemaName = Newest.SchemaName AND Original.ObjectName = Newest.ObjectName AND Newest.rnEarliest = Original.rnLatest AND Newest.rnLatest = Original.rnEarliest AND Newest.rnEarliest > 1 WHERE Original.rnEarliest = 1;
What the above does not capture are other peripheral changes that can happen to a stored procedure. For example, what about moving a procedure to a different schema? You can change the DDL Trigger above in the following way to capture the ALTER_SCHEMA event:
USE YourDatabase; GO ALTER TRIGGER DDLTrigger_Sample ON DATABASE FOR CREATE_PROCEDURE, ALTER_PROCEDURE, DROP_PROCEDURE, ALTER_SCHEMA AS BEGIN -- ...
USE YourDatabase; GO ALTER TRIGGER DDLTrigger_Sample ON DATABASE FOR CREATE_PROCEDURE, ALTER_PROCEDURE, DROP_PROCEDURE, ALTER_SCHEMA, RENAME AS BEGIN -- ...
Some other considerations:
- You may want to put in a cleanup routine that gets rid of "noise" more than <n> days old (but still keeping the set of objects that are important to you).
- To validate that your auditing process is capturing all changes, you can check modify_date in sys.procedures. Of course this only works for procedures that haven't been dropped - only if they have been created, modified, renamed, or transfered to a different schema.
- Security might be an issue, depending on what you want to accomplish. Allow me to elaborate:
DDL Triggers will not be transparent to users - first of all, they can see them in the Object Explorer tree, so it won't be a big secret that they are there and operational. They also appear in execution plans; if users have this option enabled when they create or modify objects in Management Studio, they will see the query plan for statements such as INSERT AuditDB.dbo.DDLEvents.
If you want to hide the definition of the DDL Trigger, you can encrypt it as follows:
USE YourDatabase; GO ALTER TRIGGER DDLTrigger_Sample ON DATABASE WITH ENCRYPTION FOR -- ...
TITLE: Microsoft SQL Server Management Studio ------------------------------ Script failed for DatabaseDdlTrigger 'DDLTrigger_Sample'. Property TextHeader is not available for DatabaseDdlTrigger '[DDLTrigger_Sample]'. This property may not exist for this object, or may not be retrievable due to insufficient access rights. The text is encrypted. ...
So, assuming SQL Server 2008 or above, you could use an audit specification to capture DDL events as a backup (or instead). But, given all of this, if you have to go to these lengths to prevent people from circumventing your auditing capabilites, then maybe your problems are larger and not all that technical. I suspect that in most reasonable environments, you'll sleep fine at night simply locking down the audit table.
I hope this provides a decent starting point to protect your environment(s) with DDL Triggers. However, given the manual aspect of this approach as well as its limitations, it will likely be best to consider this a short-term plan, and look into more robust source control and recovery techniques in the longer term.
Source : www.mssqltips.com
No comments:
Post a Comment