稽核就是稽核,沒什麼好解釋
Ledger 則是 2022 引入的新東西,他使用區塊鏈技術讓資料具備防竄改特性,讓稽核流程更簡化。
所以我會重點說明 Ledger 是什麼
如果真正參與法規稽核的人都知道,如果無法證明資料的真實性,這個過程會有多麻煩費工。因為就算截圖了,還是有可能造假,例如說先查好別的查詢,然後再假裝放上稽核員要的指令然後截圖,或是真的懂底層原理的 DBA 其實都有能力去竄改要被稽核的資料。
通常都是海外的公司才有這麼嚴格要求,我不是說台灣公司(包括銀行)稽核不夠嚴謹,意思是目前我看到過的公司的 DB 稽核項目,其實都是可以竄改的。而 Ledger 是一個讓竄改難度大幅上升的技術。
2022 引入區塊鏈技術解決這個問題,這項技術不只可以提供資料的變更歷史,也可以證明你的資料沒有被竄改;即使資料曾經被具有管理權限的帳號竄改,也可以避免不可否認性問題。
首先 2022 引入兩種類型的 Ledger 資料表,用來提供這項功能 :
這個 Ledger table 不允許對其執行任何 UPDATE 或 DELETE 。
所以她很適合用來存信用帳戶交易資訊等等的資料。
接下來會直接建立一個 Ledger 表
CREATE TABLE dbo.AccountTransactions (
AccountTransactionID INT NOT NULL IDENTITY PRIMARY KEY,
CustomerID INT NOT NULL,
TxDescription NVARCHAR(256) NOT NULL,
TxAmount DECIMAL(6,2) NOT NULL,
NewBalance DECIMAL(6,2) NOT NULL,
account_tx_transaction_id BIGINT GENERATED ALWAYS AS
transaction_id START HIDDEN NOT NULL,
account_tx_sequence_number BIGINT GENERATED ALWAYS AS
sequence_number START HIDDEN NOT NULL,
) WITH (LEDGER = ON (APPEND_ONLY = ON));
INSERT INTO dbo.AccountTransactions
(CustomerID, TxDescription, TxAmount, NewBalance)
VALUES
(5, 'Card Payment', 5.00, 995.00),
(5, 'Card Payment', 250.00, 745.00),
(5, 'Repayment', 255.00, 1000.00),
(5, 'Card Payment', 20.00, 980.00);
要建立 Ledger table 的話,GENERATE ALWAYS AS 是必要的,如果你沒有特別寫,SQL Server 會自動建立。
這是用來保存插入資料列交易的交易 ID 跟交易中的序號。
接著如果去嘗試更新這張表就會出現錯誤。
如果現在想檢查是誰把資料列插入 table 的話就可以利用那個系統自動填入的欄位。
SELECT
dlt.commit_time
, dlt.principal_name
, atx.*
, atx.account_tx_sequence_number
FROM dbo.AccountTransactions atx
INNER JOIN sys.database_ledger_transactions dlt
ON atx.account_tx_transaction_id = dlt.transaction_id

