diff --git a/app-google-drive/google-drive-install.ps1 b/app-google-drive/google-drive-install.ps1 new file mode 100644 index 0000000..9eabb3b --- /dev/null +++ b/app-google-drive/google-drive-install.ps1 @@ -0,0 +1,112 @@ +## PLEASE COMMENT YOUR VARIABLES DIRECTLY BELOW HERE IF YOU'RE RUNNING FROM A RMM +## No additional variables required for this script + +# Getting input from user if not running from RMM else set variables from RMM. + +$ScriptLogName = "google-drive-install.log" + +if ($RMM -ne 1) { + $ValidInput = 0 + while ($ValidInput -ne 1) { + $Description = Read-Host "Please enter the ticket # and, or your initials. Its used as the Description for the job" + if ($Description) { + $ValidInput = 1 + } else { + Write-Host "Invalid input. Please try again." + } + } + $LogPath = "$ENV:WINDIR\logs\$ScriptLogName" + +} else { + if ($null -ne $RMMScriptPath) { + $LogPath = "$RMMScriptPath\logs\$ScriptLogName" + } else { + $LogPath = "$ENV:WINDIR\logs\$ScriptLogName" + } + + if ($null -eq $Description) { + Write-Host "Description is null. This was most likely run automatically from the RMM and no information was passed." + $Description = "No Description" + } +} + +# Script Logic + +Start-Transcript -Path $LogPath + +Write-Host "Description: $Description" +Write-Host "Log path: $LogPath" +Write-Host "RMM: $RMM" + +$packageId = "Google.GoogleDrive" +$installerUrl = "https://dl.google.com/drive-file-stream/GoogleDriveSetup.exe" +$installerPath = "$env:WINDIR\temp\GoogleDriveSetup.exe" + +# Check if Google Drive is already installed +$driveExePath = "C:\Program Files\Google\Drive File Stream\launch.bat" +$driveExePath2 = "${env:ProgramFiles}\Google\Drive File Stream\GoogleDriveFS.exe" + +if ((Test-Path -Path $driveExePath) -or (Test-Path -Path $driveExePath2)) { + Write-Host "Google Drive is already installed. Skipping installation." + Stop-Transcript + exit 0 +} + +# Try winget first +$wingetAvailable = $false +try { + $wingetCheck = winget --version 2>$null + if ($LASTEXITCODE -eq 0) { + $wingetAvailable = $true + Write-Host "Winget detected: $wingetCheck" + } +} catch { + Write-Host "Winget not available." +} + +if ($wingetAvailable) { + Write-Host "Installing Google Drive via winget..." + winget install -e --id $packageId --silent --accept-package-agreements --accept-source-agreements + + if ($LASTEXITCODE -eq 0) { + Write-Host "Google Drive has been successfully installed via winget." + Stop-Transcript + exit 0 + } else { + Write-Host "Winget install failed (exit code: $LASTEXITCODE). Falling back to direct download." + } +} + +# Fallback: direct download from Google +Write-Host "Downloading Google Drive installer from $installerUrl..." +try { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath -UseBasicParsing +} catch { + Write-Host "Failed to download Google Drive installer: $_" + Stop-Transcript + exit 1 +} + +if (-not (Test-Path -Path $installerPath)) { + Write-Host "Installer not found at $installerPath. Download may have failed." + Stop-Transcript + exit 1 +} + +Write-Host "Running Google Drive installer silently..." +Start-Process -FilePath $installerPath -ArgumentList "--silent --desktop_shortcut" -Wait -NoNewWindow + +if ($LASTEXITCODE -eq 0) { + Write-Host "Google Drive has been successfully installed via direct download." +} else { + Write-Host "Google Drive installer returned exit code: $LASTEXITCODE" +} + +# Cleanup installer +if (Test-Path -Path $installerPath) { + Remove-Item -Path $installerPath -Force + Write-Host "Cleaned up installer file." +} + +Stop-Transcript diff --git a/msft-windows/msft-windows-sid-detect-duplicates.ps1 b/msft-windows/msft-windows-sid-detect-duplicates.ps1 new file mode 100644 index 0000000..713cce0 --- /dev/null +++ b/msft-windows/msft-windows-sid-detect-duplicates.ps1 @@ -0,0 +1,290 @@ +## PLEASE COMMENT YOUR VARIABLES DIRECTLY BELOW HERE IF YOU'RE RUNNING FROM A RMM +## THIS IS HOW WE EASILY LET PEOPLE KNOW WHAT VARIABLES NEED SET IN THE RMM +## +## Optional RMM Variables: +## - $Description: Ticket number or initials for tracking (defaults to "Automated Duplicate SID Scan") +## - $SearchBase: LDAP path to limit search scope (e.g., "OU=Workstations,DC=contoso,DC=com") + +# Duplicate Local Machine SID Detector +# Queries all AD computer objects with the info (Notes) attribute populated (written by msft-windows-sid-report.ps1), +# groups by SID, and reports any duplicates. Duplicate local machine SIDs indicate machines that were +# cloned/imaged without running sysprep. +# +# AD Attribute Used: info (Notes field) - built-in on all computer objects, no schema extensions required. +# +# Prerequisites: +# - msft-windows-sid-report.ps1 must have run on workstations first to populate the info attribute +# - Run this script on a Domain Controller or domain-joined admin workstation +# - Account running the script needs read access to computer objects in AD +# +# Exit Codes: +# 0 = Success, no duplicates found +# 1 = Success, duplicates found (non-zero to trigger RMM alerting) +# 2 = Error (not domain-joined, LDAP query failed, etc.) + +# Getting input from user if not running from RMM else set variables from RMM. + +$ScriptLogName = "msft-windows-sid-detect-duplicates.log" + +if ($RMM -ne 1) { + $ValidInput = 0 + # Checking for valid input. + while ($ValidInput -ne 1) { + $Description = Read-Host "Please enter the ticket # and, or your initials (press Enter for 'Automated Duplicate SID Scan')" + if (-not $Description) { + $Description = "Automated Duplicate SID Scan" + } + + $SearchBase = Read-Host "Enter LDAP search base to limit scope (press Enter to search entire domain)" + + $ValidInput = 1 + } + $LogPath = "$ENV:WINDIR\logs\$ScriptLogName" + +} else { + # Store the logs in the RMMScriptPath + if ($null -ne $RMMScriptPath) { + $LogPath = "$RMMScriptPath\logs\$ScriptLogName" + } else { + $LogPath = "$ENV:WINDIR\logs\$ScriptLogName" + } + + if ($null -eq $Description) { + $Description = "Automated Duplicate SID Scan" + } +} + +# Start the script logic here. This is the part that actually gets done what you need done. + +Start-Transcript -Path $LogPath + +Write-Host "Description: $Description" +Write-Host "Log path: $LogPath" +Write-Host "RMM: $RMM" +Write-Host "Search Base: $(if ($SearchBase) { $SearchBase } else { '(entire domain)' })" +Write-Host "" + +# Step 1: Check domain membership +$computerSystem = Get-WmiObject -Class Win32_ComputerSystem +$partOfDomain = $computerSystem.PartOfDomain + +Write-Host "Running on: $($computerSystem.Name)" +Write-Host "Part of Domain: $partOfDomain" +Write-Host "" + +if (-not $partOfDomain) { + Write-Host "[ERROR] This computer is not domain-joined. Cannot query AD." + Stop-Transcript + exit 2 +} + +# DomainRole: 4 = Backup DC, 5 = Primary DC +if ($computerSystem.DomainRole -lt 4) { + Write-Host "[ERROR] This script must be run on a Domain Controller. Current role: $($computerSystem.DomainRole) (expected 4 or 5)." + Stop-Transcript + exit 2 +} + +# Step 2: Query all computers with info (Notes) attribute containing MACHINESID: prefix +try { + $searcher = [ADSISearcher]"(&(objectCategory=computer)(info=MACHINESID:*))" + $searcher.PageSize = 1000 + $searcher.PropertiesToLoad.AddRange(@("cn", "info", "distinguishedname")) + + if (-not [string]::IsNullOrWhiteSpace($SearchBase)) { + try { + $searcher.SearchRoot = [ADSI]"LDAP://$SearchBase" + Write-Host "[INFO] Search scope limited to: $SearchBase" + } catch { + Write-Host "[ERROR] Invalid SearchBase '$SearchBase': $($_.Exception.Message)" + Stop-Transcript + exit 2 + } + } + + $results = $searcher.FindAll() + $resultCount = $results.Count + Write-Host "[INFO] Found $resultCount computer(s) with MACHINESID stamp in info attribute." + Write-Host "" +} catch { + Write-Host "[ERROR] LDAP search failed: $($_.Exception.Message)" + Stop-Transcript + exit 2 +} + +if ($resultCount -eq 0) { + Write-Host "[INFO] No computers have MACHINESID stamp in info attribute." + Write-Host "[INFO] Ensure msft-windows-sid-report.ps1 has been deployed and run on workstations first." + + # RMM Custom Field Output + Write-Host "" + Write-Host "============================================" + Write-Host "RMM CUSTOM FIELD VALUES" + Write-Host "============================================" + Write-Host "DUPSID_FOUND: False" + Write-Host "DUPSID_COUNT: 0" + Write-Host "DUPSID_AFFECTED: 0" + Write-Host "DUPSID_SCANNED: 0" + Write-Host "DUPSID_DETAILS: No computers reporting SID data yet" + Write-Host "DUPSID_SUMMARY: No computers reporting SID data - deploy sid-report script first" + Write-Host "DUPSID_SCANDATE: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + Write-Host "============================================" + + $results.Dispose() + Stop-Transcript + exit 0 +} + +# Step 3: Build SID-to-computer mapping +$sidMap = @{} +$totalComputers = 0 + +foreach ($result in $results) { + $cn = $result.Properties['cn'][0] + $rawInfo = $result.Properties['info'][0] + $dn = $result.Properties['distinguishedname'][0] + + if ([string]::IsNullOrWhiteSpace($rawInfo)) { + continue + } + + # Strip the MACHINESID: prefix to get the raw SID + $sid = $rawInfo -replace '^MACHINESID:', '' + + if ([string]::IsNullOrWhiteSpace($sid)) { + continue + } + + $totalComputers++ + + $entry = [PSCustomObject]@{ + Name = $cn + DN = $dn + } + + if ($sidMap.ContainsKey($sid)) { + $sidMap[$sid] += $entry + } else { + $sidMap[$sid] = @($entry) + } +} + +# Clean up COM objects from FindAll() +$results.Dispose() + +Write-Host "[INFO] Processed $totalComputers computer(s) with valid SID data." +Write-Host "[INFO] Unique SIDs: $($sidMap.Count)" +Write-Host "" + +# Step 4: Full SID inventory - list every SID and its associated computer(s) +Write-Host "============================================" +Write-Host "FULL SID INVENTORY" +Write-Host "============================================" +Write-Host "" + +foreach ($entry in $sidMap.GetEnumerator()) { + $sid = $entry.Key + $computers = $entry.Value + $isDuplicate = $computers.Count -gt 1 + $marker = if ($isDuplicate) { " ** DUPLICATE **" } else { "" } + + foreach ($computer in $computers) { + Write-Host " $($computer.Name) : $sid$marker" + } +} +Write-Host "" + +# Step 5: Identify and report duplicates +$duplicates = @($sidMap.GetEnumerator() | Where-Object { $_.Value.Count -gt 1 }) +$duplicateCount = $duplicates.Count +$affectedMachines = 0 +foreach ($dup in $duplicates) { + $affectedMachines += $dup.Value.Count +} +$duplicateFound = $duplicateCount -gt 0 + +Write-Host "============================================" +Write-Host "DUPLICATE SID SCAN RESULTS" +Write-Host "============================================" +Write-Host "" +Write-Host "Total computers scanned: $totalComputers" +Write-Host "Unique SIDs: $($sidMap.Count)" +Write-Host "Duplicate SID groups: $duplicateCount" +Write-Host "Affected machines: $affectedMachines" +Write-Host "" + +if ($duplicateFound) { + Write-Host "[WARNING] DUPLICATE SIDs DETECTED!" + Write-Host "" + + $groupIndex = 0 + foreach ($entry in $duplicates) { + $groupIndex++ + $sid = $entry.Key + $computers = $entry.Value + + Write-Host "--- Duplicate Group $groupIndex ---" + Write-Host " SID: $sid" + Write-Host " Affected Computers ($($computers.Count)):" + foreach ($computer in $computers) { + Write-Host " - $($computer.Name)" + Write-Host " DN: $($computer.DN)" + } + Write-Host "" + } + + Write-Host "[ACTION REQUIRED] These machines were likely cloned or imaged without running sysprep." + Write-Host "[ACTION REQUIRED] Affected machines must be reimaged with sysprep /generalize to generate unique SIDs." + Write-Host "" +} else { + Write-Host "[OK] No duplicate SIDs detected. All $totalComputers machines have unique local machine SIDs." + Write-Host "" +} + +# Step 6: RMM Custom Field Output +$scanDate = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + +Write-Host "============================================" +Write-Host "RMM CUSTOM FIELD VALUES" +Write-Host "============================================" +Write-Host "DUPSID_FOUND: $duplicateFound" +Write-Host "DUPSID_COUNT: $duplicateCount" +Write-Host "DUPSID_AFFECTED: $affectedMachines" +Write-Host "DUPSID_SCANNED: $totalComputers" +Write-Host "DUPSID_SCANDATE: $scanDate" +Write-Host "============================================" +Write-Host "" + +# Build HTML report of duplicate machines only +$htmlLines = @() +$htmlLines += "

