Day 14、Day 15 我們分別盤點了:
User Account
Computer Object
到了 Day 16,要開始碰 Active Directory 裡另一個非常重要的東西:
Group
很多企業環境的權限並不是直接給某一個 User。
而是:
User
↓
AD Group
↓
File Server / VPN / Application / Share Folder
↓
Permission
例如:
tedliu
↓
IT-File-RW
↓
\FileServer\IT
↓
Read / Write
或者:
tedliu
↓
VPN-Users
↓
VPN Permission
所以今天真正要回答的問題是:
這個 User 到底加入哪些 Group?
以及:
這個 Group 裡到底有哪些人?
更進一步還要處理:
Group 裡面還有 Group
Disabled User 還留在重要 Group
已經沒人的 Group
權限群組成員越來越多
最後做出第一份:
AD Group Membership Audit Report
先從 Get-ADGroup 開始
先看看 Domain 裡有哪些 Group:
Get-ADGroup -Filter *
可能會看到:
DistinguishedName
GroupCategory
GroupScope
Name
ObjectClass
ObjectGUID
SamAccountName
SID
整理一下:
Get-ADGroup -Filter * | Select-Object
Name,
SamAccountName,
GroupCategory,
GroupScope
可能:
Name GroupCategory GroupScope
Domain Admins Security Global
VPN-Users Security Global
IT-File-RW Security DomainLocal
HR-Mail Distribution Universal
Security Group 跟 Distribution Group
Group 最基本可以先分成:
Security
Distribution
Security Group 通常可以拿來:
ACL
File Permission
Application Permission
VPN Permission
Windows Security
Distribution Group 則比較偏:
Mail Distribution
所以如果今天做的是:
權限稽核
通常會更關心:
GroupCategory -eq "Security"
例如:
Get-ADGroup -Filter 'GroupCategory -eq "Security"' | Select-Object
Name,
GroupScope
Group Scope 又是什麼?
常見:
Global
DomainLocal
Universal
今天不深入 AD Group Design,但可以先簡單理解。
Global
→ 常拿來裝 User / Computer
DomainLocal
→ 常拿來給資源權限
Universal
→ 多 Domain 環境常見
在一些企業 AD 設計裡,會看到類似:
User
↓
Global Group
↓
Domain Local Group
↓
Permission
這也是為什麼後面會遇到:
Group 裡還有 Group。
查一個 Group
例如:
Get-ADGroup -Identity "VPN-Users"
如果想多看一點:
Get-ADGroup -Identity "VPN-Users"
-Properties Description, ManagedBy, Members |
Select-Object `
Name,
GroupCategory,
GroupScope,
Description,
ManagedBy
這時就可以知道:
這是什麼 Group?
Security 還是 Distribution?
誰負責?
Description 有沒有寫?
Group Description 其實很重要
例如:
VPN-Users
名稱看起來很好懂。
但過幾年可能出現:
VPN-Users
VPN-Users-New
VPN-Users-2025
VPN-Users-Temp
VPN-Test
沒有人知道:
哪個才是真的 Production Group?
所以企業 AD 裡:
Description
ManagedBy
其實非常值得盤點。
後面我們也可以做:
沒有 Description 的 Security Group
沒有 ManagedBy 的權限 Group
當成 Data Quality Review。
查 Group 裡有哪些 Member
今天最重要的 Cmdlet 之一:
Get-ADGroupMember
例如:
Get-ADGroupMember `
-Identity "VPN-Users"
可能:
distinguishedName
name
objectClass
objectGUID
SamAccountName
SID
整理:
Get-ADGroupMember -Identity "VPN-Users" | Select-Object
Name,
SamAccountName,
ObjectClass
例如:
Name SamAccountName ObjectClass
Ted Liu tedliu user
User A usera user
VPN-IT-Users VPN-IT-Users group
PC001 PC001$ computer
這裡立刻出現一個很重要的現象:
Group 裡面不一定只有 User
還可能有:
User
Group
Computer
所以做 Group Audit 時,不要假設每一筆 Member 都一定是 User。
ObjectClass 很重要
例如:
$Members = Get-ADGroupMember `
-Identity "VPN-Users"
只看 User:
$Members |
Where-Object {
$_.ObjectClass -eq "user"
}
只看 nested Group:
$Members |
Where-Object {
$_.ObjectClass -eq "group"
}
這樣就可以知道:
VPN-Users 裡面是不是還包了其他 Group?
Group 裡還有 Group 怎麼辦?
假設:
VPN-Users
│
├── Ted
├── User A
└── VPN-IT
│
├── User B
└── User C
如果:
Get-ADGroupMember `
-Identity "VPN-Users"
直接成員可能只看到:
Ted
User A
VPN-IT
不一定直接列出:
User B
User C
因為他們是:
Nested Group 的成員。
使用 -Recursive
可以:
Get-ADGroupMember -Identity "VPN-Users"
-Recursive
這樣 PowerShell 會往 nested Group 繼續展開。
結果可能:
Ted
User A
User B
User C
這就更接近:
最後有哪些 Principal 透過群組巢狀關係出現在這個群組底下。
Direct Member 跟 Recursive Member 要分開
這裡非常重要。
假設:
VPN-Users
↓
VPN-IT
↓
Ted
Ted 並不是:
VPN-Users 的直接 Member
而是:
VPN-IT 的 Member
VPN-IT 又是:
VPN-Users 的 Member
所以報表最好不要把這兩件事情完全混在一起。
因為排權限時你可能需要知道:
Ted 到底是誰加進來的?
實際答案可能不是:
有人把 Ted 加入 VPN-Users
而是:
Ted 加入 VPN-IT
VPN-IT 被加入 VPN-Users
這是查權限來源時很重要的差別。
查一個 User 加入哪些 Group
反過來,現在如果我知道:
tedliu
想知道他加入哪些 Group,可以使用:
Get-ADPrincipalGroupMembership `
-Identity "tedliu"
整理:
Get-ADPrincipalGroupMembership -Identity "tedliu" | Select-Object
Name,
GroupCategory,
GroupScope |
Sort-Object Name
例如:
Name GroupCategory GroupScope
Domain Users Security Global
IT-Team Security Global
VPN-Users Security Global
這種查詢非常適合回答:
這個帳號目前跟哪些 AD Group 有關?
另外一種方式:MemberOf
也可以:
Get-ADUser -Identity "tedliu"
-Properties MemberOf |
Select-Object -ExpandProperty MemberOf
可能:
CN=IT-Team,OU=Groups,DC=contoso,DC=com
CN=VPN-Users,OU=Groups,DC=contoso,DC=com
不過要注意:
MemberOf
主要反映的是直接 group membership,而且 Primary Group 不會單純透過 memberOf 這個屬性呈現。
所以如果只是日常查詢,我通常會比較偏向使用:
Get-ADPrincipalGroupMembership
如果需要追查:
直接加入?
透過 nested group?
真正權限從哪裡來?
就要再往下分析群組結構。
找出一個 Group 有多少 Member
例如:
$Members = Get-ADGroupMember `
-Identity "VPN-Users"
然後:
@($Members).Count
可能:
253
代表:
VPN-Users 有 253 個直接 Member。
Recursive:
$RecursiveMembers =
Get-ADGroupMember -Identity "VPN-Users"
-Recursive
再:
@($RecursiveMembers).Count
例如:
411
這就可以看到:
Direct Members = 253
Recursive Members = 411
兩個數字差很多,就代表:
這個 Group 內有不少 Nested Membership。
建立 Group Summary
例如:
$GroupName = "VPN-Users"
$Group = Get-ADGroup -Identity $GroupName
-Properties Description, ManagedBy
$DirectMembers =
Get-ADGroupMember `
-Identity $GroupName
$RecursiveMembers =
Get-ADGroupMember -Identity $GroupName
-Recursive
$Summary = [PSCustomObject]@{
GroupName =
$Group.Name
GroupCategory =
$Group.GroupCategory
GroupScope =
$Group.GroupScope
Description =
$Group.Description
ManagedBy =
$Group.ManagedBy
DirectMemberCount =
@($DirectMembers).Count
RecursiveMemberCount =
@($RecursiveMembers).Count
}
結果可能:
GroupName : VPN-Users
GroupCategory : Security
GroupScope : Global
Description : VPN access users
ManagedBy : ...
DirectMemberCount : 253
RecursiveMemberCount : 411
這已經可以當一份基本 Group Inventory。
找出空 Group
有些 AD 用久之後會留下:
Old-App-Users
Project-A-2022
Temp-Access
Test-Group
Group 還存在,但裡面早就沒有人。
我們可以:
$Groups = Get-ADGroup `
-Filter 'GroupCategory -eq "Security"'
接著:
foreach ($Group in $Groups) {
$Members = @(
Get-ADGroupMember `
-Identity $Group `
-ErrorAction SilentlyContinue
)
if ($Members.Count -eq 0) {
$Group.Name
}
}
這就可以找:
Empty Security Groups
但一樣:
Empty 不等於可以直接刪。
有些 Group 可能:
目前沒有 Member
但 ACL 還在使用它
例如某個 File Server Folder 已經把權限給:
Project-A-Users
只是目前 Group 暫時沒有成員。
如果直接 Delete Group,可能會留下:
SID 無法解析的 ACL
所以 Empty Group 仍然只是:
Review Candidate。
今天很重要的實戰:Disabled User 還在重要 Group
這種情況在企業 AD 很常值得盤點。
例如:
VPN-Users
├── user01 Enabled
├── user02 Enabled
└── olduser Disabled
olduser 已經 Disable。
但是:
VPN-Users Membership
還沒有清理。
雖然 Disabled Account 本身通常無法正常登入,但從帳號生命週期與權限治理角度,還是值得把 Membership 清乾淨。
找出 Group 裡的 Disabled User
例如:
$GroupName = "VPN-Users"
$Members = Get-ADGroupMember `
-Identity $GroupName
先只找 User:
$UserMembers = $Members |
Where-Object {
$_.ObjectClass -eq "user"
}
接著:
$DisabledUsers = foreach (
$Member in $UserMembers
) {
$User = Get-ADUser `
-Identity $Member.DistinguishedName `
-Properties Enabled
if (-not $User.Enabled) {
$User
}
}
顯示:
$DisabledUsers |
Select-Object `
Name,
SamAccountName,
Enabled
可能:
Name SamAccountName Enabled
Old User olduser False
User B userb False
現在就得到:
Disabled Users Still in VPN Group
對重要 Group 做 Audit 比掃全部 Group 更實際
如果 Domain 裡有:
3000 Groups
每天全部 Recursive 展開可能沒有必要。
我更喜歡先建立:
$CriticalGroups = @(
"Domain Admins"
"Server-Admins"
"VPN-Users"
"FileServer-Admins"
)
然後:
foreach ($GroupName in $CriticalGroups) {
# Audit
}
也就是:
先對高權限、高風險、重要資源群組做更詳細的稽核。
Domain Admins 要特別小心
例如:
Get-ADGroupMember `
-Identity "Domain Admins"
這種高權限 Group 通常應該:
成員數少
用途清楚
定期 Review
沒有不明帳號
沒有長期 Disabled User
但今天我們仍然只做:
Read
Audit
Report
不要在這種 Script 裡直接:
Remove-ADGroupMember
權限群組更應該保留:
Review
Approval
Change Record
Log
建立 Critical Group 設定
例如:
$CriticalGroups = @(
"Domain Admins"
"VPN-Users"
"Server-Admins"
)
接著:
$GroupAudit = @()
開始:
foreach ($GroupName in $CriticalGroups) {
$Group =
Get-ADGroup `
-Identity $GroupName `
-Properties `
Description,
ManagedBy
$Members = @(
Get-ADGroupMember `
-Identity $GroupName
)
$RecursiveMembers = @(
Get-ADGroupMember `
-Identity $GroupName `
-Recursive
)
$GroupAudit += [PSCustomObject]@{
GroupName =
$Group.Name
GroupScope =
$Group.GroupScope
Description =
$Group.Description
ManagedBy =
$Group.ManagedBy
DirectMembers =
$Members.Count
RecursiveMembers =
$RecursiveMembers.Count
CheckTime =
Get-Date
}
}
現在就有:
GroupName DirectMembers RecursiveMembers
Domain Admins 4 4
VPN-Users 253 411
Server-Admins 12 23
這是一份很好用的:
Critical Group Summary
產生 Member Detail
除了 Summary,我們當然還需要:
誰在裡面?
例如:
$GroupMemberReport = @()
然後:
foreach ($GroupName in $CriticalGroups) {
$Members = Get-ADGroupMember `
-Identity $GroupName
foreach ($Member in $Members) {
$GroupMemberReport +=
[PSCustomObject]@{
GroupName =
$GroupName
MemberName =
$Member.Name
SamAccountName =
$Member.SamAccountName
ObjectClass =
$Member.ObjectClass
MembershipType =
"Direct"
DistinguishedName =
$Member.DistinguishedName
}
}
}
結果:
Group Member Type Membership
VPN-Users tedliu user Direct
VPN-Users VPN-IT group Direct
Server-Admins Admin01 user Direct
這份資料對權限 Review 很有用。
Recursive Member 建議另外輸出
不要直接把:
Direct
Recursive
混成同一份且沒有標記。
例如:
$RecursiveReport = @()
然後:
foreach ($GroupName in $CriticalGroups) {
$Members = Get-ADGroupMember `
-Identity $GroupName `
-Recursive
foreach ($Member in $Members) {
$RecursiveReport +=
[PSCustomObject]@{
GroupName =
$GroupName
MemberName =
$Member.Name
SamAccountName =
$Member.SamAccountName
ObjectClass =
$Member.ObjectClass
MembershipType =
"Recursive"
DistinguishedName =
$Member.DistinguishedName
}
}
}
現在:
Direct Report
回答:
誰直接被加入這個 Group?
而:
Recursive Report
回答:
展開 nested membership 後,可以看到哪些成員?
兩份用途不一樣。
建立 Disabled Membership Report
現在把重要 Group 裡的 Disabled User 找出來。
$DisabledMemberReport = @()
接著:
foreach ($GroupName in $CriticalGroups) {
$Members = Get-ADGroupMember `
-Identity $GroupName `
-Recursive
$UserMembers = $Members |
Where-Object {
$_.ObjectClass -eq "user"
}
foreach ($Member in $UserMembers) {
try {
$User = Get-ADUser `
-Identity $Member.DistinguishedName `
-Properties `
Enabled,
LastLogonDate,
Department `
-ErrorAction Stop
if (-not $User.Enabled) {
$DisabledMemberReport +=
[PSCustomObject]@{
GroupName =
$GroupName
Name =
$User.Name
SamAccountName =
$User.SamAccountName
Enabled =
$User.Enabled
Department =
$User.Department
LastLogonDate =
$User.LastLogonDate
DistinguishedName =
$User.DistinguishedName
}
}
}
catch {
# Query error can be logged here
}
}
}
最後:
Group User Enabled
VPN-Users olduser False
Server-Admins admin02 False
這就是一份很實際的:
Disabled Account Membership Audit
但 Recursive Report 有一個問題
假設:
VPN-Users
↓
VPN-IT
↓
olduser
Recursive Audit 會告訴你:
olduser
出現在 VPN-Users 的遞迴成員裡
但它不會直接告訴你完整路徑:
olduser
→ VPN-IT
→ VPN-Users
如果真的要做:
權限來源追蹤
就需要進一步分析 Nested Group Path。
這個我們今天先不展開,不然 Day 16 會變成 Group Graph 演算法。
今天先把:
Direct
Recursive
分清楚就已經很重要。
Group Membership ≠ 最終資源權限
這一點也非常重要。
假設:
tedliu
是:
IT-File-RW
的 Member。
不代表我們只看 AD Group 就能百分之百知道:
Ted 對所有 File Server 有哪些權限。
因為真正的資源可能還有:
NTFS ACL
Share Permission
Application RBAC
GPO
Local Group
Cloud Permission
所以今天做的是:
AD Group Membership Audit
而不是完整:
Enterprise Effective Permission Audit
不過 AD Group 往往是非常重要的第一層。
找出沒有 Description 的 Security Group
既然今天已經查 Group,也可以順便做一個簡單的 Data Quality Check。
Get-ADGroup -Filter 'GroupCategory -eq "Security"'
-Properties Description |
Where-Object {
[string]::IsNullOrWhiteSpace(
$_.Description
)
} |
Select-Object `
Name,
GroupScope,
DistinguishedName
如果結果很多:
FileRW01
Test123
ABC-GRP
NewGroup
OldApp
未來管理上就會很痛苦。
找出沒有 ManagedBy 的 Security Group
同樣:
Get-ADGroup -Filter 'GroupCategory -eq "Security"'
-Properties ManagedBy |
Where-Object {
$null -eq $_.ManagedBy
} |
Select-Object `
Name,
GroupScope,
ManagedBy
這可以回答:
這個權限 Group 到底誰負責?
真正企業權限治理裡:
誰是 Group Owner?
誰可以核准加入?
這個 Group 是做什麼?
都非常重要。
今天完整 Script
下面把 Day 16 的主要內容整理成一支:
Critical AD Group Membership Audit
Import-Module ActiveDirectory
$CriticalGroups = @(
"Domain Admins"
"VPN-Users"
"Server-Admins"
)
$ReportFolder =
"C:\Temp\ADReports"
$Date =
Get-Date -Format "yyyyMMdd"
if (-not (Test-Path $ReportFolder)) {
New-Item `
-Path $ReportFolder `
-ItemType Directory |
Out-Null
}
$GroupSummary = @()
$DirectMemberReport = @()
$RecursiveMemberReport = @()
$DisabledMemberReport = @()
foreach ($GroupName in $CriticalGroups) {
Write-Host "Checking Group: $GroupName"
try {
$Group = Get-ADGroup `
-Identity $GroupName `
-Properties `
Description,
ManagedBy `
-ErrorAction Stop
# --------------------------
# Direct Members
# --------------------------
$DirectMembers = @(
Get-ADGroupMember `
-Identity $GroupName `
-ErrorAction Stop
)
foreach ($Member in $DirectMembers) {
$DirectMemberReport +=
[PSCustomObject]@{
GroupName =
$Group.Name
MemberName =
$Member.Name
SamAccountName =
$Member.SamAccountName
ObjectClass =
$Member.ObjectClass
MembershipType =
"Direct"
DistinguishedName =
$Member.DistinguishedName
}
}
# --------------------------
# Recursive Members
# --------------------------
$RecursiveMembers = @(
Get-ADGroupMember `
-Identity $GroupName `
-Recursive `
-ErrorAction Stop
)
foreach ($Member in $RecursiveMembers) {
$RecursiveMemberReport +=
[PSCustomObject]@{
GroupName =
$Group.Name
MemberName =
$Member.Name
SamAccountName =
$Member.SamAccountName
ObjectClass =
$Member.ObjectClass
MembershipType =
"Recursive"
DistinguishedName =
$Member.DistinguishedName
}
}
# --------------------------
# Disabled Users
# --------------------------
$UserMembers =
$RecursiveMembers |
Where-Object {
$_.ObjectClass -eq "user"
}
foreach ($Member in $UserMembers) {
try {
$User = Get-ADUser `
-Identity $Member.DistinguishedName `
-Properties `
Enabled,
Department,
LastLogonDate `
-ErrorAction Stop
if (-not $User.Enabled) {
$DisabledMemberReport +=
[PSCustomObject]@{
GroupName =
$Group.Name
Name =
$User.Name
SamAccountName =
$User.SamAccountName
Department =
$User.Department
Enabled =
$User.Enabled
LastLogonDate =
$User.LastLogonDate
DistinguishedName =
$User.DistinguishedName
}
}
}
catch {
Write-Warning `
"Failed to query user $($Member.Name): $($_.Exception.Message)"
}
}
# --------------------------
# Group Summary
# --------------------------
$GroupSummary +=
[PSCustomObject]@{
GroupName =
$Group.Name
GroupCategory =
$Group.GroupCategory
GroupScope =
$Group.GroupScope
Description =
$Group.Description
ManagedBy =
$Group.ManagedBy
DirectMemberCount =
$DirectMembers.Count
RecursiveMemberCount =
$RecursiveMembers.Count
DisabledUserCount =
@(
$DisabledMemberReport |
Where-Object {
$_.GroupName -eq
$Group.Name
}
).Count
CheckTime =
Get-Date
}
}
catch {
Write-Warning `
"Failed to audit $GroupName : $($_.Exception.Message)"
}
}
$GroupSummary |
Export-Csv -Path "$ReportFolder\AD_Group_Summary_$Date.csv"
-NoTypeInformation `
-Encoding UTF8
$DirectMemberReport |
Export-Csv -Path "$ReportFolder\AD_Group_DirectMembers_$Date.csv"
-NoTypeInformation `
-Encoding UTF8
$RecursiveMemberReport |
Export-Csv -Path "$ReportFolder\AD_Group_RecursiveMembers_$Date.csv"
-NoTypeInformation `
-Encoding UTF8
$DisabledMemberReport |
Export-Csv -Path "$ReportFolder\AD_Group_DisabledMembers_$Date.csv"
-NoTypeInformation `
-Encoding UTF8
Write-Host ""
Write-Host "===== AD Group Audit Completed ====="
Write-Host ""
$GroupSummary
Write-Host ""
Write-Host "Reports:"
Write-Host "$ReportFolder\AD_Group_Summary_$Date.csv"
Write-Host "$ReportFolder\AD_Group_DirectMembers_$Date.csv"
Write-Host "$ReportFolder\AD_Group_RecursiveMembers_$Date.csv"
Write-Host "$ReportFolder\AD_Group_DisabledMembers_$Date.csv"
執行後會得到四份報表
C:\Temp\ADReports
│
├── AD_Group_Summary_20260924.csv
├── AD_Group_DirectMembers_20260924.csv
├── AD_Group_RecursiveMembers_20260924.csv
└── AD_Group_DisabledMembers_20260924.csv
每一份回答的問題不同。
Group Summary
例如:
Group Direct Recursive Disabled
Domain Admins 4 4 0
VPN-Users 253 411 6
Server-Admins 12 23 1
這一份適合快速看:
Group 成員數突然變多?
Nested Group 很複雜?
有 Disabled User?
Direct Members
這一份回答:
誰被直接加入這個 Group?
例如:
VPN-Users
├── user01
├── user02
└── VPN-IT
這在追查:
為什麼這個人有權限?
時非常重要。
Recursive Members
這一份回答:
把 Nested Group 展開後有哪些 Member?
例如:
VPN-Users
↓
VPN-IT
↓
tedliu
Recursive Report 會讓 tedliu 出現在結果裡。
Disabled Members
這一份則專門找:
Account Disabled
但 Membership 還留在重要 Group
這就可以交給帳號或權限負責人進一步 Review。
不要在 Audit Script 裡直接 Remove Member
今天特別不加入:
Remove-ADGroupMember
原因跟前兩天完全一樣。
假設:
olduser
Enabled = False
仍然在:
VPN-Users
從治理角度來看可能應該清掉。
但是正式環境仍然最好:
Audit
↓
Report
↓
確認帳號用途
↓
確認 Group Owner
↓
Approval
↓
Remove Membership
↓
Log
而不是:
PowerShell 發現
↓
自動移除
尤其:
Domain Admins
Enterprise Admins
Server Admins
Backup Operators
這類高權限群組,更應該留下完整的 Change Record。
Day 16 小結
今天我們正式開始碰 Active Directory 的:
Group
主要使用:
Get-ADGroup
Get-ADGroupMember
Get-ADPrincipalGroupMembership
並理解了 Group Audit 裡幾個很重要的概念:
Direct Membership
≠
Nested / Recursive Membership
以及:
User 在 Group 裡
≠
已經知道完整最終資源權限
因為真正的權限可能還牽涉:
AD Group
NTFS
Share Permission
Local Group
Application RBAC
今天我們已經可以回答:
某個 User 加了哪些 AD Group?
某個 Group 有哪些成員?
Group 裡是不是還有 Group?
展開 Nested Group 後有多少人?
重要 Group 裡還有 Disabled User 嗎?
哪些 Security Group 沒有 Description?
哪些 Group 沒有 Owner?
這代表我們從前幾天的:
這個帳號還有沒有在用?
開始往更重要的一層前進:
這個帳號到底擁有哪些權限?
Day 17 預告
Day 17|AD 帳號生命週期自動化:建立、停用與離職帳號該怎麼安全處理?
前面幾天一直都是:
Query
Audit
Report
到了 Day 17,我們才會第一次開始碰真正會:
修改 Active Directory
的 Cmdlet。
例如:
New-ADUser
Set-ADUser
Disable-ADAccount
Move-ADObject
但不會直接做成:
讀 CSV
↓
看到離職
↓
直接 Disable
而是設計成:
Input
↓
Validation
↓
WhatIf / Preview
↓
Approval
↓
Action
↓
Move Disabled OU
↓
Log
↓
Report
並開始討論一個很實際的離職流程:
Disable Account
Remove Sensitive Group Membership
Move OU
更新 Description
保留稽核紀錄
Day 17 會是整個系列第一次正式從「Read-Only Automation」進入「Change Automation」。