Isolate unknown dMSA prerequisites from other AD audit classes

This commit is contained in:
Shirofune-Security committed 2026-09-19 06:20:41 +09:00
1 parent cbd1c0643b
commit 92bf29ec22
7 files changed
+67 -15

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@
**改善:**
- MDIのドメイン/Exchange Configuration監査と、明示的に選択した証明書テンプレート/登録サービスオブジェクト向けに、任意実行の`ad-object-sacl`監査・計画・設定・保守的なロールバックを追加しました。接続先DCとスキーマGUIDを検証し、既存のセキュリティ設定を保持したまま不足する監査ACEだけをSACLに追加します。変更前のSDDLと追加ACEを保存し、書き込み後と最終状態を確認します。実効監査ポリシー、継承・レプリケーション、4662/5136イベントの証拠は隔離DCで別途検証が必要です。Sigma検知範囲の向上は未検証です。 (#402) (@Shirofune-Security)
- MDIのドメイン/Exchange Configuration監査と、明示的に選択した証明書テンプレート/登録サービスオブジェクト向けに、任意実行の`ad-object-sacl`監査・計画・設定・保守的なロールバックを追加しました。接続先DCとスキーマGUIDを検証し、既存のセキュリティ設定を保持したまま不足する監査ACEだけをSACLに追加します。変更前のSDDLと追加ACEを保存し、書き込み後と最終状態を確認します。任意のdMSA前提条件が不明な場合は未確認のスキップ項目として報告し、独立した他の5種類のドメインクラスの監査ACEは引き続き設定します。実効監査ポリシー、継承・レプリケーション、4662/5136イベントの証拠は隔離DCで別途検証が必要です。Sigma検知範囲の向上は未検証です。 (#402) (@Shirofune-Security)
- ネイティブのDomain/Private/Publicテキストログを監査・計画・設定する任意実行の`firewall-logging`を追加しました。許可・破棄ログの有効化、最小サイズの確認、既存パスと大きな上限値の保持、CIS v4.0.0のパスの明示的な選択に対応します。ファイアウォールサービスのディレクトリ権限を確認し、ローカル設定と実効設定を記録して変更後の実効設定を検証します。通信制御やACLは変更しません。実通信によるログ生成と収集の検証は別途必要です。 (#394) (@Shirofune-Security)
- イベントログのサイズ監査と設定に共通のバイト単位プロファイルを導入し、AppLocker・ファイアウォールログの256 MiB、Setupの32 MiB、ASD推奨のSecurityログ2048 MiBに対応した。`-LogProfile`と`configure-eventlogs`で送信元と収集サーバーのサイズ・保存方式を選択できる。明示的に指定しない限り、既存の大きいバッファと保存方式は維持する。結果には検証した設定を記録し、未測定の保存日数は不明と表示する。 (#396) (@Shirofune-Security)
+1 -1
View File
@@ -4,7 +4,7 @@
**Improvements:**
- Added opt-in `ad-object-sacl` audit, plan, configure and conservative rollback actions for MDI domain/Exchange Configuration auditing and explicitly selected certificate template/enrollment service objects. Exact DC binding, schema GUID checks, additive SACL-only changes, pre-write SDDL/ACE receipts and read-back preserve existing security entries. Effective audit policy, inheritance/replication and 4662/5136 event evidence remain separate isolated-DC checks; no Sigma uplift is claimed. (#402) (@Shirofune-Security)
- Added opt-in `ad-object-sacl` audit, plan, configure and conservative rollback actions for MDI domain/Exchange Configuration auditing and explicitly selected certificate template/enrollment service objects. Exact DC binding, schema GUID checks, additive SACL-only changes, pre-write SDDL/ACE receipts and read-back preserve existing security entries. Unknown optional dMSA prerequisites are reported as a separate skipped gap while the five independent domain class ACEs continue. Effective audit policy, inheritance/replication and 4662/5136 event evidence remain separate isolated-DC checks; no Sigma uplift is claimed. (#402) (@Shirofune-Security)
- Added opt-in `firewall-logging` audit, plan and configure actions for native Domain/Private/Public text logs, with allowed/dropped logging, minimum size checks, preserved operator paths/larger limits, and explicit CIS v4.0.0 paths. Configuration checks firewall service directory permissions, journals local/effective state and verifies effective policy without changing firewall enforcement or ACLs. Traffic and ingestion validation remains required. (#394) (@Shirofune-Security)
- Unified event-log size auditing and configuration with shared byte-based profiles, including 256 MiB AppLocker/firewall logs, 32 MiB Setup and ASD 2048 MiB Security. Added separate source and collector size/mode choices through `-LogProfile` and `configure-eventlogs`; larger buffers and existing retention modes are preserved unless explicitly changed. Results include verified state and unknown retention duration. (#396) (@Shirofune-Security)
+1 -1
View File
@@ -41,7 +41,7 @@ All ACEs use **Everyone (`S-1-1-0`)**. An empty object GUID is `00000000-0000-00
| msDS-GroupManagedServiceAccount | 852075 (`0xD006B`) | 7b8b558a-93a5-4af7-adca-c017e67f1057 |
| msDS-DelegatedManagedServiceAccount | 852075 (`0xD006B`) | 0feb936f-47b3-49f2-9386-1dedc2c23765 |
Both masks include CreateChild, DeleteChild, Self, WriteProperty, DeleteTree, Delete, WriteDacl and WriteOwner. `852331` additionally includes ExtendedRight. dMSA is omitted, with a diagnostic, unless its schema class exists and a domain DC computer reports a version at least `10.0 (26100)`. This follows [MDI's Server 2025 domain condition](https://learn.microsoft.com/en-us/defender-for-identity/deploy/configure-windows-event-collection#configure-auditing-on-domain-objects). Missing mandatory schema classes or unreadable/unknown applicability produce `Unknown`, not an assumed audit configuration. Protected child SACLs and inheritance propagation are not established by a root ACE read-back.
Both masks include CreateChild, DeleteChild, Self, WriteProperty, DeleteTree, Delete, WriteDacl and WriteOwner. `852331` additionally includes ExtendedRight. dMSA is omitted, with a diagnostic, unless its schema class exists and a domain DC computer reports a version at least `10.0 (26100)`. This follows [MDI's Server 2025 domain condition](https://learn.microsoft.com/en-us/defender-for-identity/deploy/configure-windows-event-collection#configure-auditing-on-domain-objects). Missing or unreadable mandatory schema classes still fail the domain request closed. If only the conditional dMSA schema/DC check is unreadable or cannot classify DC versions, the other five class ACEs continue; the plan records dMSA in `SkippedDefinitions` with `PrerequisiteStatus: Unknown` and the original diagnostic. Known inapplicability uses `NotApplicable` instead. Configure emits a separate `Skipped` prerequisite row, writes only the selected five definitions, and reports completion with a skipped control. This does not establish dMSA auditing or complete six-class coverage; resolve the prerequisite and rerun to assess dMSA. Protected child SACLs and inheritance propagation are not established by a root ACE read-back.
`MdiConfiguration` targets RootDSE `configurationNamingContext`, with **WriteProperty (`32`, `0x20`), Success and Failure**, **this object and all descendants**, flags `194` (`0xC2`), and both GUIDs empty. Microsoft's [Configuration container guidance](https://learn.microsoft.com/en-us/defender-for-identity/deploy/configure-windows-event-collection#configure-auditing-on-the-configuration-container) is conditional on current or former Exchange deployments. WELA checks for an `msExchOrganizationContainer`; no matching object produces `NotApplicable`. If all historical Exchange configuration was removed, inspect that history manually: absence does not prove Exchange never existed. Configuration is replicated forest-wide; this operation is not scoped to one domain's users.
+30 -6
View File
@@ -248,14 +248,32 @@ function Get-WelaAdSaclPlan {
foreach ($dn in @($ObjectDn | Select-Object -Unique)) { $requests += [pscustomobject]@{ Profile = 'PkiObjects'; Dn = $dn } }
} elseif ($ObjectDn.Count) { throw '-AdObjectDn is valid only with the PkiObjects profile.' }
foreach ($request in $requests) {
$definitions = @(); $before = $null; $notes = @(); $status = 'Unknown'
$definitions = @(); $skippedDefinitions = @(); $before = $null; $notes = @(); $status = 'Unknown'
try {
if ($request.Profile -eq 'MdiDomain') {
foreach ($definition in Get-WelaAdAuditDefinitions) {
$exists = Test-WelaAdSchemaClass $Session $definition.Class $definition.InheritedObjectType
if ($definition.Class -eq 'msDS-DelegatedManagedServiceAccount') {
if (-not $exists -or -not (Test-WelaAdDmsaDomain $Session)) { $notes += 'dMSA skipped: schema class and a domain DC version >= 10.0 (26100) are required.'; continue }
} elseif (-not $exists) { throw "Required MDI schema class is absent: $($definition.Class)." }
# dMSA is conditional. Its unknown schema/DC prerequisites
# must not prevent the five independent class ACEs.
$prerequisiteStatus = 'Applicable'; $diagnostic = ''
try {
$exists = Test-WelaAdSchemaClass $Session $definition.Class $definition.InheritedObjectType
if (-not $exists -or -not (Test-WelaAdDmsaDomain $Session)) {
$prerequisiteStatus = 'NotApplicable'
$diagnostic = 'dMSA skipped: schema class and a domain DC version >= 10.0 (26100) are required.'
}
} catch {
$prerequisiteStatus = 'Unknown'
$diagnostic = "dMSA skipped: prerequisite Unknown ($($_.Exception.Message)). dMSA auditing is not established; the other five class ACEs remain independent."
}
if ($prerequisiteStatus -ne 'Applicable') {
$notes += $diagnostic
$skippedDefinitions += [pscustomobject]@{ Definition = $definition; Status = 'Skipped'; PrerequisiteStatus = $prerequisiteStatus; Diagnostic = $diagnostic }
continue
}
} elseif (-not (Test-WelaAdSchemaClass $Session $definition.Class $definition.InheritedObjectType)) {
throw "Required MDI schema class is absent: $($definition.Class)."
}
$definitions += $definition
}
} elseif ($request.Profile -eq 'MdiConfiguration') {
@@ -282,7 +300,7 @@ function Get-WelaAdSaclPlan {
if (-not $Session.Writable -and $missing.Count) { $status = 'Blocked'; $notes += 'Selected DC is read-only.' }
} catch { $notes += $_.Exception.Message }
[pscustomobject]@{ Profile = $request.Profile; Server = $Session.Server; Dn = $request.Dn; Status = $status;
Definitions = $definitions; Before = $before; Diagnostic = $notes -join ' ' }
Definitions = $definitions; SkippedDefinitions = $skippedDefinitions; Before = $before; Diagnostic = $notes -join ' ' }
}
}
@@ -290,6 +308,12 @@ function Set-WelaAdSaclControls {
param($Session, $Context, [array]$Plan)
foreach ($entry in $Plan) {
$id = "AdSacl/$($entry.Profile)/$($entry.Dn)"
foreach ($skipped in $entry.SkippedDefinitions) {
$Context.Results.Add([pscustomobject]@{ Id = "$id/$($skipped.Definition.Class)/Prerequisite"; Kind = 'AdObjectSaclPrerequisite';
Target = @{ Server = $Session.Server; Dn = $entry.Dn; Class = $skipped.Definition.Class }; Desired = $skipped.Definition;
Before = $null; After = $null; Status = 'Skipped'; PrerequisiteStatus = $skipped.PrerequisiteStatus; Diagnostic = $skipped.Diagnostic })
Write-Host "[Skipped] $id/$($skipped.Definition.Class) $($skipped.Diagnostic)" -ForegroundColor Yellow
}
if ($entry.Status -notin @('SaclConfigured', 'ChangeRequired')) {
$Context.Results.Add([pscustomobject]@{ Id = $id; Kind = 'AdObjectSacl'; Target = @{ Server = $Session.Server; Dn = $entry.Dn }; Desired = $entry.Definitions;
Before = $entry.Before; After = $null; Status = $(if ($entry.Status -eq 'NotApplicable') { 'Skipped' } else { 'Failed' }); Diagnostic = $entry.Diagnostic })
@@ -415,7 +439,7 @@ function Invoke-WelaAdSaclCommand {
$report | Add-Member NoteProperty AuditPolicyPrerequisites @(
[pscustomobject]@{ Name = 'Directory Service Access'; Guid = '0cce923b-69ae-11d9-bed3-505054503030'; Required = 'Success (Failure also required for Configuration failure auditing)'; Status = 'Unknown'; Diagnostic = 'Remote DC audit policy is not read or changed by this LDAP command.' },
[pscustomobject]@{ Name = 'Directory Service Changes'; Guid = '0cce923c-69ae-11d9-bed3-505054503030'; Required = 'Success'; Status = 'Unknown'; Diagnostic = 'Verify effective policy and 5136 on the DC handling the object change.' })
$report | Add-Member NoteProperty VerificationScope 'Selected-DC object SACL state only. Inherited child SACLs, protected objects, policy, 4662/5136 generation, replication and collection are unverified. No Sigma uplift is claimed.'
$report | Add-Member NoteProperty VerificationScope 'Selected-DC object SACL state only. Skipped or Unknown class prerequisites do not establish auditing for those classes. Inherited child SACLs, protected objects, policy, 4662/5136 generation, replication and collection are unverified. No Sigma uplift is claimed.'
if ($ResultsPath) { $report | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop }
return $report
} finally { $session.Connection.Dispose() }
+32 -4
View File
@@ -21,12 +21,19 @@ function Reset-Mocks {
$script:writes = 0; $script:reads = 0; $script:readError = $false; $script:writeError = $false
$script:race = $false; $script:finalDrift = $false; $script:badReadback = $false; $script:removeExisting = $false
$script:absentClass = ''; $script:dmsa = $true; $script:exchange = $true; $script:onPrompt = $null; $script:pkiMember = $true; $script:pkiRequest = $null
$script:dmsaError = ''; $script:schemaErrorClass = ''; $script:plannedAdditions = @()
$script:state = [pscustomobject]@{ Server = $session.Server; Dn = $session.DomainDn; ObjectGuid = '01234567-89ab-cdef-0123-456789abcdef'; UsnChanged = '17'; Classes = @('top', 'domainDNS');
Descriptor = [pscustomobject]@{ Binary = 'before'; Sddl = 'O:SYG:SYD:(A;;GA;;;SY)S:(AU;SA;WP;;;BA)'; Owner = 'S-1-5-18'; Group = 'S-1-5-18'; Dacl = 'unchanged-dacl'; ControlFlags = 32788; Sacl = @('unrelated') } }
$session.Writable = $true
}
function Test-WelaAdSchemaClass { param($Session, $Class, $Guid) return $Class -ne $script:absentClass }
function Test-WelaAdDmsaDomain { param($Session) return $script:dmsa }
function Test-WelaAdSchemaClass { param($Session, $Class, $Guid)
if ($Class -eq $script:schemaErrorClass) { throw 'Schema lookup access denied' }
return $Class -ne $script:absentClass
}
function Test-WelaAdDmsaDomain { param($Session)
if ($script:dmsaError) { throw $script:dmsaError }
return $script:dmsa
}
function Search-WelaAdDirectory {
param($Session, $Dn, $Filter, $Scope, $Attributes, [switch]$SecurityDescriptor)
if ($Filter -eq '(objectClass=msExchOrganizationContainer)' -and $script:exchange) { [pscustomobject]@{ Dn = 'CN=Exchange'; Values = @{} } }
@@ -47,14 +54,15 @@ function Get-WelaAdObjectState {
function Test-WelaAdAcePresent { param($Descriptor, $Definition) return $Descriptor.Sacl -contains ('added-' + $Definition.Class) }
function New-WelaAdSaclAddition {
param($Before, $Definitions)
[pscustomobject]@{ Binary = 'after'; AddedAces = @($Definitions | Where-Object { -not (Test-WelaAdAcePresent $Before.Descriptor $_) } | ForEach-Object { 'added-' + $_.Class }) }
$script:plannedAdditions = @($Definitions | Where-Object { -not (Test-WelaAdAcePresent $Before.Descriptor $_) } | ForEach-Object { 'added-' + $_.Class })
[pscustomobject]@{ Binary = 'after'; AddedAces = $script:plannedAdditions }
}
function Write-WelaAdSacl {
param($Session, $Dn, $Binary)
if ($script:writeError) { throw 'LDAP insufficientAccessRights' }
$script:writes++
if (-not $script:badReadback) {
$script:state.Descriptor.Sacl = @('unrelated') + @(Get-WelaAdAuditDefinitions | ForEach-Object { 'added-' + $_.Class })
$script:state.Descriptor.Sacl = @($script:state.Descriptor.Sacl) + $script:plannedAdditions
$script:state.Descriptor.Binary = $Binary
}
if ($script:removeExisting) { $script:state.Descriptor.Sacl = @($script:state.Descriptor.Sacl | Where-Object { $_ -ne 'unrelated' }) }
@@ -82,8 +90,28 @@ try {
$script:dmsa = $false
$plan = @(Get-WelaAdSaclPlan $session @('MdiDomain') @())
Assert ($plan[0].Definitions.Count -eq 5 -and $plan[0].Diagnostic -like '*dMSA skipped*') 'dMSA omitted without a 2025 domain DC'
Assert ($plan[0].SkippedDefinitions[0].PrerequisiteStatus -eq 'NotApplicable') 'known dMSA omission is distinct from unknown prerequisites'
$script:absentClass = 'user'
Assert ((@(Get-WelaAdSaclPlan $session @('MdiDomain') @()))[0].Status -eq 'Unknown') 'missing required class blocks writes'
foreach ($errorMessage in @('DC versions could not all be classified; dMSA applicability is unknown.', 'LDAP DC version query access denied')) {
Reset-Mocks; $script:dmsaError = $errorMessage
$plan = @(Get-WelaAdSaclPlan $session @('MdiDomain') @())
Assert ($plan[0].Status -eq 'ChangeRequired' -and $plan[0].Definitions.Count -eq 5) 'unknown optional dMSA check preserves five independent planned ACEs'
Assert ($plan[0].SkippedDefinitions.Count -eq 1 -and $plan[0].SkippedDefinitions[0].PrerequisiteStatus -eq 'Unknown' -and
$plan[0].SkippedDefinitions[0].Diagnostic.Contains($errorMessage)) 'plan records the skipped dMSA prerequisite and exact unknown diagnostic'
$ctx = Get-Context; $report = Invoke-TestConfigure $ctx
$applied = @($report.Results | Where-Object Kind -eq 'AdObjectSacl')
$gap = @($report.Results | Where-Object Kind -eq 'AdObjectSaclPrerequisite')
Assert ($report.ExitCode -eq 0 -and $report.Skipped -eq 1 -and $applied[0].Status -eq 'Applied' -and $applied[0].Desired.Count -eq 5) 'five ACEs configure with a visible skipped-control result'
Assert ($gap.Count -eq 1 -and $gap[0].Status -eq 'Skipped' -and $gap[0].PrerequisiteStatus -eq 'Unknown' -and $gap[0].Target.Class -eq 'msDS-DelegatedManagedServiceAccount') 'configuration retains structured dMSA Unknown gap'
Assert ($script:plannedAdditions.Count -eq 5 -and $script:state.Descriptor.Sacl -notcontains 'added-msDS-DelegatedManagedServiceAccount') 'unknown dMSA never produces a sixth ACE write'
}
Reset-Mocks; $script:schemaErrorClass = 'msDS-DelegatedManagedServiceAccount'
$plan = @(Get-WelaAdSaclPlan $session @('MdiDomain') @())
Assert ($plan[0].Status -eq 'ChangeRequired' -and $plan[0].Definitions.Count -eq 5 -and $plan[0].SkippedDefinitions[0].PrerequisiteStatus -eq 'Unknown') 'optional dMSA schema read error also remains isolated'
Reset-Mocks; $script:schemaErrorClass = 'user'
$ctx = Get-Context; $report = Invoke-TestConfigure $ctx
Assert ($report.ExitCode -eq 1 -and $script:writes -eq 0 -and $report.Results[0].Status -eq 'Failed') 'mandatory schema lookup failure still fails closed'
Reset-Mocks; $script:exchange = $false
Assert ((@(Get-WelaAdSaclPlan $session @('MdiConfiguration') @()))[0].Status -eq 'NotApplicable') 'configuration gated on Exchange history evidence'
$script:exchange = $true
+1 -1
View File
@@ -7,7 +7,7 @@
**改善:**
- MDIのドメイン/Exchange Configuration監査と、明示的に選択した証明書テンプレート/登録サービスオブジェクト向けに、任意実行の`ad-object-sacl`監査・計画・設定・保守的なロールバックを追加しました。接続先DCとスキーマGUIDを検証し、既存のセキュリティ設定を保持したまま不足する監査ACEだけをSACLに追加します。変更前のSDDLと追加ACEを保存し、書き込み後と最終状態を確認します。実効監査ポリシー、継承・レプリケーション、4662/5136イベントの証拠は隔離DCで別途検証が必要です。Sigma検知範囲の向上は未検証です。 (#402) (@Shirofune-Security)
- MDIのドメイン/Exchange Configuration監査と、明示的に選択した証明書テンプレート/登録サービスオブジェクト向けに、任意実行の`ad-object-sacl`監査・計画・設定・保守的なロールバックを追加しました。接続先DCとスキーマGUIDを検証し、既存のセキュリティ設定を保持したまま不足する監査ACEだけをSACLに追加します。変更前のSDDLと追加ACEを保存し、書き込み後と最終状態を確認します。任意のdMSA前提条件が不明な場合は未確認のスキップ項目として報告し、独立した他の5種類のドメインクラスの監査ACEは引き続き設定します。実効監査ポリシー、継承・レプリケーション、4662/5136イベントの証拠は隔離DCで別途検証が必要です。Sigma検知範囲の向上は未検証です。 (#402) (@Shirofune-Security)
- ネイティブのDomain/Private/Publicテキストログを監査・計画・設定する任意実行の`firewall-logging`を追加しました。許可・破棄ログの有効化、最小サイズの確認、既存パスと大きな上限値の保持、CIS v4.0.0のパスの明示的な選択に対応します。ファイアウォールサービスのディレクトリ権限を確認し、ローカル設定と実効設定を記録して変更後の実効設定を検証します。通信制御やACLは変更しません。実通信によるログ生成と収集の検証は別途必要です。 (#394) (@Shirofune-Security)
- イベントログのサイズ監査と設定に共通のバイト単位プロファイルを導入し、AppLocker・ファイアウォールログの256 MiB、Setupの32 MiB、ASD推奨のSecurityログ2048 MiBに対応した。`-LogProfile`と`configure-eventlogs`で送信元と収集サーバーのサイズ・保存方式を選択できる。明示的に指定しない限り、既存の大きいバッファと保存方式は維持する。結果には検証した設定を記録し、未測定の保存日数は不明と表示する。 (#396) (@Shirofune-Security)
+1 -1
View File
@@ -7,7 +7,7 @@
**Improvements:**
- Added opt-in `ad-object-sacl` audit, plan, configure and conservative rollback actions for MDI domain/Exchange Configuration auditing and explicitly selected certificate template/enrollment service objects. Exact DC binding, schema GUID checks, additive SACL-only changes, pre-write SDDL/ACE receipts and read-back preserve existing security entries. Effective audit policy, inheritance/replication and 4662/5136 event evidence remain separate isolated-DC checks; no Sigma uplift is claimed. (#402) (@Shirofune-Security)
- Added opt-in `ad-object-sacl` audit, plan, configure and conservative rollback actions for MDI domain/Exchange Configuration auditing and explicitly selected certificate template/enrollment service objects. Exact DC binding, schema GUID checks, additive SACL-only changes, pre-write SDDL/ACE receipts and read-back preserve existing security entries. Unknown optional dMSA prerequisites are reported as a separate skipped gap while the five independent domain class ACEs continue. Effective audit policy, inheritance/replication and 4662/5136 event evidence remain separate isolated-DC checks; no Sigma uplift is claimed. (#402) (@Shirofune-Security)
- Added opt-in `firewall-logging` audit, plan and configure actions for native Domain/Private/Public text logs, with allowed/dropped logging, minimum size checks, preserved operator paths/larger limits, and explicit CIS v4.0.0 paths. Configuration checks firewall service directory permissions, journals local/effective state and verifies effective policy without changing firewall enforcement or ACLs. Traffic and ingestion validation remains required. (#394) (@Shirofune-Security)
- Unified event-log size auditing and configuration with shared byte-based profiles, including 256 MiB AppLocker/firewall logs, 32 MiB Setup and ASD 2048 MiB Security. Added separate source and collector size/mode choices through `-LogProfile` and `configure-eventlogs`; larger buffers and existing retention modes are preserved unless explicitly changed. Results include verified state and unknown retention duration. (#396) (@Shirofune-Security)