Blog Post

Audit Configuration Changes

,

Do you know the last time a Server Property was changed on your instances of SQL Server?

Are you wondering when the setting for max degree of parallelism was changed?

Do you know who changed the setting?

In some environments there are a lot of hands trying to help mix the pot.  Sometimes more hands can make light work.  This is not always the case though.  More hands in the mix can be a troublesome thing.  Especially when things start changing and the finger pointing starts but nobody really knows for sure who made the change or when the change was made.

I know, that is why there is this concept called change management.  If you make a change to a setting, it should be documented, approved and communicated.  Unfortunately the process does not always dictate the work flow and it may be circumvented.  This is why auditing is a tool that is in place and should be in place – like it or not.

Auditing can be a very good tool.  Like many things – too much of a good thing is not a good thing.  Too much auditing can be more of a hindrance than help.  You don’t want to cause interference by auditing too many things.  You also don’t want too much data that the facts get blurred.  I hope that this script strikes more of a balance with just the right amount of data being captured to be of use.

The basic principle to auditing server configs is to find what values changes, when they were changed and by whom.  There are ancillary details that can be helpful in tracking the source of the change such as the hostname of the source computer making the change.  These are all things that we should capture.  But if a setting hasn’t changed – then we need not necessarily report that the setting was unchanged (it should go without saying).

So for this, I created a couple of tables and then a script that I can add to a job to run on a regular basis.  I can put the script in a stored procedure should I desire.  I’ll leave that to you as an exercise to perform.

USE AdminDB;
GO
SET NOCOUNT ON;
 
DECLARE @ConfigLastUpdateDateDATETIME
,@PreviousPollDateDATETIME
,@MaxPollDateDATETIME
,@PATHNVARCHAR(260);
 
