# 역할 할당 일괄 삭제 스크립트 작성

Get-UserGroup-Owner-RoleAssignments-Subscriptions.ps1

# =====================================================================
# タイムスタンプ生成(CSV/LOG共通で使用)
# =====================================================================
$jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
$stamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyyMMdd-HHmmss")

# =====================================================================
# ログファイルパス(タイムスタンプ付き)
# =====================================================================
$logFilePath = "RoleAssignments.$stamp.log"

# =====================================================================
# ログメッセージ作成関数
# =====================================================================
function Write-LogMessage {
  param ([string]$logMessage)
  $jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
  $timestamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyy-MM-dd HH:mm:ss")
  $logEntry = "$timestamp - $logMessage"
  Write-Host $logEntry
  Add-Content -Path $logFilePath -Value $logEntry
}

# =====================================================================
# サブスクリプション一覧の取得(Id & Name)およびマッピングテーブル作成
# =====================================================================
$azureSubscriptionsJsonString = az account list --query "[].{Id:id,Name:name}" -o json
if ($LASTEXITCODE -ne 0) {
  Write-LogMessage "Failed to retrieve subscription list."
  return
}
$azureSubscriptionObjects = $azureSubscriptionsJsonString | ConvertFrom-Json
$azureSubscriptionIdList = $azureSubscriptionObjects.Id

# サブスクリプションID → サブスクリプション名のマッピングテーブルを作成
$azureSubscriptionIdToNameMap = @{}
foreach ($subscriptionObject in $azureSubscriptionObjects) {
  $azureSubscriptionIdToNameMap[$subscriptionObject.Id] = $subscriptionObject.Name
}

# =====================================================================
# 役割割り当て結果を保存する配列を初期化
# =====================================================================
$allUserGroupRoleAssignments = @()

foreach ($azureSubscriptionId in $azureSubscriptionIdList) {
  Write-LogMessage "Processing subscription: $azureSubscriptionId"

  # 役割割り当てを取得(JSON)
  $roleAssignmentsJsonString = az role assignment list --all --subscription $azureSubscriptionId --output json
  if ($LASTEXITCODE -ne 0) {
    Write-LogMessage "Failed to retrieve role assignments: $azureSubscriptionId"
    continue
  }

  # JSON → オブジェクト変換 & User/Groupのみフィルタリング(Owner除外、管理グループ継承除外)
  $filteredUserGroupRoleAssignments = $roleAssignmentsJsonString | ConvertFrom-Json | Where-Object {
    $_.principalType -in @("User", "Group") -and
    $_.roleDefinitionName -ne "Owner" -and
    $_.scope -like "/subscriptions/*"
  }

  if ($filteredUserGroupRoleAssignments) {
    # SubscriptionId/NameおよびPrincipalObjectId情報を追加
    $enrichedUserGroupRoleAssignments = $filteredUserGroupRoleAssignments | ForEach-Object {
      # ScopeからSubscriptionIdを抽出: /subscriptions/<id>/...
      $regexMatch = [regex]::Match($_.scope, '^/subscriptions/([^/]+)')
      $subscriptionIdFromScope = if ($regexMatch.Success) { $regexMatch.Groups[1].Value } else { $null }

      [pscustomobject]@{
        SubscriptionName   = $azureSubscriptionIdToNameMap[$subscriptionIdFromScope]
        SubscriptionId     = $subscriptionIdFromScope
        PrincipalName      = $_.principalName
        PrincipalObjectId  = $_.principalId
        RoleDefinitionName = $_.roleDefinitionName
        Scope              = $_.scope
        PrincipalType      = $_.principalType
      }
    }

    # ログ出力用テーブルフォーマット
    $tableOutputForLog = $enrichedUserGroupRoleAssignments |
    Format-Table SubscriptionName, SubscriptionId, PrincipalObjectId, PrincipalName, RoleDefinitionName, Scope, PrincipalType -AutoSize |
    Out-String
    Write-LogMessage "User/Group role assignments:`n$tableOutputForLog"

    # CSV出力用に累積
    $allUserGroupRoleAssignments += $enrichedUserGroupRoleAssignments
  }
  else {
    Write-LogMessage "No User/Group role assignments: $azureSubscriptionId"
  }
}

# =====================================================================
# CSVファイルパス定義(ログと同じタイムスタンプを使用)
# =====================================================================
$exportCsvFilePath = "RoleAssignments.$stamp.csv"

# =====================================================================
# 結果CSVのエクスポート
# =====================================================================
if ($allUserGroupRoleAssignments.Count -gt 0) {
  $allUserGroupRoleAssignments |
  Select-Object SubscriptionName, SubscriptionId, PrincipalName, PrincipalObjectId, RoleDefinitionName, Scope, PrincipalType |
  Export-Csv -Path $exportCsvFilePath -NoTypeInformation -Encoding UTF8

  Write-LogMessage "Exported to CSV: $exportCsvFilePath"
}
else {
  Write-LogMessage "No User/Group role assignments to export."
}

Get-UserGroup-Owner-RoleAssignments-Subscriptions-FromCSV.ps1

param(
  [Parameter(Mandatory = $true)]
  [string]$CsvFilePath  # 対象のSubscriptionIdリストが含まれるCSVパス(ヘッダーにSubscriptionId必須)
)

