final.ps1


<#PSScriptInfo
 
.VERSION 1.0.0
 
.GUID bc0e9d07-193e-440a-97ca-590cc75bd806
 
.AUTHOR Rahul Acharya
 
.COMPANYNAME
 
.COPYRIGHT
 
.TAGS
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
 
.PRIVATEDATA
 
#>


<#
 
.DESCRIPTION
 This script manages users and groups in Microsoft 365 Groups by allowing creation, updates, and deletion.
 
#>
 

Param()


<#PSScriptInfo
.VERSION='1.0.0'
.AUTHOR = 'Rahul Acharya'
.COMPANYNAME = 'Your Company Name'
.COPYRIGHT = 'Copyright 2025, Rahul Acharya'
.TAGS = 'PowerShell', 'Microsoft365', 'Groups'
.GUID='8e2f5f4d-3b3a-4f8b-987f-12c64b06f3e6'
.RELEASENOTES = 'Initial version.'
.DESCRIPTION = 'This script manages users and groups in Microsoft 365 Groups by allowing creation, updates, and deletion.'
#>



# Import Excel Module
Import-Module ImportExcel -ErrorAction Stop

# Azure AD app registration details
$ClientId = "ed2ecbd5-8bf4-4d34-8085-c66e7f9f36fc"
$TenantId = "4ab02f96-fd79-417b-9642-9b5fd0a15eeb"

# Get the access token
$AccessToken = Get-AccessToken -ClientId $ClientId -TenantId $TenantId

function Get-AccessToken {
    param (
        [Parameter(Mandatory = $true)]
        [string]$ClientId,

        [Parameter(Mandatory = $true)]
        [string]$TenantId
    )

    # Define the scope for Microsoft Graph
    $Scopes = @("https://graph.microsoft.com/.default")

    # Get the token using MSAL.PS
    $TokenResponse = Get-MsalToken -ClientId $ClientId -TenantId $TenantId -RedirectUri "http://localhost/xyzurl" -Scopes $Scopes -Interactive
    return $TokenResponse.AccessToken
}



$ErrorFilePath =  "C:\Users\rahulsachin1\Desktop\PSExcel\DL_Creation_ErrorLogs.txt"

function Log_Message {
    param (
        [string]$Message,
        [string]$LogFilePath = "C:\Users\rahulsachin1\Desktop\PSExcel\DL_Creation_Logs.txt" # Default log file path
    )
    # Get the current timestamp
    $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    # Format the log entry
    $LogEntry = "$Timestamp - $Message"
    # Write to console
    Write-Host $LogEntry
    # Append to log file
    Add-Content -Path $LogFilePath -Value $LogEntry
}

function Error_Log_Message {
    param (
        [string]$Message
    )
    # Get the current timestamp
    $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    # Format the log entry
    $LogEntry = "$Timestamp - $Message"
    # Append to log file
    Add-Content -Path $ErrorFilePath -Value $LogEntry
}

# Function to get Azure AD Users using Graph API
function Get-AzureADUsers {
    param (
        [string]$AccessToken
    )

    $graphApiUrl = "https://graph.microsoft.com/v1.0/users?`$select=displayName,userPrincipalName,id,department,jobTitle&`$top=999"
    $response = Invoke-RestMethod -Uri $graphApiUrl -Headers @{ Authorization = "Bearer $AccessToken" } -Method Get
    return $response.value
}

function deleteUser {
    param (
        [string]$UserId
    )
    # Headers
    $Headers = @{
        Authorization = "Bearer $AccessToken"
        "Content-Type" = "application/json"
    }

    # Construct the request URL
    $RequestUrl = "https://graph.microsoft.com/v1.0/users/$UserId"

    # Delete the user
    try {
        Invoke-RestMethod -Uri $RequestUrl -Method Delete -Headers $Headers

        # Output success message
        Log_Message "User with ID '$UserId' deleted successfully."
    } catch {
        Error_Log_Message "Failed to delete user: $_"
    }
    
}