Duplicate SID Report - $scanDate

" +$htmlLines += "

Scanned: $totalComputers | Duplicate Groups: $duplicateCount | Affected Machines: $affectedMachines

" +if ($duplicateFound) { + $htmlLines += "" + foreach ($entry in $duplicates) { + $sid = $entry.Key + $computers = $entry.Value + foreach ($computer in $computers) { + $htmlLines += "" + } + } + $htmlLines += "
ComputerSID
$($computer.Name)$sid
" + $htmlLines += "

ACTION REQUIRED: These machines were likely cloned/imaged without running sysprep.

" +} else { + $htmlLines += "

No duplicate SIDs detected.

" +} +$htmlReport = $htmlLines -join "" + +# NinjaRMM custom fields (uncomment if using NinjaRMM): +# Ninja-Property-Set dupSidFound $duplicateFound +# Ninja-Property-Set dupSidCount $duplicateCount +# Ninja-Property-Set dupSidAffected $affectedMachines +# $htmlReport | Ninja-Property-Set-Piped dupSidSummary + +Stop-Transcript + +if ($duplicateFound) { + exit 1 +} else { + exit 0 +} diff --git a/msft-windows/msft-windows-sid-report.ps1 b/msft-windows/msft-windows-sid-report.ps1 new file mode 100644 index 0000000..768b194 --- /dev/null +++ b/msft-windows/msft-windows-sid-report.ps1 @@ -0,0 +1,182 @@ +## PLEASE COMMENT YOUR VARIABLES DIRECTLY BELOW HERE IF YOU'RE RUNNING FROM A RMM +## THIS IS HOW WE EASILY LET PEOPLE KNOW WHAT VARIABLES NEED SET IN THE RMM +## +## Optional RMM Variables: +## - $Description: Ticket number or initials for tracking (defaults to "Automated SID Report") + +# Local Machine SID Reporter +# Writes the local machine SID to the "info" (Notes) attribute on the computer's own AD object. +# This enables centralized duplicate SID detection for cloned/imaged machines +# that were not properly sysprepped. +# +# The local machine SID is NOT the AD objectSid. It is the SID baked into the OS +# during installation. When a machine is cloned without sysprep, multiple machines +# share this SID, which can cause authentication and security issues. +# +# AD Attribute Used: info (Notes field) - built-in on all computer objects, no schema extensions required. +# +# Exit Codes: +# 0 = Success (SID written to AD) +# 1 = Error (not domain-joined, computer not found, permission denied, etc.) + +# Getting input from user if not running from RMM else set variables from RMM. + +$ScriptLogName = "msft-windows-sid-report.log" + +if ($RMM -ne 1) { + $ValidInput = 0 + # Checking for valid input. + while ($ValidInput -ne 1) { + $Description = Read-Host "Please enter the ticket # and, or your initials (press Enter for 'Automated SID Report')" + if (-not $Description) { + $Description = "Automated SID Report" + } + $ValidInput = 1 + } + $LogPath = "$ENV:WINDIR\logs\$ScriptLogName" + +} else { + # Store the logs in the RMMScriptPath + if ($null -ne $RMMScriptPath) { + $LogPath = "$RMMScriptPath\logs\$ScriptLogName" + } else { + $LogPath = "$ENV:WINDIR\logs\$ScriptLogName" + } + + if ($null -eq $Description) { + $Description = "Automated SID Report" + } +} + +# Start the script logic here. This is the part that actually gets done what you need done. + +Start-Transcript -Path $LogPath + +Write-Host "Description: $Description" +Write-Host "Log path: $LogPath" +Write-Host "RMM: $RMM" +Write-Host "" + +# Step 1: Check domain membership - exit immediately if not domain-joined +$computerSystem = Get-WmiObject -Class Win32_ComputerSystem +$computerName = $computerSystem.Name +$partOfDomain = $computerSystem.PartOfDomain + +Write-Host "Computer Name: $computerName" +Write-Host "Part of Domain: $partOfDomain" +Write-Host "" + +if (-not $partOfDomain) { + Write-Host "[ERROR] This computer is not domain-joined. Cannot write to AD." + Write-Host "[INFO] Machine SID reporting requires domain membership. Exiting." + Stop-Transcript + exit 1 +} + +# Step 2: Get the local machine SID +# Query the built-in Administrator account (RID -500) and strip the RID to get the machine SID +try { + $adminAccount = Get-WmiObject -Class Win32_UserAccount -Filter "LocalAccount = True AND SID LIKE '%-500'" + if ($null -eq $adminAccount) { + Write-Host "[ERROR] Could not find built-in Administrator account (SID ending in -500)." + Write-Host "[INFO] This is unexpected and may indicate a corrupted SAM database." + Stop-Transcript + exit 1 + } + $adminSid = $adminAccount.SID + $machineSid = $adminSid.Substring(0, $adminSid.LastIndexOf('-')) + Write-Host "[INFO] Local Machine SID: $machineSid" + Write-Host "[INFO] Derived from built-in Administrator SID: $adminSid" + Write-Host "" +} catch { + Write-Host "[ERROR] Failed to retrieve local machine SID: $($_.Exception.Message)" + Stop-Transcript + exit 1 +} + +# Step 3: Find the computer's own AD object using ADSISearcher +try { + $searcher = [ADSISearcher]"(&(objectCategory=computer)(cn=$computerName))" + $searcher.PropertiesToLoad.AddRange(@("distinguishedname", "info")) + $result = $searcher.FindOne() + + if ($null -eq $result) { + Write-Host "[ERROR] Computer object '$computerName' not found in Active Directory." + Write-Host "[INFO] Ensure the computer is properly joined to the domain." + Stop-Transcript + exit 1 + } + + Write-Host "[INFO] Found AD computer object: $($result.Properties['distinguishedname'][0])" +} catch { + Write-Host "[ERROR] LDAP search failed: $($_.Exception.Message)" + Stop-Transcript + exit 1 +} + +# Step 4: Write the machine SID to the info (Notes) attribute with MACHINESID: prefix +$sidStamp = "MACHINESID:$machineSid" + +try { + $computerObject = $result.GetDirectoryEntry() + + $currentValue = $null + if ($computerObject.Properties["info"].Count -gt 0) { + $currentValue = $computerObject.Properties["info"][0] + } + + if ($currentValue) { + Write-Host "[INFO] Current info (Notes) value: $currentValue" + if ($currentValue -eq $sidStamp) { + Write-Host "[INFO] info attribute already contains the correct prefixed machine SID. No update needed." + Stop-Transcript + exit 0 + } + # Migrate raw SID values from previous version to prefixed format + if ($currentValue -eq $machineSid) { + Write-Host "[INFO] Found raw SID without prefix. Migrating to prefixed format." + } elseif ($currentValue -like "MACHINESID:*") { + Write-Host "[INFO] SID has changed since last report. Updating." + } else { + Write-Host "[WARNING] info field contains non-SID data: '$currentValue'. Overwriting with machine SID." + } + } else { + Write-Host "[INFO] info (Notes) attribute is currently empty." + } + + $computerObject.Put("info", $sidStamp) + $computerObject.SetInfo() + + Write-Host "[SUCCESS] Written '$sidStamp' to info (Notes) attribute on AD object." +} catch { + Write-Host "[ERROR] Failed to write info attribute: $($_.Exception.Message)" + Write-Host "[INFO] This may be a permissions issue. The script requires write access to the computer's own AD object." + Write-Host "[INFO] When running as SYSTEM, the computer account typically has write access to its own attributes." + Write-Host "[INFO] If running as a user, domain admin or delegated permissions may be required." + Stop-Transcript + exit 1 +} + +# Step 5: Verify the write was successful +try { + $verifySearcher = [ADSISearcher]"(&(objectCategory=computer)(cn=$computerName))" + $verifySearcher.PropertiesToLoad.AddRange(@("info")) + $verifyResult = $verifySearcher.FindOne() + $verifiedValue = $verifyResult.Properties['info'][0] + + if ($verifiedValue -eq $sidStamp) { + Write-Host "[VERIFIED] info (Notes) attribute confirmed: $verifiedValue" + } else { + Write-Host "[WARNING] Verification mismatch. Written: $sidStamp, Read back: $verifiedValue" + } +} catch { + Write-Host "[WARNING] Could not verify write. The update may still have succeeded: $($_.Exception.Message)" +} + +Write-Host "" +Write-Host "============================================" +Write-Host "SID Report Complete" +Write-Host "============================================" + +Stop-Transcript +exit 0