# =====================================================================
# タイムスタンプ生成(CSV/LOG共通で使用)
# =====================================================================
$jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
$stamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyyMMdd-HHmmss")

# =====================================================================
# ログファイルパス(タイムスタンプ付き)
# =====================================================================
$logFilePath = "RoleAssignments.$stamp.log"

# =====================================================================
# ログメッセージ作成関数(JST基準時間)
# =====================================================================
function Write-LogMessage {
  param ([string]$logMessage)
  $jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
  $timestamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyy-MM-dd HH:mm:ss")
  $logEntry = "$timestamp - $logMessage"
  Write-Host $logEntry
  Add-Content -Path $logFilePath -Value $logEntry
}

# =====================================================================
# 入力CSVの検証およびロード(SubscriptionIdを基準に収集)
# =====================================================================
if (-not (Test-Path $CsvFilePath)) {
  Write-LogMessage "CSV not found: $CsvFilePath"
  return
}
try {
  $csvRows = Import-Csv -Path $CsvFilePath
}
catch {
  Write-LogMessage "Failed to import CSV: $($_.Exception.Message)"
  return
}

if (-not $csvRows -or -not ($csvRows | Get-Member -Name SubscriptionId -MemberType NoteProperty)) {
  Write-LogMessage "CSV must contain a 'SubscriptionId' column."
  return
}

# CSVからSubscriptionIdのみを抽出(重複排除、空白/NULLを除外)
$azureSubscriptionIdList = $csvRows |
Where-Object { $_.SubscriptionId -and $_.SubscriptionId.Trim() -ne "" } |
ForEach-Object { $_.SubscriptionId.Trim() } |
Select-Object -Unique

if (-not $azureSubscriptionIdList -or $azureSubscriptionIdList.Count -eq 0) {
  Write-LogMessage "No SubscriptionId values found in CSV."
  return
}

# =====================================================================
# サブスクリプション一覧の取得(Id & Name)およびマッピングテーブル作成
#   - 名前のマッピングは可能な場合のみ使用(存在しない場合は空欄)
# =====================================================================
$azureSubscriptionsJsonString = az account list --query "[].{Id:id,Name:name}" -o json
if ($LASTEXITCODE -ne 0) {
  Write-LogMessage "Failed to retrieve subscription list."
  return
}
$azureSubscriptionObjects = $azureSubscriptionsJsonString | ConvertFrom-Json

# サブスクリプションID → サブスクリプション名のマッピングテーブルを作成
$azureSubscriptionIdToNameMap = @{}
foreach ($subscriptionObject in $azureSubscriptionObjects) {
  $azureSubscriptionIdToNameMap[$subscriptionObject.Id] = $subscriptionObject.Name
}

# =====================================================================
# 役割割り当て結果を保存する配列を初期化
# =====================================================================
$allUserGroupRoleAssignments = @()

foreach ($azureSubscriptionId in $azureSubscriptionIdList) {
  Write-LogMessage "Processing subscription: $azureSubscriptionId"

  # 役割割り当てを取得(JSON)
  $roleAssignmentsJsonString = az role assignment list --all --subscription $azureSubscriptionId --output json
  if ($LASTEXITCODE -ne 0) {
    Write-LogMessage "Failed to retrieve role assignments: $azureSubscriptionId"
    continue
  }

  # JSON → オブジェクト変換 & User/Groupのみフィルタリング(Ownerのみ出力、管理グループ継承除外)
  $filteredUserGroupRoleAssignments = $roleAssignmentsJsonString | ConvertFrom-Json | Where-Object {
    $_.principalType -in @("User", "Group") -and
    $_.roleDefinitionName -eq "Owner" -and
    $_.scope -like "/subscriptions/*"
  }

  if ($filteredUserGroupRoleAssignments) {
    # SubscriptionId/NameおよびPrincipalObjectId情報を追加
    $enrichedUserGroupRoleAssignments = $filteredUserGroupRoleAssignments | ForEach-Object {
      # ScopeからSubscriptionIdを抽出: /subscriptions/<id>/...
      $regexMatch = [regex]::Match($_.scope, '^/subscriptions/([^/]+)')
      $subscriptionIdFromScope = if ($regexMatch.Success) { $regexMatch.Groups[1].Value } else { $null }

      [pscustomobject]@{
        SubscriptionName   = $azureSubscriptionIdToNameMap[$subscriptionIdFromScope]
        SubscriptionId     = $subscriptionIdFromScope
        PrincipalName      = $_.principalName
        PrincipalObjectId  = $_.principalId
        RoleDefinitionName = $_.roleDefinitionName
        Scope              = $_.scope
        PrincipalType      = $_.principalType
      }
    }

    # ログ出力用テーブルフォーマット
    $tableOutputForLog = $enrichedUserGroupRoleAssignments |
    Format-Table SubscriptionName, SubscriptionId, PrincipalObjectId, PrincipalName, RoleDefinitionName, Scope, PrincipalType -AutoSize |
    Out-String
    Write-LogMessage "User/Group role assignments:`n$tableOutputForLog"

    # CSV出力用に累積
    $allUserGroupRoleAssignments += $enrichedUserGroupRoleAssignments
  }
  else {
    Write-LogMessage "No User/Group role assignments: $azureSubscriptionId"
  }
}

