SQL Server Logins And Users Audit (資料庫帳號權限清查)
2025-06-13
筆記如何定期進行資料庫帳號與登入權限清查的 Script。
伺服器 Logins Check
SELECT
name,
loginname,
CASE
WHEN sysadmin = 1
OR securityadmin = 1
OR serveradmin = 1
OR setupadmin = 1
OR processadmin = 1
OR diskadmin = 1
OR dbcreator = 1
OR bulkadmin = 1
THEN 1
ELSE 0
END AS special_role,
createdate,
updatedate
FROM
sys.syslogins
WHERE
status = 9 AND
denylogin = 0;
資料庫 Users Check
IF OBJECT_ID('tempdb..#AllDatabaseUsersAndRolesWithLogin') IS NOT NULL
DROP TABLE #AllDatabaseUsersAndRolesWithLogin;
CREATE TABLE #AllDatabaseUsersAndRolesWithLogin (
db_name NVARCHAR(128),
user_name NVARCHAR(128),
login_name NVARCHAR(128), -- 新增登入名稱欄位
role_name NVARCHAR(128),
createdate DATETIME,
updatedate DATETIME,
islogin NVARCHAR(3)
);
DECLARE @sql NVARCHAR(MAX);
SET @sql = '
USE [?];
INSERT INTO #AllDatabaseUsersAndRolesWithLogin (db_name, user_name, login_name, role_name, createdate, updatedate, islogin)
SELECT
DB_NAME() AS db_name,
dp.name AS user_name,
sp.name AS login_name, -- 從 sys.server_principals 取得登入名稱
dr.name AS role_name,
dp.create_date AS createdate,
dp.modify_date AS updatedate,
CASE
WHEN dp.type IN (''S'', ''U'', ''G'') THEN ''Yes'' -- S: SQL User, U: Windows User, G: Windows Group
ELSE ''No''
END AS islogin
FROM
sys.database_principals AS dp
LEFT JOIN
sys.database_role_members AS drm ON dp.principal_id = drm.member_principal_id
LEFT JOIN
sys.database_principals AS dr ON drm.role_principal_id = dr.principal_id
LEFT JOIN
sys.server_principals AS sp ON dp.sid = sp.sid -- 根據 SID 聯結資料庫使用者與伺服器登入
WHERE
dp.type IN (''S'', ''U'', ''G'') -- S: SQL User, U: Windows User, G: Windows Group
AND dp.name NOT IN (''guest'', ''INFORMATION_SCHEMA'', ''sys''); -- 排除系統使用者
';
EXEC sp_MSforeachdb @command1 = @sql;
SELECT
db_name,
user_name,
ISNULL(login_name, 'N/A') AS login_name, -- 如果沒有對應的登入,顯示 'N/A'
ISNULL(role_name, 'No Role') AS role_name,
createdate,
updatedate,
islogin
FROM
#AllDatabaseUsersAndRolesWithLogin
ORDER BY
db_name,
user_name,
role_name;
DROP TABLE #AllDatabaseUsersAndRolesWithLogin;
排程檢查 Server Logins 異動
週期性執行,所有的登入帳號會儲存在 LoginsHistory
資料表中累積上去,差異的部份則透過 Script 比較後寫入 LoginAuditLog
資料表。
LoginsHistory 資料表
-- 如果舊的資料表存在,請先刪除
IF OBJECT_ID('dbo.LoginsHistory', 'U') IS NOT NULL
DROP TABLE dbo.LoginsHistory;
GO
-- 根據 sys.syslogins 的欄位重建資料表
CREATE TABLE dbo.LoginsHistory (
[CaptureDate] DATETIME NOT NULL,
-- sys.syslogins columns
[sid] VARBINARY(85) NOT NULL,
[status] SMALLINT NULL,
[createdate] DATETIME NOT NULL,
[updatedate] DATETIME NOT NULL,
[accdate] DATETIME NOT NULL,
[name] SYSNAME NOT NULL,
[dbname] SYSNAME NOT NULL,
[language] SYSNAME NULL,
[denylogin] INT NOT NULL,
[hasaccess] INT NOT NULL,
[isntname] INT NOT NULL,
[isntgroup] INT NOT NULL,
[isntuser] INT NOT NULL,
[sysadmin] INT NOT NULL,
[securityadmin] INT NOT NULL,
[serveradmin] INT NOT NULL,
[setupadmin] INT NOT NULL,
[processadmin] INT NOT NULL,
[diskadmin] INT NOT NULL,
[dbcreator] INT NOT NULL,
[bulkadmin] INT NOT NULL,
[loginname] SYSNAME NOT NULL,
-- Custom columns
[RowChecksum] INT NULL
);
GO
-- 建立索引以提升效能
CREATE CLUSTERED INDEX IX_LoginsHistory_CaptureDate_Name
ON dbo.LoginsHistory (CaptureDate, name);
GO
AuditLog 資料表
-- 建立新的 LoginAuditLog 資料表
IF OBJECT_ID('dbo.LoginAuditLog', 'U') IS NULL
BEGIN
CREATE TABLE dbo.LoginAuditLog (
[AuditLogID] INT IDENTITY(1,1) PRIMARY KEY,
[LogDate] DATETIME NOT NULL DEFAULT GETDATE(),
[EventType] NVARCHAR(20) NOT NULL, -- '新增', '刪除', '變更'
[LoginName] SYSNAME NOT NULL,
[ChangeDescription] NVARCHAR(MAX) NULL -- 記錄變更的詳細內容
);
END
GO
作業 Scripts
/*******************************************************************
每月 SQL Server Logins 變更稽核作業 (v2.1 - 增加權限變更檢查)
1. 比對差異
2. 將新增、刪除、變更事件寫入 LoginsAuditLog 資料表
3. 封存本次 Logins 狀態
*******************************************************************/
SET NOCOUNT ON;
-- 變數宣告
DECLARE @CurrentCaptureDate DATETIME = GETDATE();
DECLARE @PreviousCaptureDate DATETIME;
-- 1. 將當前 logins 狀態存入暫存表
SELECT
l.*,
CHECKSUM(
l.status, l.updatedate, l.dbname, l.language, l.denylogin, l.hasaccess,
l.sysadmin, l.securityadmin, l.serveradmin, l.setupadmin, l.processadmin,
l.diskadmin, l.dbcreator, l.bulkadmin
) AS RowChecksum
INTO #CurrentLogins
FROM sys.syslogins AS l;
-- 2. 取得上一次快照的日期
SELECT TOP 1 @PreviousCaptureDate = CaptureDate
FROM dbo.LoginsHistory
ORDER BY CaptureDate DESC;
-- 3. 如果有舊資料才進行比對並記錄
IF @PreviousCaptureDate IS NOT NULL
BEGIN
-- 找出上次的資料
SELECT *
INTO #PreviousLogins
FROM dbo.LoginsHistory
WHERE CaptureDate = @PreviousCaptureDate;
-- >> 事件: 新增的登入 (Logins Added)
INSERT INTO dbo.LoginAuditLog (LogDate, EventType, LoginName, ChangeDescription)
SELECT
@CurrentCaptureDate,
'新增',
cur.name,
'LoginName: ' + cur.loginname + ', Default DB: ' + cur.dbname + ', Has Access: ' + CAST(cur.hasaccess AS VARCHAR)
FROM #CurrentLogins cur
WHERE cur.name NOT IN (SELECT name FROM #PreviousLogins);
-- >> 事件: 已刪除的登入 (Logins Deleted)
INSERT INTO dbo.LoginAuditLog (LogDate, EventType, LoginName, ChangeDescription)
SELECT
@CurrentCaptureDate,
'刪除',
prev.name,
'LoginName: ' + prev.loginname + ', Default DB: ' + prev.dbname
FROM #PreviousLogins prev
WHERE prev.name NOT IN (SELECT name FROM #CurrentLogins);
-- >> 事件: 已變更的登入 (Logins Modified)
INSERT INTO dbo.LoginAuditLog (LogDate, EventType, LoginName, ChangeDescription)
SELECT
@CurrentCaptureDate,
--- *** 1. 新增檢查權限變更的邏輯,用以決定事件類型 *** ---
CASE
WHEN cur.sysadmin <> prev.sysadmin OR
cur.securityadmin <> prev.securityadmin OR
cur.serveradmin <> prev.serveradmin OR
cur.setupadmin <> prev.setupadmin OR
cur.processadmin <> prev.processadmin OR
cur.diskadmin <> prev.diskadmin OR
cur.dbcreator <> prev.dbcreator OR
cur.bulkadmin <> prev.bulkadmin OR
cur.denylogin <> prev.denylogin OR
cur.hasaccess <> prev.hasaccess
THEN '高權限變更'
ELSE '一般變更'
END AS EventType,
cur.name,
--- *** 2. 新增檢查權限變更的邏輯,用以加上警告訊息 *** ---
CONCAT(
CASE
WHEN cur.sysadmin <> prev.sysadmin OR
cur.securityadmin <> prev.securityadmin OR
cur.serveradmin <> prev.serveradmin OR
cur.setupadmin <> prev.setupadmin OR
cur.processadmin <> prev.processadmin OR
cur.diskadmin <> prev.diskadmin OR
cur.dbcreator <> prev.dbcreator OR
cur.bulkadmin <> prev.bulkadmin
THEN '***重要權限變更!*** '
ELSE ''
END,
CONCAT_WS(
'; ',
CASE WHEN cur.dbname <> prev.dbname THEN 'dbname: ' + prev.dbname + ' -> ' + cur.dbname ELSE NULL END,
CASE WHEN cur.language <> prev.language THEN 'language: ' + ISNULL(prev.language, 'NULL') + ' -> ' + ISNULL(cur.language, 'NULL') ELSE NULL END,
CASE WHEN cur.denylogin <> prev.denylogin THEN 'denylogin: ' + CAST(prev.denylogin AS VARCHAR) + ' -> ' + CAST(cur.denylogin AS VARCHAR) ELSE NULL END,
CASE WHEN cur.hasaccess <> prev.hasaccess THEN 'hasaccess: ' + CAST(prev.hasaccess AS VARCHAR) + ' -> ' + CAST(cur.hasaccess AS VARCHAR) ELSE NULL END,
CASE WHEN cur.sysadmin <> prev.sysadmin THEN 'sysadmin: ' + CAST(prev.sysadmin AS VARCHAR) + ' -> ' + CAST(cur.sysadmin AS VARCHAR) ELSE NULL END,
CASE WHEN cur.securityadmin <> prev.securityadmin THEN 'securityadmin: ' + CAST(prev.securityadmin AS VARCHAR) + ' -> ' + CAST(cur.securityadmin AS VARCHAR) ELSE NULL END,
CASE WHEN cur.serveradmin <> prev.serveradmin THEN 'serveradmin: ' + CAST(prev.serveradmin AS VARCHAR) + ' -> ' + CAST(cur.serveradmin AS VARCHAR) ELSE NULL END,
CASE WHEN cur.setupadmin <> prev.setupadmin THEN 'setupadmin: ' + CAST(prev.setupadmin AS VARCHAR) + ' -> ' + CAST(cur.setupadmin AS VARCHAR) ELSE NULL END,
CASE WHEN cur.processadmin <> prev.processadmin THEN 'processadmin: ' + CAST(prev.processadmin AS VARCHAR) + ' -> ' + CAST(cur.processadmin AS VARCHAR) ELSE NULL END,
CASE WHEN cur.diskadmin <> prev.diskadmin THEN 'diskadmin: ' + CAST(prev.diskadmin AS VARCHAR) + ' -> ' + CAST(cur.diskadmin AS VARCHAR) ELSE NULL END,
CASE WHEN cur.dbcreator <> prev.dbcreator THEN 'dbcreator: ' + CAST(prev.dbcreator AS VARCHAR) + ' -> ' + CAST(cur.dbcreator AS VARCHAR) ELSE NULL END,
CASE WHEN cur.bulkadmin <> prev.bulkadmin THEN 'bulkadmin: ' + CAST(prev.bulkadmin AS VARCHAR) + ' -> ' + CAST(cur.bulkadmin AS VARCHAR) ELSE NULL END,
'updatedate: ' + CONVERT(VARCHAR, prev.updatedate, 120) + ' -> ' + CONVERT(VARCHAR, cur.updatedate, 120)
)
) AS ChangeDescription
FROM #CurrentLogins cur
JOIN #PreviousLogins prev ON cur.name = prev.name
WHERE cur.RowChecksum <> prev.RowChecksum;
-- 清理暫存表
DROP TABLE #PreviousLogins;
END
-- 4. 將當前快照存入歷史資料表
INSERT INTO dbo.LoginsHistory
SELECT
@CurrentCaptureDate,
l.sid, l.status, l.createdate, l.updatedate, l.accdate, l.name, l.dbname, l.language,
l.denylogin, l.hasaccess, l.isntname, l.isntgroup, l.isntuser, l.sysadmin,
l.securityadmin, l.serveradmin, l.setupadmin, l.processadmin, l.diskadmin,
l.dbcreator, l.bulkadmin, l.loginname, l.RowChecksum
FROM #CurrentLogins AS l;
-- 清理暫存表
DROP TABLE #CurrentLogins;