function restoreUser {
    param (
        [string]$UserId
    )
    # Restore the deleted user by userId
    $restoreUrl = "https://graph.microsoft.com/v1.0/directory/deletedItems/microsoft.graph.user/$UserId/restore"
    try {
        $response = Invoke-RestMethod -Method Post -Uri $restoreUrl -Headers @{
            Authorization = "Bearer $AccessToken"
            "Content-Type" = "application/json"
        }
        Log_Message "User restored successfully! User ID: $($response.id)" -ForegroundColor Green
    } catch {
        Error_Log_Message "Error restoring user: $($_.Exception.Message)" -ForegroundColor Red
    }
}

# Function to update any existing User
function updateUser {
    param (
        [string]$userId,
        [string]$newdisplayName,
        [string]$newjobTitle,
        [string]$newDepartmentName
    )
    if($newDepartmentName -eq ""){
        $newDepartment = $null
    }
    if($newjobTitle -eq ""){
        $newJob = $null
    }
    # Updated property
    $UpdatedProperties = @{
        displayName = $newdisplayName
        jobTitle = $newJob
        department = $newDepartment
    }
    # Convert updated properties to JSON
    $Body = $UpdatedProperties | ConvertTo-Json -Depth 10 -Compress

    # Headers
    $Headers = @{
        Authorization = "Bearer $AccessToken"
        "Content-Type" = "application/json"
    }

    # Construct the request URL
    $RequestUrl = "https://graph.microsoft.com/v1.0/users/$UserId"

    # Update the user's department
    try {
        $Response = Invoke-RestMethod -Uri $RequestUrl -Method Patch -Headers $Headers -Body $Body

        # Output success message
        Log_Message "User department updated successfully."
        $Response | ConvertTo-Json -Depth 10 | Write-Output
    } catch {
        Error_Log_Message "Failed to update user's department: $_"
    }
}

function Update-AzureAD{
    # Define the Excel file path
    $excelFilePath = ".\UserDetails.xlsx"

    # Get Azure AD users
    $azureADUsers = Get-AzureADUsers -AccessToken $AccessToken

    $excelData = Import-Excel -Path $excelFilePath -WorksheetName "AzureADUsers"

    foreach ($user in $excelData){
        
        $matchingUser = $azureADUsers | Where-Object { $_.id -eq $user.id }
        if($user.is_active -eq 0){
            if($matchingUser){
                deleteUser -UserId $($user.id)
                continue
            }else{
                continue
            }
        }
        if ($matchingUser) {
            $newDepartment = $user.department
            updateUser -UserId $($user.id) -newdisplayName $($user.displayName) -newjobTitle $($user.jobTitle) -newDepartmentName $newDepartment
        }else{
            if(($user.id).Length -eq 36){
                restoreUser -UserId $($user.id)
                $newDepartment = $user.department
                updateUser -UserId $($user.id) -newdisplayName $($user.displayName) -newjobTitle $($user.jobTitle) -newDepartmentName $newDepartment
            }else{
                Log_Message "Add a new User."
            }
        }
    }
}

function Update-Excel{
    # Define the Excel file path
    $excelFilePath = ".\UserDetails.xlsx"
    # Get Azure AD users
    $azureADUsers = Get-AzureADUsers -AccessToken $AccessToken

    # Read the Excel file
    if (-Not (Test-Path $excelFilePath)) {
        Log_Message "Excel file not found. Creating a new file."
        $excelData = @()
        Export-Excel -Path $excelFilePath -WorksheetName "AzureADUsers" -Data $excelData -AutoSize
    }

    $excelData = Import-Excel -Path $excelFilePath -WorksheetName "AzureADUsers"

    # Check for the 'is_active' column in the Excel data
    if (-Not ($excelData -and $excelData[0].PSObject.Properties.Name -contains 'is_active')) {
        $excelData | ForEach-Object {
            $_ | Add-Member -MemberType NoteProperty -Name "is_active" -Value $null
        }
    }

    # Process the users
    $updatedData = @()

    foreach ($user in $azureADUsers) {
        $matchingUser = $excelData | Where-Object { $_.id -eq $user.id }
        if ($matchingUser) {
            # Update is_active column for existing users
            $matchingUser.is_active = 1
            $matchingUser.displayName = $user.displayName
            if($null -eq $user.department){
                $matchingUser.department = ""
            }else{
                $matchingUser.department = $user.department
            }
            $updatedData += $matchingUser
        } else {
            # Add new user
            $updatedData += [PSCustomObject]@{
                id                = $user.id
                displayName       = $user.displayName
                userPrincipalName = $user.userPrincipalName
                department        = $user.department
                jobTitle          = $user.jobTitle
                is_active         = 1
            }
        }
    }

    foreach ($user in $excelData){
        $matchingUser = $azureADUsers | Where-Object { $_.id -eq $user.id }
        if ($matchingUser) {
            continue
        }else{
            $user.is_active = 0
            $updatedData += $user
        }
    }

    # Export the sorted data back to the Excel file
    $sortedData = $updatedData | Sort-Object -Property displayName
    $sortedData | Export-Excel -Path $excelFilePath -WorksheetName "AzureADUsers" -AutoSize

    Log_Message "User details have been updated in the Excel file: $excelFilePath"
}