# =====================================================================
# CSVファイルパス定義(ログと同じタイムスタンプを使用、JST基準)
# =====================================================================
$exportCsvFilePath = "RoleAssignments.$stamp.csv"

# =====================================================================
# 結果CSVのエクスポート
# =====================================================================
if ($allUserGroupRoleAssignments.Count -gt 0) {
  $allUserGroupRoleAssignments |
  Select-Object SubscriptionName, SubscriptionId, PrincipalName, PrincipalObjectId, RoleDefinitionName, Scope, PrincipalType |
  Export-Csv -Path $exportCsvFilePath -NoTypeInformation -Encoding UTF8

  Write-LogMessage "Exported to CSV: $exportCsvFilePath"
}
else {
  Write-LogMessage "No User/Group role assignments to export."
}

Get-UserGroup-NonOwner-RoleAssignments-Subscriptions.ps1

# =====================================================================
# 日本標準時(JST)タイムゾーン情報の準備
# =====================================================================
$jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")

# =====================================================================
# タイムスタンプ生成(CSV/LOG共通で使用、JST基準)
# =====================================================================
$stamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyyMMdd-HHmmss")

# =====================================================================
# ログファイルパス(タイムスタンプ付き)
# =====================================================================
$logFilePath = "RoleAssignments.$stamp.log"

# =====================================================================
# ログメッセージ作成関数(JST基準時間)
# =====================================================================
function Write-LogMessage {
  param ([string]$logMessage)
  $jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
  $timestamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyy-MM-dd HH:mm:ss")
  $logEntry = "$timestamp - $logMessage"
  Write-Host $logEntry
  Add-Content -Path $logFilePath -Value $logEntry
}

# =====================================================================
# サブスクリプション一覧の取得(Id & Name)およびマッピングテーブル作成
# =====================================================================
$azureSubscriptionsJsonString = az account list --query "[].{Id:id,Name:name}" -o json
if ($LASTEXITCODE -ne 0) {
  Write-LogMessage "Failed to retrieve subscription list."
  return
}
$azureSubscriptionObjects = $azureSubscriptionsJsonString | ConvertFrom-Json
$azureSubscriptionIdList = $azureSubscriptionObjects.Id

# サブスクリプションID → サブスクリプション名のマッピングテーブルを作成
$azureSubscriptionIdToNameMap = @{}
foreach ($subscriptionObject in $azureSubscriptionObjects) {
  $azureSubscriptionIdToNameMap[$subscriptionObject.Id] = $subscriptionObject.Name
}

# =====================================================================
# 役割割り当て結果を保存する配列を初期化
# =====================================================================
$allUserGroupRoleAssignments = @()

foreach ($azureSubscriptionId in $azureSubscriptionIdList) {
  Write-LogMessage "Processing subscription: $azureSubscriptionId"

  # 役割割り当てを取得(JSON)
  $roleAssignmentsJsonString = az role assignment list --all --subscription $azureSubscriptionId --output json
  if ($LASTEXITCODE -ne 0) {
    Write-LogMessage "Failed to retrieve role assignments: $azureSubscriptionId"
    continue
  }

  # JSON → オブジェクト変換 & User/Groupのみフィルタリング(Owner除外、管理グループ継承除外)
  $filteredUserGroupRoleAssignments = $roleAssignmentsJsonString | ConvertFrom-Json | Where-Object {
    $_.principalType -in @("User", "Group") -and
    $_.roleDefinitionName -ne "Owner" -and
    $_.scope -like "/subscriptions/*"
  }

  if ($filteredUserGroupRoleAssignments) {
    # SubscriptionId/NameおよびPrincipalObjectId情報を追加
    $enrichedUserGroupRoleAssignments = $filteredUserGroupRoleAssignments | ForEach-Object {
      # ScopeからSubscriptionIdを抽出: /subscriptions/<id>/...
      $regexMatch = [regex]::Match($_.scope, '^/subscriptions/([^/]+)')
      $subscriptionIdFromScope = if ($regexMatch.Success) { $regexMatch.Groups[1].Value } else { $null }

      [pscustomobject]@{
        SubscriptionName   = $azureSubscriptionIdToNameMap[$subscriptionIdFromScope]
        SubscriptionId     = $subscriptionIdFromScope
        PrincipalName      = $_.principalName
        PrincipalObjectId  = $_.principalId
        RoleDefinitionName = $_.roleDefinitionName
        Scope              = $_.scope
        PrincipalType      = $_.principalType
      }
    }

    # ログ出力用テーブルフォーマット
    $tableOutputForLog = $enrichedUserGroupRoleAssignments |
    Format-Table SubscriptionName, SubscriptionId, PrincipalObjectId, PrincipalName, RoleDefinitionName, Scope, PrincipalType -AutoSize |
    Out-String
    Write-LogMessage "User/Group role assignments:`n$tableOutputForLog"

    # CSV出力用に累積
    $allUserGroupRoleAssignments += $enrichedUserGroupRoleAssignments
  }
  else {
    Write-LogMessage "No User/Group role assignments: $azureSubscriptionId"
  }
}

# =====================================================================
# CSVファイルパス定義(ログと同じタイムスタンプを使用、JST基準)
# =====================================================================
$exportCsvFilePath = "RoleAssignments.$stamp.csv"