SELECT @PATH = REVERSE(SUBSTRING(REVERSE([PATH]), 
CHARINDEX('\', REVERSE([path])), 260)) + N'LOG.trc'
FROM sys.traces 
WHERE is_default = 1;
 
IF OBJECT_ID('tempdb..#DBCCConfig') IS NOT NULL DROP TABLE #DBCCConfig;
CREATE TABLE #DBCCConfig(
[ParentObject] VARCHAR (100),
[Object]       VARCHAR (100),
[Field]        VARCHAR (100),
[Value]        VARCHAR (100)); 
 
INSERT INTO #DBCCConfig (ParentObject, Object, Field, Value)
EXECUTE ('DBCC CONFIG WITH TABLERESULTS');
 
WITH cte AS
(
SELECT cfgupddate = MAX(CASE WHEN t1.Field = 'cfgupddate' THEN t1.Value ELSE NULL END),
        cfgupdtime = MAX(CASE WHEN t1.Field = 'cfgupdtime' THEN t1.Value ELSE NULL END)
FROM #DBCCConfig t1
WHERE   Field IN ('cfgupddate', 'cfgupdtime')
)
SELECT  @ConfigLastUpdateDate = CONVERT(DATETIME,t3.configure_upd_dt)
FROM    cte t1
        CROSS APPLY (SELECT cfgupddate = DATEADD(DAY, CONVERT(INT, t1.cfgupddate), '1900-01-01')) t2
        CROSS APPLY (SELECT configure_upd_dt = DATEADD(ms, CONVERT(INT, t1.cfgupdtime)*3.3, t2.cfgupddate)) t3;
 
IF NOT EXISTS (SELECT Name 
FROM sys.objects 
WHERE name = 'SysConfigAudit')
CREATE TABLE SysConfigAudit (
configuration_idint
,nameNVARCHAR(256)
,valuesql_variant
,minimumsql_variant
,maximumsql_variant
,value_in_usesql_variant
,descriptionNVARCHAR(MAX)
,is_dynamicbit
,is_advancedBIT
,PollDateDATE
,LastConfigUpdtDateDATETIME)
 
IF NOT EXISTS (SELECT Name 
FROM sys.objects 
WHERE name = 'SysConfigChangeLog')
CREATE TABLE SysConfigChangeLog (
configuration_idint
,nameNVARCHAR(256)
,CurrValueSQL_VARIANT
,PrevValueSQL_VARIANT
,descriptionNVARCHAR(MAX)
,PollDateDATE
,LastConfigUpdtDateDATETIME
,PrevConfigUpdtDateDATETIME
,ChangeDateDATETIME
,ChangeByNVARCHAR(256)
,HostNameNVARCHAR(256)
,ChangeSPIDSQL_VARIANT
,ChangedataNVARCHAR(2000)
,ApplicationName NVARCHAR(256)
,Severity INT
,ERROR SQL_VARIANT
,ChangeBySessionLoginNVARCHAR(256))
IF NOT EXISTS (SELECT Name 
FROM sys.key_constraints 
WHERE name ='PK_SysConfigChangeLog' 
AND OBJECT_NAME(parent_object_id) = 'SysConfigChangeLog')
BEGIN
ALTER TABLE dbo.SysConfigChangeLog ADD CONSTRAINT
PK_SysConfigChangeLog PRIMARY KEY CLUSTERED 
(
configuration_id,
ChangeDate DESC
) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) 
ON Admin_Data
 
END
 
SET @MaxPollDate = ISNULL((SELECT TOP 1 PollDate
FROM dbo.SysConfigAudit 
ORDER BY PollDate DESC),'1/1/1900')
 
/* Write the configurations out to audit table 
*/IF CONVERT(DATE,GETDATE()) <> @MaxPollDate
BEGIN
INSERT INTO SysConfigAudit
( configuration_id ,
  name ,
  value ,
  minimum ,
  maximum ,
  value_in_use ,
  description ,
  is_dynamic ,
  is_advanced,
  PollDate,
  LastConfigUpdtDate
)
SELECT configuration_id,name
,value
,minimum,maximum
,value_in_use
,description
,is_dynamic,is_advanced
,GETDATE(),@ConfigLastUpdateDate
FROM master.sys.configurations;
END
 
/* Recast MaxPollDate */SET @MaxPollDate = ISNULL((SELECT TOP 1 PollDate
FROM dbo.SysConfigAudit 
ORDER BY PollDate DESC),'1/1/1900')
 
SET @PreviousPollDate = ISNULL((SELECT TOP 1 PollDate 
FROM dbo.SysConfigAudit 
WHERE Polldate <> @MaxPollDate 
ORDER BY PollDate DESC),'1/1/1900');
 
/*A configuration has changed and a reboot has occurred 
causing the updtdate to be written to the config block of the page 10
The configuration may not be written to the page, but we will write it to
the table anyway and then compare to the default trace file in all cases.
If there are any values changed, then write those to the change log with
the login of the person who changed the value
*/ 
BEGIN
 
WITH presel AS (
SELECT df.LoginName,df.TextData,df.StartTime,df.HostName,df.Severity,df.DatabaseName,df.SPID,df.ERROR
,df.SessionLoginName,df.ApplicationName
,SUBSTRING(df.TextData
,CHARINDEX('''',df.TextData)+1
,CHARINDEX('''',df.TextData
,CHARINDEX('''',df.TextData)+1)-CHARINDEX('''',df.TextData)-1)
AS ConfigName
,ROW_NUMBER() OVER (PARTITION BY SUBSTRING(df.TextData
,CHARINDEX('''',df.TextData)+1
,CHARINDEX('''',df.TextData
,CHARINDEX('''',df.TextData)+1)-CHARINDEX('''',df.TextData)-1) 
ORDER BY df.StartTime DESC) AS RowNum
FROM ::fn_trace_gettable( @path, DEFAULT )  df
WHERE 1=1
AND df.TextData LIKE '%Configuration option%'
AND df.ApplicationName NOT IN ('SQLAgent - Initial Boot Probe','SQLAgent - Enabling/disabling Agent XPs')
AND df.Severity IS NOT NULL
)
 
INSERT INTO SysConfigChangeLog (configuration_id,name,CurrValue,PrevValue,description,PollDate
,LastConfigUpdtDate,PrevConfigUpdtDate,ChangeDate,ChangeBy
,HostName,ChangeSPID,Changedata,ApplicationName,Severity
,ERROR,ChangeBySessionLogin)
SELECT CUR.configuration_id,cur.NAME,cur.VALUE AS CurrValue,Prev.VALUE AS PrevValue,cur.description
,cur.polldate, cur.LastConfigUpdtDate,prev.LastConfigUpdtDate AS PrevConfigUpdtDate
,df.StartTime AS ChangeDate,df.LoginName AS ChangeBy,df.HostName,df.SPID AS ChangeSPID
,df.TextData AS ChangeData,df.ApplicationName,df.Severity,df.ERROR
,df.SessionLoginName AS ChangeBySessionLogin
FROM AdminDB.dbo.SysConfigAudit CUR
INNER JOIN AdminDB.dbo.SysConfigAudit Prev
ON CUR.configuration_id = Prev.configuration_id
AND CUR.PollDate = @MaxPollDate
AND Prev.PollDate = @PreviousPollDate
CROSS APPLY presel  df
WHERE df.RowNum = 1
AND df.ConfigName = CUR.NAME
AND CUR.VALUE <> Prev.VALUE
AND NOT EXISTS (SELECT configuration_id
FROM SysConfigChangeLog
WHERE Name = Cur.NAME
AND CurrValue = CUR.VALUE
AND PrevValue = Prev.VALUE
AND ChangeDate = df.StartTime
AND ChangeBy = df.LoginName)
;
END
 
SELECT *
FROM dbo.SysConfigChangeLog
ORDER BY ChangeDate DESC;

Here I am trapping the config settings on a daily basis (as the script is written for now). I then cross reference the current settings against the previous settings.  Then I check the default trace to see if any of those settings exist in the trace file.

The default trace captures the data related to configuration changes.  On busy systems, it is still possible for these settings to roll out of the trace files.  For those systems, we may need to make some alterations to the script (such as running more frequently and changing the comparisons to account for smaller time intervals than a day break).

To ensure proper comparison between the audit tables and the trace file, note the substring function employed.  I can capture the configuration name and then join to the audit tables on configuration name.

This has proven useful to me so far in tracking who did what to which setting and when they did it.

I hope you will find good use for it as well.

Rate

You rated this post out of 5. Change rating

Share

Share

Rate

You rated this post out of 5. Change rating