function Get-ExcelLastUpdatedTime {
    param (
        [Parameter(Mandatory = $true)]
        [string]$FilePath  # Path to the Excel file
    )

    try {
        # Check if the file exists
        if (-not (Test-Path -Path $FilePath)) {
            throw [System.IO.FileNotFoundException]::new("File not found: $FilePath")
        }

        # Get the file properties
        $file = Get-Item -Path $FilePath

        # Retrieve the LastWriteTime property
        $lastUpdatedTime = $file.LastWriteTime

        # Format the date and time as MM/dd/yyyy HH:mm:ss
        $formattedTime = $lastUpdatedTime.ToString("MM'/'dd'/'yyyy HH:mm:ss")
        return $formattedTime
    }
    catch {
        # Handle errors
        Write-Error "An error occurred: $_"
    }
}

function auditLogs {
    param (
        [Parameter(Mandatory = $true)]
        [string]$AccessToken,  # Access Token
        
        [Parameter(Mandatory = $true)]
        [string]$UserPrincipalName  # User Principal Name to filter logs
    )
    
    # Base URL for audit logs
    $baseUri = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits"

    # Define query parameters
    $queryParams = "?`$filter=result eq 'Success' and initiatedBy/user/userPrincipalName eq '$UserPrincipalName' and category eq 'UserManagement'&`$orderby=activityDateTime desc"

    # Full URL with query parameters
    $uri = "$baseUri$queryParams"

    # Define headers with the access token
    $headers = @{
        "Authorization" = "Bearer $AccessToken"
        "Content-Type"  = "application/json"
    }

    # Make the API call
    try {
        $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get -ContentType "application/json"
        
        if ($response.value) {
            foreach ($log in $response.value) {
                $time = $($log.activityDateTime)
                $actualTime = $time.ToLocalTime()
                return $actualTime
            }
        } else {
            Log_Message "No audit logs found for the specified user with 'Success' status."
        }
    } catch {
        Error_Log_Message "Error occurred while fetching audit logs: $_"
    }
}

function Get-GraphGroups {
    param (
        [string]$AccessToken
    )

    # Set API endpoint for Microsoft Graph groups
    $graphApiEndpoint = "https://graph.microsoft.com/v1.0/groups"

    # Initialize an array to store group details
    $groupDetails = @()

    # Fetch groups using the Microsoft Graph API
    try {
        $response = Invoke-RestMethod -Uri $graphApiEndpoint -Headers @{Authorization = "Bearer $AccessToken"} -Method Get
        
        # Process each group and store the display name and ID
        foreach ($group in $response.value) {
            $groupDetails += [PSCustomObject]@{
                DisplayName = $group.displayName
                GroupId     = $group.id
            }
        }

        # Store group details in a variable for further use
        $GroupsData = $groupDetails
        Log_Message "Group details successfully stored in variable 'GroupsData'"
        return $GroupsData
    } catch {
        Error_Log_Message "An error occurred: $_"
        return $null
    }
}