# =====================================================================
# 結果CSVのエクスポート
# =====================================================================
if ($allUserGroupRoleAssignments.Count -gt 0) {
  $allUserGroupRoleAssignments |
  Select-Object SubscriptionName, SubscriptionId, PrincipalName, PrincipalObjectId, RoleDefinitionName, Scope, PrincipalType |
  Export-Csv -Path $exportCsvFilePath -NoTypeInformation -Encoding UTF8

  Write-LogMessage "Exported to CSV: $exportCsvFilePath"
}
else {
  Write-LogMessage "No User/Group role assignments to export."
}

Get-UserGroup-NonOwner-RoleAssignments-Subscriptions-FromCSV.ps1

param(
  [Parameter(Mandatory = $true)]
  [string]$CsvFilePath  # 対象のSubscriptionIdリストが含まれるCSVパス(ヘッダーにSubscriptionId必須)
)

# =====================================================================
# タイムスタンプ生成(CSV/LOG共通で使用)
# =====================================================================
$jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
$stamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyyMMdd-HHmmss")

# =====================================================================
# ログファイルパス(タイムスタンプ付き)
# =====================================================================
$logFilePath = "RoleAssignments.$stamp.log"

# =====================================================================
# ログメッセージ作成関数(JST基準時間)
# =====================================================================
function Write-LogMessage {
  param ([string]$logMessage)
  $jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
  $timestamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyy-MM-dd HH:mm:ss")
  $logEntry = "$timestamp - $logMessage"
  Write-Host $logEntry
  Add-Content -Path $logFilePath -Value $logEntry
}

# =====================================================================
# 入力CSVの検証およびロード(SubscriptionIdを基準に収集)
# =====================================================================
if (-not (Test-Path $CsvFilePath)) {
  Write-LogMessage "CSV not found: $CsvFilePath"
  return
}
try {
  $csvRows = Import-Csv -Path $CsvFilePath
}
catch {
  Write-LogMessage "Failed to import CSV: $($_.Exception.Message)"
  return
}

if (-not $csvRows -or -not ($csvRows | Get-Member -Name SubscriptionId -MemberType NoteProperty)) {
  Write-LogMessage "CSV must contain a 'SubscriptionId' column."
  return
}

# CSVからSubscriptionIdのみを抽出(重複排除、空白/NULLを除外)
$azureSubscriptionIdList = $csvRows |
Where-Object { $_.SubscriptionId -and $_.SubscriptionId.Trim() -ne "" } |
ForEach-Object { $_.SubscriptionId.Trim() } |
Select-Object -Unique

if (-not $azureSubscriptionIdList -or $azureSubscriptionIdList.Count -eq 0) {
  Write-LogMessage "No SubscriptionId values found in CSV."
  return
}

# =====================================================================
# サブスクリプション一覧の取得(Id & Name)およびマッピングテーブル作成
#   - 名前のマッピングは可能な場合のみ使用(存在しない場合は空欄)
# =====================================================================
$azureSubscriptionsJsonString = az account list --query "[].{Id:id,Name:name}" -o json
if ($LASTEXITCODE -ne 0) {
  Write-LogMessage "Failed to retrieve subscription list."
  return
}
$azureSubscriptionObjects = $azureSubscriptionsJsonString | ConvertFrom-Json

# サブスクリプションID → サブスクリプション名のマッピングテーブルを作成
$azureSubscriptionIdToNameMap = @{}
foreach ($subscriptionObject in $azureSubscriptionObjects) {
  $azureSubscriptionIdToNameMap[$subscriptionObject.Id] = $subscriptionObject.Name
}

# =====================================================================
# 役割割り当て結果を保存する配列を初期化
# =====================================================================
$allUserGroupRoleAssignments = @()

foreach ($azureSubscriptionId in $azureSubscriptionIdList) {
  Write-LogMessage "Processing subscription from CSV: $azureSubscriptionId"

  # 役割割り当てを取得(JSON)
  $roleAssignmentsJsonString = az role assignment list --all --subscription $azureSubscriptionId --output json
  if ($LASTEXITCODE -ne 0) {
    Write-LogMessage "Failed to retrieve role assignments: $azureSubscriptionId"
    continue
  }

  # JSON → オブジェクト変換 & User/Groupのみフィルタリング(Owner除外、管理グループ継承除外)
  $filteredUserGroupRoleAssignments = $roleAssignmentsJsonString | ConvertFrom-Json | Where-Object {
    $_.principalType -in @("User", "Group") -and
    $_.roleDefinitionName -ne "Owner" -and
    $_.scope -like "/subscriptions/*"
  }

  if ($filteredUserGroupRoleAssignments) {
    # SubscriptionId/NameおよびPrincipalObjectId情報を追加
    $enrichedUserGroupRoleAssignments = $filteredUserGroupRoleAssignments | ForEach-Object {
      # ScopeからSubscriptionIdを抽出: /subscriptions/<id>/...
      $regexMatch = [regex]::Match($_.scope, '^/subscriptions/([^/]+)')
      $subscriptionIdFromScope = if ($regexMatch.Success) { $regexMatch.Groups[1].Value } else { $null }

      [pscustomobject]@{
        SubscriptionName   = if ($subscriptionIdFromScope -and $azureSubscriptionIdToNameMap.ContainsKey($subscriptionIdFromScope)) { $azureSubscriptionIdToNameMap[$subscriptionIdFromScope] } else { "" }
        SubscriptionId     = $subscriptionIdFromScope
        PrincipalName      = $_.principalName
        PrincipalObjectId  = $_.principalId
        RoleDefinitionName = $_.roleDefinitionName
        Scope              = $_.scope
        PrincipalType      = $_.principalType
      }
    }

    # ログ出力用テーブルフォーマット
    $tableOutputForLog = $enrichedUserGroupRoleAssignments |
    Format-Table SubscriptionName, SubscriptionId, PrincipalObjectId, PrincipalName, RoleDefinitionName, Scope, PrincipalType -AutoSize |
    Out-String
    Write-LogMessage "User/Group role assignments:`n$tableOutputForLog"

    # CSV出力用に累積
    $allUserGroupRoleAssignments += $enrichedUserGroupRoleAssignments
  }
  else {
    Write-LogMessage "No User/Group role assignments: $azureSubscriptionId"
  }
}