一旦 table 被設定成僅附加,那就無法關閉這個設定,最高權限的人也無法。
如果有一個需求是這個資料庫我確定所有的表都是 Ledger 表,那就可以建立一個 Ledger 資料庫,之後所有在這個資料庫底下建立的表,都是 Ledger。
CREATE DATABASE TestLedger WITH LEDGER = ON;
GO
USE TestLedger
GO
-- 這裡不+ with 他預設就會是 Ledger 表 並且是可修改
CREATE TABLE dbo.GoodsIn (
ID INT NOT NULL IDENTITY PRIMARY KEY,
StockID INT NOT NULL,
QtyOrdered INT NOT NULL,
QtyReceived INT NOT NULL,
ReceivedBy NVARCHAR(128) NOT NULL,
ReceivedDate DATETIME2 NOT NULL,
Damaged BIT NOT NULL,
);
--也可以+ with 去設定它變成僅附加模式
CREATE TABLE dbo.AccountTransactions (
AccountTransactionID INT NOT NULL IDENTITY PRIMARY KEY,
CustomerID INT NOT NULL,
TxDescription NVARCHAR(256) NOT NULL,
TxAmount DECIMAL(6,2) NOT NULL,
NewBalance DECIMAL(6,2) NOT NULL
) WITH (LEDGER = ON (APPEND_ONLY = ON));
這個指令可以去查看目前 Ledger 的狀態
SELECT
name
, is_ledger_on
FROM sys.databases
WHERE name LIKE 'TEST%'
SELECT
'TEST' AS DatabaseName
, name
, ledger_type_desc
, ledger_view_id
, is_dropped_ledger_table
FROM TEST.sys.tables
UNION ALL
SELECT
'TESTLedger'
, name
, ledger_type_desc
, ledger_view_id
, is_dropped_ledger_table
FROM TESTLedger.sys.tables
Ledger 使用區塊鏈技術,隨著時間逐步擷取資料庫的狀態。
接下來會有很多名詞,所以要先額外解釋一下。
這邊說的區塊鏈技術並不是去中心化節點那種,是用了區塊鏈常見的防竄改技術。
包括 hash chain / Merkle tree / block digest
主要流程是這樣
假設我有四筆資料
DATA 1
DATA 2
DATA 3
DATA 4
先分別算 hash
H1 = hash (DATA 1)
H2 = hash (DATA 2)
H3 = hash (DATA 3)
H4 = hash (DATA 4)
然後兩兩合併再 hash
H12 = hash (H1+H2)
H34 = hash (H3+H4)
最後再合併
Roort = hash (H12 + H34)
Root
|
----------------
| |
H12 H34
| |
-------- --------
| | | |
H1 H2 H3 H4
| | | |
Data1 Data2 Data3 Data4
所以在這個結構之下,如果 H3 被竄改了,那 H34、ROOT 都會跟這變,日後要驗證的時候就會跟一開始的對不上。
而分段取 HASH 不一次全部取的原因是,如果真的篡改,又用一次全部加再一起取HASH 會變得不知道到底是哪一筆被竄改。
假設現在有一筆交易
BEGIN TRAN;
INSERT INTO dbo.AccountTransactions
(AccountTransactionID, CustomerID, TxAmount, NewBalance)
VALUES
(1, 5, 250.00, 745.00),
(2, 5, 255.00, 1000.00);
COMMIT;
交易資訊大約是這樣
Transaction ID = 1001
使用者 = WU
時間 = 2026-06-12 10:00:00
第一步 SQL Server 會先序列化,然後去計算資料 metadata + 資料本身的 hash
第二步 用兩個 RowHash 開始按照上面的說法兩個兩個一組,去組成 Merkle Tree
第三步 把這次交易完整資訊 + Merkle Tree 的 root hash 湊再一起再算一次 hash
第四步 會有一個 block 把第三步所得到的所有 hash 一一存起來
第五步 如果
這時候 block 就會關閉,然後會把 block 的資訊 + 裡面所有的hash 都框起來,再去算一次 hash,而這個就是 digest。
最後 digest 就會長這樣
{
"database_name": “AccountTransactions",
"block_id": 0,
"hash": "26bcb075f8773d080961d65ade24ecda04a10bbc06abe290c123c5aee4177818",
"last_transaction_commit_time": "2026-06-12T10:00:00",
"digest_time": "2026-06-12T10:05:00"
}
然後 block 會被寫入 sys.database_ledger_blocks 這張表保存。
而digest 就類似於一個指紋,用來驗證 block 是不是沒有被修改過。
所以分離儲存 digest 至關重要,因為這些都只是驗證,不是加密,所以想改還是可以改,如果 digest 放在一個開放環境,那驗證也沒有意義,通常會配合 AZURE Blob 使用。
只有跟 Azure 整合的時候,才能設定自動產生 digest。
-- 產生 digest
EXEC sys.sp_generate_database_ledger_digest
-- 檢視 blocks
SELECT *
FROM sys.database_ledger_blocks
這是產生的 digest
{
"database_name": "TestLedger",
"block_id": 0,
"hash": "0x2B2DA01307232989EEA5FACAEE1E58B5947AB24F43B3D5E5C262F0EB770C87CE",
"last_transaction_commit_time": "2026-06-12T14:13:10.7100000",
"digest_time": "2026-06-12T07:27:33.2732296"
}
然後驗證,驗證之前必須把隔離等級切換到Snapshot,這是必要條件。
隔離等級之後會說。
ALTER DATABASE TestLedger SET ALLOW_SNAPSHOT_ISOLATION ON
GO
EXECUTE sp_verify_database_ledger N'
[
{
"database_name": "TestLedger",
"block_id": 0,
"hash": "0x2B2DA01307232989EEA5FACAEE1E58B5947AB24F43B3D5E5C262F0EB770C87CE",
"last_transaction_commit_time": "2026-06-12T14:13:10.7100000",
"digest_time": "2026-06-12T07:27:33.2732296"
},
{
"database_name": "TestLedger",
"block_id": 0,
"hash": "0x2B2DA01307232989EEA5FACAEE1E58B5947AB24F43B3D5E5C262F0EB770C87CE",
"last_transaction_commit_time": "2026-06-12T14:13:10.7100000",
"digest_time": "2026-06-12T07:27:33.2732296"
}
]';
一般的稽核紀錄的資料跟 ledger 紀錄的幾乎沒有差別,ledger 只是有經過 hash,所以不容易作假而已。
可是 block、digest 你放在同一個地方的話,那一樣是可以作假。
SQL Server Audit 讓 DBA 能夠針對 執行個體層級 與 資料庫層級 的活動進行細緻稽核,並將這些活動儲存到:
稽核資料儲存的位置稱為 target。
每個執行個體中可以有多個 server audit。
如果需要在繁忙環境中稽核大量事件,這會很有用,因為可以使用檔案作為 target,並將每個 target file 放在不同的磁碟區上,以分散 I/O。
從安全性角度來看,選擇正確的 target 很重要。
如果選擇 Windows Application log 作為 target,那麼任何已驗證到該伺服器的 Windows 使用者都可以存取它。
Security log 比 Application log 安全許多,但對 SQL Server Audit 來說設定也更複雜。
執行 SQL Server 服務的 service account 需要在該伺服器的本機安全性原則中,被指派 Generate Security Audits 使用者權限。
此外,也需要在 audit policy 中啟用應用程式產生的稽核成功與失敗事件。
target 的另一個考量是大小。
如果決定使用 Application log 或 Security log,開始使用它們做稽核之前,應該考慮並可能增加這些 log 的大小。
此外你也要去跟團隊或是你自己思考一下 log 滿了的時候怎麼循環。
如果要稽核很多動作,建立多個 server audit specification 或 database audit specification 會很有幫助,因為可以將它們分類,使管理更容易,同時仍然關聯到同一個 server audit。
建立 Server Audit 有這些選項可以用
| 選項 | 說明 |
|---|---|
FILEPATH |
只有在選擇 file target 時適用。指定 audit log 產生的位置。 |
MAXSIZE |
只有在選擇 file target 時適用。指定單一 audit file 可以成長到的最大大小。可指定的最小值是 2 MB。 |
MAX_ROLLOVER_FILES |
只有在選擇 file target 時適用。當 audit file 滿了以後,可以循環使用該檔案,或產生新的檔案。MAX_ROLLOVER_FILES 控制在開始循環前,可以產生多少個新檔案。預設值是 UNLIMITED,但你可以指定數字限制檔案數量。如果設為 0,代表永遠只有一個檔案,每次滿了就覆蓋自己。任何大於 0 的值代表允許的 rollover file 數量。例如指定 5,總共最多會有 6 個檔案。 |
MAX_FILES |
只有在選擇 file target 時適用。這是 MAX_ROLLOVER_FILES 的替代選項。MAX_FILES 指定 audit file 可以產生的最大數量;當達到這個數量後,log 不會循環。相反地,audit 會失敗,而導致 audit action 發生的事件會依照 ON_FAILURE 設定處理。 |
RESERVE_DISK_SPACE |
只有在選擇 file target 時適用。預先在磁碟區配置等同於 MAXSIZE 設定值的空間,而不是讓 audit log 隨需求慢慢成長。 |
QUEUE_DELAY |
指定 audit event 是同步寫入還是非同步寫入。如果設為 0,event 會同步寫入 log。否則,指定 event 在被強制寫入前,可以等待的毫秒數。預設值是 1000,也就是 1 秒,也是最小值。 |
ON_FAILURE |
指定 audit action 無法寫入 log 時要怎麼處理。可接受值是 CONTINUE、SHUTDOWN、FAIL_OPERATION。CONTINUE 代表操作繼續,但可能造成未被稽核的活動。FAIL_OPERATION 代表可稽核事件失敗,但其他操作繼續。SHUTDOWN 代表如果可稽核事件無法寫入 log,就強制停止 instance。 |
AUDIT_GUID |
因為 server audit specification 與 database audit specification 是透過 GUID 連到 server audit,所以有些情況下 audit specification 可能會變成 orphaned。例如把 database attach 到另一個 instance,或使用 database mirroring 等技術時。這個選項允許你為 server audit 指定特定 GUID,而不是讓 SQL Server 自動產生新的 GUID。 |
可以在 server audit 上建立篩選條件。
當 audit specification 擷取整個物件類別的活動,但實際上只對其中一部分有興趣時,這會很有用。
例如,可能設定一個 server audit specification,用來記錄任何 server role 的成員變更;但是你實際上只關心 sysadmin server role 的成員是否被修改。
在這種情境下,可以針對 sysadmin role 做篩選。