function NewGroup {
    param (
        [string]$AccessToken,
        [hashtable]$GroupDetails
    )

    # Set API endpoint for creating the group
    $graphApiEndpoint = "https://graph.microsoft.com/v1.0/groups"

    # Convert group details to JSON
    $groupDetailsJson = $GroupDetails | ConvertTo-Json -Depth 10 -Compress

    # Create the group using the Microsoft Graph API
    try {
        $response = Invoke-RestMethod -Uri $graphApiEndpoint -Headers @{Authorization = "Bearer $AccessToken"} -Body $groupDetailsJson -ContentType "application/json" -Method Post
        
        # Output the created group's details
        Log_Message "Group created successfully!"
        Log_Message "Group ID: $($response.id)"
        Log_Message "Group Display Name: $($response.displayName)"
        Log_Message "Group Mail Nickname: $($response.mailNickname)"
        return $response
    } catch {
        Error_Log_Message "An error occurred: $_"
        return $null
    }
}

# Function to get Group ID by department name
function Get-GroupIdByName {
    param (
        [string]$departmentName,
        [array]$MatchingData
    )

    foreach ($entry in $MatchingData) {
        if ($entry.Department -eq $DepartmentName) {
            return $entry.GroupId
        }
    }
    return $null # Return null if the department is not found
}

# Function to add a user to a group in Azure AD
function Add-UserToAzureADGroup {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$AccessToken,  # OAuth token from MSAL authentication

        [Parameter(Mandatory = $true)]
        [string]$GroupId,      # Azure AD Group ID

        [Parameter(Mandatory = $true)]
        [string]$UserId        # Azure AD User ID
    )
    # Microsoft Graph API endpoint to add a member to a group
    $GraphUri = "https://graph.microsoft.com/v1.0/groups/{$GroupId}/members/`$ref"

    # Define the request body
    $Body = @{
        '@odata.id' = "https://graph.microsoft.com/v1.0/directoryObjects/{$UserId}"
    } | ConvertTo-Json -Depth 10

    # Set the headers with the access token
    $Headers = @{
        "Authorization" = "Bearer $AccessToken"
        "Content-Type"  = "application/json"
    }

    # Make the API call
    try {
        Invoke-RestMethod -Method Post -Uri $GraphUri -Body $Body -Headers $Headers
        Log_Message "User with ID $UserId added to Group with ID $GroupId successfully."
    } catch {
        Error_Log_Message "An error occurred: $_"
    }
}

# Function to check if user is in the group
function UserInGroup {
    param (
        [string]$AccessToken,
        [string]$UserId,
        [string]$GroupId
    )

    # API Endpoint
    $graphApiEndpoint = "https://graph.microsoft.com/v1.0/groups/$GroupId/members/`$ref"

    try {
        $response = Invoke-RestMethod -Uri $graphApiEndpoint -Headers @{Authorization = "Bearer $AccessToken"} -Method Get
        foreach ($member in $response.value) {
            if ($member.id -eq $UserId) {
                return $true
            }
        }
        return $false
    } catch {
        Error_Log_Message "An error occurred while checking user membership: $_"
        return $false
    }
}

function Remove-UserFromGroup {
    param (
        [string]$AccessToken,  # Access token for authentication
        [string]$UserId,       # ID of the user to remove
        [string]$GroupId       # ID of the group to remove the user from
    )

    # Define the URI for the API request to remove the user from the group
    $uri = "https://graph.microsoft.com/v1.0/groups/$GroupId/members/$UserId/`$ref"

    # Send the DELETE request to remove the user from the group
    try {
        Invoke-RestMethod -Uri $uri -Headers @{Authorization = "Bearer $AccessToken"} -Method Delete
        Log_Message "Successfully removed user $UserId from group $GroupId."
    } catch {
    }
}