# =====================================================================
# CSVファイルパス定義(ログと同じタイムスタンプを使用、JST基準)
# =====================================================================
$exportCsvFilePath = "RoleAssignments.$stamp.csv"

# =====================================================================
# 結果CSVのエクスポート
# =====================================================================
if ($allUserGroupRoleAssignments.Count -gt 0) {
  $allUserGroupRoleAssignments |
  Select-Object SubscriptionName, SubscriptionId, PrincipalName, PrincipalObjectId, RoleDefinitionName, Scope, PrincipalType |
  Export-Csv -Path $exportCsvFilePath -NoTypeInformation -Encoding UTF8

  Write-LogMessage "Exported to CSV: $exportCsvFilePath"
}
else {
  Write-LogMessage "No User/Group role assignments to export."
}

Delete-RoleAssignments-FromCSV.ps1

param(
  [Parameter(Mandatory = $true)]
  [string]$CsvFilePath,   # 処理するCSVファイルのパス(必須、SubscriptionId/PrincipalObjectId/RoleDefinitionName/Scope 必須)

  [switch]$EnableWhatIf   # WhatIfモードスイッチ(trueの場合、実際には実行せずシミュレーションのみ)
)

# =====================================================================
# 日本標準時(JST)タイムゾーン情報の準備
# =====================================================================
$jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")

# =====================================================================
# タイムスタンプ生成およびログ/CSVファイルパス定義(JST基準)
# =====================================================================
$stamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyyMMdd-HHmmss")
$logFilePath = "RoleAssignments.delete.$stamp.log"

# =====================================================================
# ログメッセージ作成関数(JST基準時間)
# =====================================================================
function Write-LogMessage {
  param ([string]$Message)
  $jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
  $timestamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyy-MM-dd HH:mm:ss")
  $entry = "$timestamp - $Message"
  Write-Host $entry
  Add-Content -Path $logFilePath -Value $entry
}

# =====================================================================
# 結果CSVパス生成関数(全体/失敗専用、JSTタイムスタンプ使用)
# =====================================================================
function Get-ResultCsvPath {
  param([string]$SourceCsvPath)
  $jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
  $dir = Split-Path -Parent $SourceCsvPath
  if (-not $dir -or $dir -eq "") {
    $dir = $PSScriptRoot
  }
  $base = Split-Path -Leaf  $SourceCsvPath
  $name = [System.IO.Path]::GetFileNameWithoutExtension($base)
  $ext = [System.IO.Path]::GetExtension($base)
  $stamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyyMMdd-HHmmss")
  return (Join-Path $dir "$name.delete.with-results.$stamp$ext")
}
function Get-FailedCsvPath {
  param([string]$SourceCsvPath)
  $jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
  $dir = Split-Path -Parent $SourceCsvPath
  if (-not $dir -or $dir -eq "") {
    $dir = $PSScriptRoot
  }
  $base = Split-Path -Leaf  $SourceCsvPath
  $name = [System.IO.Path]::GetFileNameWithoutExtension($base)
  $ext = [System.IO.Path]::GetExtension($base)
  $stamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyyMMdd-HHmmss")
  return (Join-Path $dir "$name.delete.failed-only.$stamp$ext")
}

# =====================================================================
# 入力CSVのロード
# =====================================================================
# CsvFilePath引数が単純なファイル名の場合、スクリプトフォルダ基準で補正
if (-not (Split-Path -Parent $CsvFilePath)) {
  $CsvFilePath = Join-Path $PSScriptRoot $CsvFilePath
}

if (-not (Test-Path $CsvFilePath)) {
  throw "CSV file not found: $CsvFilePath"
}
$inputRows = Import-Csv -Path $CsvFilePath
if (-not $inputRows -or $inputRows.Count -eq 0) {
  throw "No rows to process in CSV: $CsvFilePath"
}

# [ADD] ----------------------------------------------------------------
# 実行ごとに保存ディレクトリを作成(CSVと同じフォルダ下に run.delete.<stamp>)
# 既存の変数/ロジックを変更せず、作成とパス再割り当てのみ実施
$__baseDir = Split-Path -Parent $CsvFilePath
if (-not $__baseDir -or $__baseDir -eq "") { $__baseDir = $PSScriptRoot }
$RunOutputDir = Join-Path $__baseDir ("run.delete." + $stamp)
New-Item -ItemType Directory -Path $RunOutputDir -Force | Out-Null