USE Master
GO
-- 建立一個 Server Audit 物件,名稱叫 Audit-ProSQLAdmin
CREATE SERVER AUDIT [Audit-ProSQLAdmin]
-- 指定 audit target 為檔案
TO FILE
(
-- 指定 audit file 產生的資料夾路徑
FILEPATH = N'E:\audit'
-- 指定單一 audit file 最大大小為 512 MB
-- 檔案達到 512 MB 後,會依照 rollover 設定產生新檔或循環使用
,MAXSIZE = 512 MB
-- 指定最多允許多少個 rollover file
-- 2147483647 幾乎等同允許非常大量的檔案
-- 也就是檔案滿了就繼續產生新檔,通常不會因為檔案數量限制而停止
,MAX_ROLLOVER_FILES = 2147483647
-- OFF 代表不要一開始就預先保留 512 MB 空間
-- audit file 會隨寫入量慢慢成長
-- 如果設為 ON,SQL Server 會依照 MAXSIZE 預先配置空間
,RESERVE_DISK_SPACE = OFF
)
-- 設定 audit 寫入行為與失敗處理方式
WITH
(
-- audit event 最多可以延遲 1000 毫秒,也就是 1 秒,再寫入 audit target
-- 這是非同步寫入,效能影響較小
-- 如果設為 0,代表同步寫入,audit event 必須立即寫入才繼續
QUEUE_DELAY = 1000
-- 如果 audit log 寫入失敗,SQL Server 操作仍然繼續執行
,ON_FAILURE = CONTINUE
)
-- 只保留 object_name = 'sysadmin' 的 audit event
WHERE object_name = 'sysadmin';
CREATE SERVER AUDIT SPECIFICATION [ServerAuditSpecification-ProSQLAdmin]
FOR SERVER AUDIT [Audit-ProSQLAdmin]
ADD (SERVER_ROLE_MEMBER_CHANGE_GROUP)