function Remove-UserFromGroupIfNotMatched {
    param (
        [string]$AccessToken,
        [string]$UserId,
        [string]$GroupId,
        [array]$MatchingData  # Array containing matching group IDs
    )
    foreach ($entry in $MatchingData){
        # Check if the GroupId matches any entry in the MatchingData array
        $currGroupID = $entry.GroupId
        if ($currGroupID -eq $GroupId) {
            Log_Message "GroupId $GroupId matches an entry in the MatchingData array. No action needed."
        } else {
            Remove-UserFromGroup -AccessToken $AccessToken -UserId $UserId -GroupId $currGroupID
        }
    }
}

function Process_DistributionLists {
    Log_Message "Processing DL"
    # Define the file path for the Excel data
    $ExcelFilePath = ".\UserDetails.xlsx"

    # Import the data from the Excel file
    $EmployeeData = Import-Excel -Path $ExcelFilePath

    # Get all the department and member details from the excel
    $Departments = $EmployeeData | Select-Object -ExpandProperty department | Sort-Object -Unique

    # Initialize $MatchingData as an empty array
    $MatchingData = @()

    foreach ($Department in $Departments) {
        $GroupsData = Get-GraphGroups -AccessToken $AccessToken
        $GroupFound = $false

        foreach ($Group in $GroupsData) {
            if ($Group.DisplayName -like "*$Department*") {
                $MatchingData += [PSCustomObject]@{
                    Department = $Department
                    GroupName  = $Group.DisplayName
                    GroupId    = $Group.GroupId
                }
                $GroupFound = $true
                break
            }
        }

        if (-not $GroupFound) {
            $NewGroupDetails = @{
                displayName     = $Department
                mailNickname    = $Department.ToLower() -replace "\s", ""
                mailEnabled     = $true
                securityEnabled = $false
                groupTypes      = @("Unified")
            }
            $NewGroupResponse = NewGroup -AccessToken $AccessToken -GroupDetails $NewGroupDetails
            if ($NewGroupResponse) {
                $MatchingData += [PSCustomObject]@{
                    Department = $Department
                    GroupName  = $NewGroupResponse.displayName
                    GroupId    = $NewGroupResponse.id
                }
            }
        }
    }

    Log_Message $MatchingData

    # Iterate through the Excel data
    foreach ($row in $EmployeeData) {
        if($row.is_active -eq 1){
            $UserId = $row.id
            $departmentName = $row.department

            # Check if departmentName is null or empty before calling the function
            if (![string]::IsNullOrEmpty($departmentName)) {
                # Get group ID
                $GroupId = Get-GroupIdByName -AccessToken $AccessToken -departmentName $departmentName -MatchingData $MatchingData
                #Log_Message $GroupId
                if ($GroupId) {
                    Log_Message "User $UserId needs to be added to group $GroupId"
                    # Check if the user is in the group
                    $isInGroup = UserInGroup -AccessToken $AccessToken -UserId $UserId -GroupId $GroupId
                    if (-not $isInGroup) {
                        # Add the user to the group
                        Add-UserToAzureADGroup -AccessToken $AccessToken -GroupId $GroupId -UserId $UserId
                        # Call the function
                        Remove-UserFromGroupIfNotMatched -AccessToken $AccessToken -UserId $UserId -GroupId $GroupId -MatchingData $MatchingData
                    }
                }
            } else {
                Log_Message "Department is null or empty for user $UserId"
            }
        }
    }

}

# Infinite loop to run the script every 10 minutes
while ($true) {
    try {
        # Call the auditLogs function with the desired user and access token
        $azureADtime = (auditLogs -AccessToken $AccessToken -UserPrincipalName "sachinrahul@sachinrahul.onmicrosoft.com")

        $excelFilePath = ".\UserDetails.xlsx"
        $excelTime = [datetime](Get-ExcelLastUpdatedTime -FilePath $excelFilePath)
        
        if($excelTime -gt $azureADtime){
            Log_Message "Excel is updated now update Azure AD"
            Update-AzureAD
            Process_DistributionLists
        }else{
            Log_Message "Azure AD is updated last."
            Update-Excel
            Process_DistributionLists
        }
    } catch {
        Error_Log_Message "An error occurred: $_"
    }
    Log_Message "Script execution completed. Waiting for the next run."
    Start-Sleep -Seconds 60 # 1 minutes
}