# ログファイルを保存ディレクトリに移動(パスのみ再指定、変数名/関数は変更なし)
$logFilePath = Join-Path $RunOutputDir (Split-Path -Leaf $logFilePath)
# ---------------------------------------------------------------------

# 必須カラムチェック
$required = @('SubscriptionId', 'PrincipalObjectId', 'RoleDefinitionName', 'Scope')
$missing = $required | Where-Object { $inputRows[0].PSObject.Properties.Name -notcontains $_ }
if ($missing.Count -gt 0) {
  throw "Missing required columns in CSV: $($missing -join ', ')"
}

# =====================================================================
# 結果CSVパスの準備
# =====================================================================
$resultCsvPath = Get-ResultCsvPath -SourceCsvPath $CsvFilePath
$failedCsvPath = Get-FailedCsvPath -SourceCsvPath $CsvFilePath

# [ADD] ----------------------------------------------------------------
# 結果CSVも保存ディレクトリに配置(ファイル名は既存ロジックを維持)
$resultCsvPath = Join-Path $RunOutputDir (Split-Path -Leaf $resultCsvPath)
$failedCsvPath = Join-Path $RunOutputDir (Split-Path -Leaf $failedCsvPath)

Write-LogMessage "Run output directory: $RunOutputDir"
# ---------------------------------------------------------------------

Write-LogMessage "Result CSV (all): $resultCsvPath"
Write-LogMessage "Result CSV (failed-only): $failedCsvPath"

# 結果累積配列(元のカラム+実行結果カラム)
$rowsWithResults = @()

# 進行度計算用
$totalRows = $inputRows.Count

# =====================================================================
# メイン処理ループ(CSV順に処理)— 進行度+リソースグループスコープ分岐
# =====================================================================
for ($i = 0; $i -lt $totalRows; $i++) {
  $row = $inputRows[$i]
  $rowIndex = $i + 1

  # 元のカラム
  $subscriptionId = $row.SubscriptionId
  $PrincipalObjectId = $row.PrincipalObjectId
  $PrincipalName = $row.PrincipalName
  $roleDefinitionName = $row.RoleDefinitionName
  $scope = $row.Scope

  # 実行メタデータ
  $executedAt = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("o")  # JST, ISO8601
  $startTime = Get-Date  # 経過時間計算用(UTC/JSTは不問)

  # スコープがリソースグループの場合 --resource-group を使用
  # 例: /subscriptions/<subId>/resourceGroups/<rgName>
  $useRgParam = $false
  $rgName = $null
  if ($scope -match '^/subscriptions/[^/]+/resourceGroups/([^/]+)/*$') {
    $useRgParam = $true
    $rgName = $Matches[1]
  }

  # 実行するAzure CLIコマンド
  if ($useRgParam -and $rgName) {
    # リソースグループ範囲
    $commandToRun = "az role assignment delete --assignee `"$PrincipalObjectId`" --role `"$roleDefinitionName`" --resource-group `"$rgName`" --subscription $subscriptionId"
  }
  else {
    # サブスクリプション/リソース範囲(既存方式)
    $commandToRun = "az role assignment delete --assignee `"$PrincipalObjectId`" --role `"$roleDefinitionName`" --scope `"$scope`" --subscription $subscriptionId"
  }

  $stdAll = $null
  $exitCode = $null
  $result = $null

  if ($EnableWhatIf) {
    # WhatIfモード:実際には実行しない
    $result = "WhatIf"
    $exitCode = ""
    $stdAll = "[WhatIf] Command not executed."
    Write-LogMessage "[WhatIf] $commandToRun"
  }
  else {
    # 実際に実行:標準出力+エラーをすべてキャプチャ
    $stdAll = Invoke-Expression "$commandToRun 2>&1"
    $exitCode = $LASTEXITCODE

    if ($exitCode -eq 0) {
      $result = "Success"
      Write-LogMessage "Processing Row [$rowIndex/$totalRows] DELETE succeeded: $PrincipalName / $roleDefinitionName / $scope"
    }
    else {
      $result = "Failed"
      Write-LogMessage "Processing Row [$rowIndex/$totalRows] DELETE failed (ExitCode=$exitCode): $PrincipalName / $roleDefinitionName / $scope"
    }
  }

  # 実行時間(ms)
  $durationMs = [int]((Get-Date) - $startTime).TotalMilliseconds

  # 元の行+実行結果カラムを結合した新しいオブジェクトを作成
  $rowWithResult = [pscustomobject]@{}
  foreach ($col in $row.PSObject.Properties.Name) {
    $rowWithResult | Add-Member -NotePropertyName $col -NotePropertyValue $row.$col
  }
  $rowWithResult | Add-Member -NotePropertyName ExecutedAt  -NotePropertyValue $executedAt
  $rowWithResult | Add-Member -NotePropertyName DurationMs  -NotePropertyValue $durationMs
  $rowWithResult | Add-Member -NotePropertyName Result      -NotePropertyValue $result
  $rowWithResult | Add-Member -NotePropertyName ExitCode    -NotePropertyValue $exitCode
  $rowWithResult | Add-Member -NotePropertyName CommandLine -NotePropertyValue $commandToRun
  $rowWithResult | Add-Member -NotePropertyName StdAll      -NotePropertyValue ($stdAll -join "`n")

  # 蓄積
  $rowsWithResults += $rowWithResult
}