剛剛建立好之後,預設是不啟用的,所以要馬GUI 介面去啟用要馬用以下指令
ALTER SERVER AUDIT [Audit-ProSQLAdmin]
WITH (STATE = ON);
ALTER SERVER AUDIT SPECIFICATION [ServerAuditSpecification-ProSQLAdmin]
WITH (STATE = ON);

然後把前面建立的 WU 這個 Login 加入 sysadmin server role 故意去觸發稽核。
ALTER SERVER ROLE sysadmin
ADD MEMBER WU


前面示範過僅附加,現在說明可更新 Ledger
主要差異就是,可更新的 Ledger 可以用 INSERT、UPDATE
還有一個是 history table,這東西僅附加的 Ledger table 也會有,只是因為僅附加的 Ledger 不會有什麼歷史資訊,所以那時候沒有提,微軟保留這個是為了一致性,就大家都有不用那麼複雜這樣。
history table 會記錄歷史資訊,這個只要建立 Ledger 就一定會有,可以去自訂名稱,也可以自訂 schema。
建立 Ledger 的時候,也會自動建立一個 view,這個 view 是用來簡單查看資料表歷史用的,一樣可以自訂名稱跟 schema。
從安全性的角度來說,把這兩個東西放在不同的 schema 很有幫助。
跟僅附加的 Ledger 一樣,可更新的 Ledger 也有一些必要欄位如下。
| 欄位 | 資料型別 | 說明 |
|---|---|---|
ledger_start_transaction_id |
BIGINT |
插入該資料列的交易 ID |
ledger_end_transaction_id |
BIGINT |
刪除該資料列的交易 ID |
ledger_start_sequence_number |
BIGINT |
在交易中建立該資料列版本的操作序號 |
ledger_end_sequence_number |
BIGINT |
在交易中刪除該資料列版本的操作序號 |
CREATE TABLE dbo.GoodsIn (
ID INT NOT NULL IDENTITY PRIMARY KEY,
StockID INT NOT NULL,
QtyOrdered INT NOT NULL,
QtyReceived INT NOT NULL,
ReceivedBy NVARCHAR(128) NOT NULL,
ReceivedDate DATETIME2 NOT NULL,
Damaged BIT NOT NULL
)
WITH (
SYSTEM_VERSIONING = ON (
--這是建立可更新式 Ledger 資料表的必要條件,且一定要設定。
HISTORY_TABLE = dbo.GoodsIn_Ledger_History
--自訂 history table名稱,這邊我沒有用不同的 schema
),
LEDGER = ON (
LEDGER_VIEW = dbo.GoodsIn_Ledger_View
--自訂 view table名稱,這邊我沒有用不同的 schema
)
);
GO
USE master;
GO
-- 建立 Andrew 這個 SQL Login
-- 這是 Instance 層級的登入帳號
CREATE LOGIN [Andrew]
WITH PASSWORD = N'Pa$$w0rd',
CHECK_POLICY = OFF;
GO
-- 建立 GoodsInApplication 這個 SQL Login
-- 書上用它來模擬應用程式連線帳號
CREATE LOGIN [GoodsInApplication]
WITH PASSWORD = N'Pa$$w0rd',
CHECK_POLICY = OFF;
GO
USE TestLedger
CREATE USER [Andrew]
FOR LOGIN [Andrew]
WITH DEFAULT_SCHEMA = dbo;
GO
CREATE USER [GoodsInApplication]
FOR LOGIN [GoodsInApplication]
WITH DEFAULT_SCHEMA = dbo;
GO
GRANT INSERT ON dbo.GoodsIn TO [Andrew];
GRANT UPDATE ON dbo.GoodsIn TO [Andrew];
GRANT DELETE ON dbo.GoodsIn TO [Andrew];
GRANT SELECT ON dbo.GoodsIn TO [Andrew];
GO
GRANT SELECT ON dbo.GoodsIn TO [GoodsInApplication];
GRANT INSERT ON dbo.GoodsIn TO [GoodsInApplication];
GRANT UPDATE ON dbo.GoodsIn TO [GoodsInApplication];
GRANT DELETE ON dbo.GoodsIn TO [GoodsInApplication];
GO
EXECUTE AS LOGIN = 'GoodsInApplication' -- 模擬由應用程式執行的活動
-- 插入初始資料
INSERT INTO dbo.GoodsIn
(StockID, QtyOrdered, QtyReceived, ReceivedBy, ReceivedDate, Damaged)
VALUES
(17, 25, 25, 'Pete', '20220807 10:59', 0),
(6, 20, 19, 'Brian', '20220810 15:01', 0),
(17, 20, 20, 'Steve', '20220810 16:56', 1),
(36, 10, 10, 'Steve', '20220815 18:11', 0),
(36, 10, 10, 'Steve', '20220815 18:12', 1),
(1, 85, 85, 'Andrew', '20220820 10:27', 0);
-- 由應用程式刪除重複資料列
DELETE FROM dbo.GoodsIn
WHERE ID = 4;
REVERT -- 停止模擬 GoodsInApplication
EXECUTE AS LOGIN = 'Andrew' -- 模擬由使用者執行的活動
-- Andrew 從應用程式外部更新資料列(警示)
UPDATE dbo.GoodsIn
SET QtyReceived = 70
WHERE ID = 6;
REVERT -- 停止模擬 Andrew
EXECUTE AS LOGIN = 'GoodsInApplication' -- 模擬由應用程式執行的活動
-- 由應用程式更新資料列
UPDATE dbo.GoodsIn
SET Damaged = 1
WHERE ID = 1;
INSERT INTO dbo.GoodsIn
(StockID, QtyOrdered, QtyReceived, ReceivedBy, ReceivedDate, Damaged)
VALUES
(17, 25, 25, 'Pete', '20220822 08:59', 0);
REVERT -- 停止模擬 GoodsInApplication
SELECT
t.name AS [可更新式Ledger資料表]
, h.name AS [歷史資料表]
, v.name AS [Ledger檢視表]
FROM sys.tables AS t
INNER JOIN sys.tables AS h
ON h.object_id = t.history_table_id
JOIN sys.views v
ON v.object_id = t.ledger_view_id
WHERE t.name = 'GoodsIn';
DECLARE @SQL NVARCHAR(MAX)
SET @SQL = (
SELECT
'SELECT * FROM ' + t.name --Updateable Ledger Table
+ ' SELECT * FROM ' + h.name --History Table
+ ' SELECT * FROM ' + v.name + ' ORDER BY ledger_transaction_id, ledger_sequence_number' --Ledger View
FROM sys.tables AS t
INNER JOIN sys.tables AS h
ON h.object_id = t.history_table_id
JOIN sys.views v
ON v.object_id = t.ledger_view_id
WHERE t.name = 'GoodsIn'
)
EXEC(@SQL)
SELECT
lv.ID AS [資料列ID]
, lv.StockID AS [庫存ID]
, lv.QtyOrdered AS [訂購數量]
, lv.QtyReceived AS [收貨數量]
, lv.ReceivedBy AS [收貨人]
, lv.ReceivedDate AS [收貨日期]
, lv.Damaged AS [是否損壞]
, lv.ledger_operation_type_desc AS [Ledger操作類型說明]
, dlt.commit_time AS [交易提交時間]
, dlt.principal_name AS [執行修改的主體名稱]
FROM dbo.GoodsIn_Ledger_View lv
INNER JOIN sys.database_ledger_transactions dlt
ON dlt.transaction_id = lv.ledger_transaction_id;
這邊可以注意看到 principal_name,他存的是 SQL Server 登入 instance 的帳號,儘管我們前面是用 EXECUTE AS 去模擬,但她還是會存是誰登入的 instance。
刪除整個 Ledger,不會真的把它刪掉,他只會重新命名這個 tabel,然後藏起來,這是為了避免高權限的人,把 table 複製出去,然後刪掉 table ,修改完複製出去的之後再建回來。
DROP TABLE dbo.GoodsIn;
GO
SELECT
t.name AS [可更新式Ledger資料表]
, h.name AS [歷史資料表]
, v.name AS [Ledger檢視表]
FROM sys.tables AS t
INNER JOIN sys.tables AS h
ON h.object_id = t.history_table_id
JOIN sys.views v
ON v.object_id = t.ledger_view_id;
這個根伺服器稽核類似,只是從 instance 改成指向 database 而已。
為了示範我會把 Trump 這個 Login 對一到這個 database 中的一個 user,並授予他對 senstitivedata table 的 select 權限。
然後還會建立一個新的伺服器稽核,這個伺服器稽核會作為資料庫稽核規格所附加的稽核。
USE Master
GO
-- 建立 DatabaseAudit資料庫
CREATE DATABASE DatabaseAudit
GO
USE DatabaseAudit
GO
CREATE TABLE dbo.SensitiveData (
ID INT PRIMARY KEY IDENTITY NOT NULL ,
SensitiveText NVARCHAR(256) NOT NULL
);
GO
-- 建立 Server Audit
USE master
GO
CREATE SERVER AUDIT [Audit-DatabaseAudit]
TO FILE
(
FILEPATH = N'E:\Audit'
, MAXSIZE = 512 MB
, MAX_ROLLOVER_FILES = 2147483647
, RESERVE_DISK_SPACE = OFF
)
WITH
(
QUEUE_DELAY = 1000
, ON_FAILURE = CONTINUE
);
GO
USE DatabaseAudit
GO
-- 從 wu Login 建立 database user
CREATE USER wu FOR LOGIN wu
WITH DEFAULT_SCHEMA = dbo;
GO
GRANT SELECT ON dbo.SensitiveData TO wu ;
GO
USE DatabaseAudit
GO
CREATE DATABASE AUDIT SPECIFICATION
[DatabaseAuditSpecification-DatabaseAudit-SensitiveData]
FOR SERVER AUDIT [Audit-DatabaseAudit]
ADD (INSERT ON OBJECT::dbo.SensitiveData BY public),
ADD (SELECT ON OBJECT::dbo.SensitiveData BY wu);
GO
ALTER DATABASE AUDIT SPECIFICATION
[DatabaseAuditSpecification-DatabaseAudit-SensitiveData]
WITH (STATE = ON);
GO
USE Master
GO
ALTER SERVER AUDIT [Audit-DatabaseAudit]
WITH (STATE = ON);
GO
USE DatabaseAudit
GO
GRANT SELECT ,INSERT, UPDATE ON dbo.SensitiveData TO wu;
GO
INSERT INTO dbo.SensitiveData (SensitiveText)
VALUES ('testing');
GO
UPDATE dbo.SensitiveData
SET SensitiveText = 'Boo'
WHERE ID = 2;
GO
EXECUTE AS USER = 'wu';
GO
INSERT dbo.SensitiveData (SensitiveText)
VALUES ('testing again');
GO
UPDATE dbo.SensitiveData
SET SensitiveText = 'Boo'
WHERE ID = 1;
GO
REVERT;
GO

到目前為止,我們已經實作了稽核功能,但這裡仍然存在一個安全漏洞。
如果某個具備管理 server audit 權限的管理員有惡意意圖,那麼他有可能在執行惡意動作之前,先修改 audit specification,然後在執行完惡意動作之後,再把 audit 重新設定回原本狀態,以此消除可歸責性。
不過,Server Audit 可以讓你防範這種威脅,因為它允許你 稽核 audit 本身。
如果我們將 AUDIT_CHANGE_GROUP 加入稽核規格,那麼對該規格的任何改變都會被捕捉
USE DatabaseAudit
GO
ALTER DATABASE AUDIT SPECIFICATION
[DatabaseAuditSpecification-DatabaseAudit-SensitiveData]
WITH (STATE = OFF);
GO
ALTER DATABASE AUDIT SPECIFICATION
[DatabaseAuditSpecification-DatabaseAudit-SensitiveData]
ADD (AUDIT_CHANGE_GROUP);
GO
ALTER DATABASE AUDIT SPECIFICATION
[DatabaseAuditSpecification-DatabaseAudit-SensitiveData]
WITH (STATE = ON);
GO