# =====================================================================
# 結果CSVの保存(全体+失敗のみ)
# =====================================================================
# 1) 全体結果
$rowsWithResults | Export-Csv -Path $resultCsvPath -NoTypeInformation -Encoding UTF8
Write-LogMessage "Saved result CSV (all): $resultCsvPath"

# 2) 失敗のみ
$failedRows = $rowsWithResults | Where-Object { $_.Result -eq 'Failed' }
if ($failedRows -and $failedRows.Count -gt 0) {
  $failedRows | Export-Csv -Path $failedCsvPath -NoTypeInformation -Encoding UTF8
  Write-LogMessage "Saved result CSV (failed-only): $failedCsvPath (count: $($failedRows.Count))"
}
else {
  Write-LogMessage "No failed rows. Skipping failed-only CSV."
}

Backout-RoleAssignments-FromCSV.ps1

param(
  [Parameter(Mandatory = $true)]
  [string]$CsvFilePath,   # 処理するCSVファイルのパス(必須、SubscriptionId/PrincipalObjectId/RoleDefinitionName/Scope 必須)

  [switch]$EnableWhatIf   # WhatIfモードスイッチ(trueの場合、実際には実行せずシミュレーションのみ)
)

# =====================================================================
# 日本標準時(JST)タイムゾーン情報の準備
# =====================================================================
$jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")

# =====================================================================
# タイムスタンプ生成およびログ/CSVファイルパス定義(JST基準)
# =====================================================================
$stamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyyMMdd-HHmmss")
$logFilePath = "RoleAssignments.backout.$stamp.log"

# =====================================================================
# ログメッセージ作成関数(JST基準時間)
# =====================================================================
function Write-LogMessage {
  param ([string]$Message)
  $jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
  $timestamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyy-MM-dd HH:mm:ss")
  $entry = "$timestamp - $Message"
  Write-Host $entry
  Add-Content -Path $logFilePath -Value $entry
}

# =====================================================================
# 結果CSVパス生成関数(全体/失敗専用、JSTタイムスタンプ使用)
# =====================================================================
function Get-ResultCsvPath {
  param([string]$SourceCsvPath)
  $jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
  $dir = Split-Path -Parent $SourceCsvPath
  if (-not $dir -or $dir -eq "") {
    $dir = $PSScriptRoot
  }
  $base = Split-Path -Leaf  $SourceCsvPath
  $name = [System.IO.Path]::GetFileNameWithoutExtension($base)
  $ext = [System.IO.Path]::GetExtension($base)
  $stamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyyMMdd-HHmmss")
  return (Join-Path $dir "$name.backout.with-results.$stamp$ext")
}
function Get-FailedCsvPath {
  param([string]$SourceCsvPath)
  $jpTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById("Tokyo Standard Time")
  $dir = Split-Path -Parent $SourceCsvPath
  if (-not $dir -or $dir -eq "") {
    $dir = $PSScriptRoot
  }
  $base = Split-Path -Leaf  $SourceCsvPath
  $name = [System.IO.Path]::GetFileNameWithoutExtension($base)
  $ext = [System.IO.Path]::GetExtension($base)
  $stamp = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("yyyyMMdd-HHmmss")
  return (Join-Path $dir "$name.backout.failed-only.$stamp$ext")
}

# =====================================================================
# 入力CSVのロード
# =====================================================================
# CsvFilePath引数が単純なファイル名の場合、スクリプトフォルダ基準で補正
if (-not (Split-Path -Parent $CsvFilePath)) {
  $CsvFilePath = Join-Path $PSScriptRoot $CsvFilePath
}
if (-not (Test-Path $CsvFilePath)) {
  throw "CSV file not found: $CsvFilePath"
}
$inputRows = Import-Csv -Path $CsvFilePath
if (-not $inputRows -or $inputRows.Count -eq 0) {
  throw "No rows to process in CSV: $CsvFilePath"
}

# [ADD] ----------------------------------------------------------------
# 実行ごとに保存ディレクトリを作成(CSVと同じフォルダ下に run.backout.<stamp>)
$__baseDir = Split-Path -Parent $CsvFilePath
if (-not $__baseDir -or $__baseDir -eq "") { $__baseDir = $PSScriptRoot }
$RunOutputDir = Join-Path $__baseDir ("run.backout." + $stamp)
New-Item -ItemType Directory -Path $RunOutputDir -Force | Out-Null

# ログファイルを保存ディレクトリに移動(パスのみ再指定、変数名/関数は変更なし)
$logFilePath = Join-Path $RunOutputDir (Split-Path -Leaf $logFilePath)
# ---------------------------------------------------------------------

# 必須カラムチェック
$required = @('SubscriptionId', 'PrincipalObjectId', 'RoleDefinitionName', 'Scope')
$missing = $required | Where-Object { $inputRows[0].PSObject.Properties.Name -notcontains $_ }
if ($missing.Count -gt 0) {
  throw "Missing required columns in CSV: $($missing -join ', ')"
}

# =====================================================================
# 結果CSVパスの準備
# =====================================================================
$resultCsvPath = Get-ResultCsvPath -SourceCsvPath $CsvFilePath
$failedCsvPath = Get-FailedCsvPath -SourceCsvPath $CsvFilePath

# [ADD] ----------------------------------------------------------------
# 結果CSVも保存ディレクトリに配置(ファイル名は既存ロジックを維持)
$resultCsvPath = Join-Path $RunOutputDir (Split-Path -Leaf $resultCsvPath)
$failedCsvPath = Join-Path $RunOutputDir (Split-Path -Leaf $failedCsvPath)

Write-LogMessage "Run output directory: $RunOutputDir"
# ---------------------------------------------------------------------

Write-LogMessage "Result CSV (all): $resultCsvPath"
Write-LogMessage "Result CSV (failed-only): $failedCsvPath"

# 結果累積配列(元のカラム+実行結果カラム)
$rowsWithResults = @()

# 進行度計算用
$totalRows = $inputRows.Count

# =====================================================================
# メイン処理ループ(CSV順に処理)— 進行度+リソースグループスコープ分岐
# =====================================================================
for ($i = 0; $i -lt $totalRows; $i++) {
  $row = $inputRows[$i]
  $rowIndex = $i + 1

  # 元のカラム
  $subscriptionId = $row.SubscriptionId
  $PrincipalObjectId = $row.PrincipalObjectId
  $PrincipalName = $row.PrincipalName
  $roleDefinitionName = $row.RoleDefinitionName
  $scope = $row.Scope

  # 実行メタデータ
  $executedAt = [System.TimeZoneInfo]::ConvertTime((Get-Date), $jpTimeZone).ToString("o")  # JST, ISO8601
  $startTime = Get-Date  # 経過時間計算用

  # スコープがリソースグループの場合 --resource-group を使用(削除スクリプトと同様)
  # 例: /subscriptions/<subId>/resourceGroups/<rgName>
  $useRgParam = $false
  $rgName = $null
  if ($scope -match '^/subscriptions/[^/]+/resourceGroups/([^/]+)/*$') {
    $useRgParam = $true
    $rgName = $Matches[1]
  }

  # 実行するAzure CLIコマンド(削除スクリプトと同じスコープ分岐/形式)
  if ($useRgParam -and $rgName) {
    # リソースグループ範囲
    $commandToRun = "az role assignment create --assignee `"$PrincipalObjectId`" --role `"$roleDefinitionName`" --resource-group `"$rgName`" --subscription $subscriptionId"
  }
  else {
    # サブスクリプション/リソース範囲
    $commandToRun = "az role assignment create --assignee `"$PrincipalObjectId`" --role `"$roleDefinitionName`" --scope `"$scope`" --subscription $subscriptionId"
  }

  $stdAll = $null
  $exitCode = $null
  $result = $null

  if ($EnableWhatIf) {
    # WhatIfモード:実際には実行しない
    $result = "WhatIf"
    $exitCode = ""
    $stdAll = "[WhatIf] Command not executed."
    Write-LogMessage "[WhatIf] $commandToRun"
  }
  else {
    # 実際に実行:標準出力+エラーをすべてキャプチャ
    $stdAll = Invoke-Expression "$commandToRun 2>&1"
    $exitCode = $LASTEXITCODE

    if ($exitCode -eq 0) {
      $result = "Success"
      Write-LogMessage "Processing Row [$rowIndex/$totalRows] CREATE succeeded: $PrincipalName | $roleDefinitionName | $scope"
    }
    else {
      $result = "Failed"
      Write-LogMessage "Processing Row [$rowIndex/$totalRows] CREATE failed (ExitCode=$exitCode): $PrincipalName | $roleDefinitionName | $scope"
    }
  }

  # 実行時間(ms)
  $durationMs = [int]((Get-Date) - $startTime).TotalMilliseconds

  # 元の行+実行結果カラムを結合した新しいオブジェクトを作成
  $rowWithResult = [pscustomobject]@{}
  foreach ($col in $row.PSObject.Properties.Name) {
    $rowWithResult | Add-Member -NotePropertyName $col -NotePropertyValue $row.$col
  }
  $rowWithResult | Add-Member -NotePropertyName ExecutedAt  -NotePropertyValue $executedAt
  $rowWithResult | Add-Member -NotePropertyName DurationMs  -NotePropertyValue $durationMs
  $rowWithResult | Add-Member -NotePropertyName Result      -NotePropertyValue $result
  $rowWithResult | Add-Member -NotePropertyName ExitCode    -NotePropertyValue $exitCode
  $rowWithResult | Add-Member -NotePropertyName CommandLine -NotePropertyValue $commandToRun
  $rowWithResult | Add-Member -NotePropertyName StdAll      -NotePropertyValue ($stdAll -join "`n")

  # 蓄積
  $rowsWithResults += $rowWithResult
}

# =====================================================================
# 結果CSVの保存(全体+失敗のみ)
# =====================================================================
# 1) 全体結果
$rowsWithResults | Export-Csv -Path $resultCsvPath -NoTypeInformation -Encoding UTF8
Write-LogMessage "Saved result CSV (all): $resultCsvPath"

# 2) 失敗のみ
$failedRows = $rowsWithResults | Where-Object { $_.Result -eq 'Failed' }
if ($failedRows -and $failedRows.Count -gt 0) {
  $failedRows | Export-Csv -Path $failedCsvPath -NoTypeInformation -Encoding UTF8
  Write-LogMessage "Saved result CSV (failed-only): $failedCsvPath (count: $($failedRows.Count))"
}
else {
  Write-LogMessage "No failed rows. Skipping failed-only CSV."
}