diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/DNS Detection Script.ps1 b/DNS Detection Script.ps1 new file mode 100755 index 0000000..341c307 --- /dev/null +++ b/DNS Detection Script.ps1 @@ -0,0 +1,28 @@ +''' + +Created: 2025-03-05 +Modified: 2025-03-05 +Description: This script checks to ensure the DNS servers are set to the DNS servers that filter malware + +''' + +# Get all network adapters +$adapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } + +# Check DNS server settings +foreach ($adapter in $adapters) { + $dnsServers = Get-DnsClientServerAddress -InterfaceIndex $adapter.InterfaceIndex + $staticDnsServers = $dnsServers | Where-Object { $_.ServerAddresses -contains '9.9.9.9' -or $_.ServerAddresses -contains '149.112.112.11' -or $_.ServerAddresses -contains '1.1.1.3' -or $_.ServerAddresses -contains '1.0.0.3' } + + if ($staticDnsServers.Count -eq 0) { + Write-Host "Adapter $($adapter.Name) does not have the correct DNS servers configured." + $compliance = "false" + } + else { + Write-Host "Adapter $($adapter.Name) has the correct DNS servers configured." + $compliance = "true" + } +} + +$hash = @{ Compliant = $compliance} +return $hash | ConvertTo-Json -Compress \ No newline at end of file diff --git a/DirectoryListing.ps1 b/DirectoryListing.ps1 new file mode 100755 index 0000000..d8cfee8 --- /dev/null +++ b/DirectoryListing.ps1 @@ -0,0 +1,209 @@ +$root = 'E:\Sync Method\Corporate\Zefiro Methane Corp. - Documents' +$outputDirectory = 'C:\Users\MattSpeer\OneDrive - Add Source Group\Documents\1- Projects\Zefiro - Data Migration' +$textFile = Join-Path $outputDirectory 'Zefiro-Methane-Corp-Documents-tree.txt' +$htmlFile = Join-Path $outputDirectory 'Zefiro-Methane-Corp-Documents-tree.html' + +if (-not (Test-Path -LiteralPath $root -PathType Container)) { + throw "Folder not found: $root" +} + +New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null + +$script:textLines = [System.Collections.Generic.List[string]]::new() +$script:html = [System.Text.StringBuilder]::new() +$script:scanErrors = 0 + +function ConvertTo-HtmlEncoded { + param([string]$Text) + return [System.Net.WebUtility]::HtmlEncode($Text) +} + +function Add-TreeItems { + param( + [string]$Path, + [string]$Prefix + ) + + try { + $items = @( + Get-ChildItem -LiteralPath $Path -Force -ErrorAction Stop | + Sort-Object @{ Expression = { if ($_.PSIsContainer) { 0 } else { 1 } } }, Name + ) + } + catch { + $script:scanErrors++ + [void]$script:textLines.Add("$Prefix[Unable to read: $($_.Exception.Message)]") + [void]$script:html.AppendLine( + "
  • Unable to read: $(ConvertTo-HtmlEncoded $_.Exception.Message)
  • " + ) + return + } + + for ($index = 0; $index -lt $items.Count; $index++) { + $item = $items[$index] + $isLast = $index -eq ($items.Count - 1) + $connector = if ($isLast) { '\-- ' } else { '|-- ' } + $nextPrefix = $Prefix + $(if ($isLast) { ' ' } else { '| ' }) + + $nameHtml = ConvertTo-HtmlEncoded $item.Name + $pathHtml = ConvertTo-HtmlEncoded $item.FullName + $isLink = -not [string]::IsNullOrWhiteSpace([string]$item.LinkType) + + if ($item.PSIsContainer -and -not $isLink) { + [void]$script:textLines.Add("$Prefix$connector$($item.Name)") + [void]$script:html.AppendLine( + "
  • 📁 $nameHtml
  • ') + } + elseif ($item.PSIsContainer -and $isLink) { + $target = @($item.Target) -join ', ' + [void]$script:textLines.Add( + "$Prefix$connector$($item.Name) [Link → $target]" + ) + [void]$script:html.AppendLine( + "" + ) + } + else { + [void]$script:textLines.Add("$Prefix$connector$($item.Name)") + [void]$script:html.AppendLine( + "
  • 📄 $nameHtml
  • " + ) + } + } +} + +$rootNameHtml = ConvertTo-HtmlEncoded (Split-Path -Path $root -Leaf) +$rootPathHtml = ConvertTo-HtmlEncoded $root +$generated = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + +[void]$script:textLines.Add($root) + +[void]$script:html.AppendLine(@" + + + + + +Team Site Documents Directory + + + +

    Team Site – Documents

    +
    +Generated $generated
    +Root: $rootPathHtml +
    +
    + + + +
    + + + + +"@) + +$script:textLines | Set-Content -LiteralPath $textFile -Encoding UTF8 +$script:html.ToString() | Set-Content -LiteralPath $htmlFile -Encoding UTF8 + +Write-Host "Text report: $textFile" +Write-Host "HTML report: $htmlFile" +Write-Host "Unreadable folders: $script:scanErrors" + +Start-Process $htmlFile \ No newline at end of file diff --git a/Download-All-SharePoint-Files.ps1 b/Download-All-SharePoint-Files.ps1 new file mode 100755 index 0000000..0305705 --- /dev/null +++ b/Download-All-SharePoint-Files.ps1 @@ -0,0 +1,112 @@ +$SiteUrl = "https://zefiromethane.sharepoint.com/sites/contentTypeHub" +$Destination = "F:\TeamSite" + +# Opens a Microsoft sign-in window. +Connect-MgGraph -Scopes "Sites.Read.All" -NoWelcome + +function Get-GraphCollection { + param([string]$Uri) + + do { + $response = Invoke-MgGraphRequest -Method GET -Uri $Uri + + foreach ($item in @($response.value)) { + Write-Output $item + } + + $Uri = $response.'@odata.nextLink' + } + while ($Uri) +} + +function Copy-GraphFolder { + param( + [string]$DriveId, + [string]$ChildrenUri, + [string]$LocalFolder + ) + + New-Item -ItemType Directory -Path $LocalFolder -Force | + Out-Null + + foreach ($item in Get-GraphCollection -Uri $ChildrenUri) { + $localPath = Join-Path $LocalFolder $item.name + $escapedDriveId = [Uri]::EscapeDataString($DriveId) + $escapedItemId = [Uri]::EscapeDataString([string]$item.id) + + if ($null -ne $item.folder -or $null -ne $item.package) { + # Create the folder, including empty folders. + New-Item -ItemType Directory -Path $localPath -Force | + Out-Null + + $childUri = ` + "https://graph.microsoft.com/v1.0/drives/" + + "$escapedDriveId/items/$escapedItemId/children" + + Copy-GraphFolder ` + -DriveId $DriveId ` + -ChildrenUri $childUri ` + -LocalFolder $localPath + } + elseif ($null -ne $item.file) { + if (Test-Path -LiteralPath $localPath) { + Write-Host "Skipping existing file: $localPath" + continue + } + + Write-Host "Downloading: $localPath" + + $contentUri = ` + "https://graph.microsoft.com/v1.0/drives/" + + "$escapedDriveId/items/$escapedItemId/content" + + Invoke-MgGraphRequest ` + -Method GET ` + -Uri $contentUri ` + -OutputFilePath $localPath + } + else { + Write-Warning "Unsupported item skipped: $($item.name)" + } + } +} + +# Resolve the SharePoint site from its URL. +$parsedSiteUrl = [Uri]$SiteUrl +$sitePath = $parsedSiteUrl.AbsolutePath.TrimEnd("/") + +if ([string]::IsNullOrWhiteSpace($sitePath)) { + $siteLookupUri = ` + "https://graph.microsoft.com/v1.0/sites/$($parsedSiteUrl.Host)" +} +else { + $siteLookupUri = ` + "https://graph.microsoft.com/v1.0/sites/" + + "$($parsedSiteUrl.Host):$sitePath" +} + +$site = Invoke-MgGraphRequest -Method GET -Uri $siteLookupUri + +New-Item -ItemType Directory -Path $Destination -Force | + Out-Null + +# A Graph "drive" represents a SharePoint document library. +$drivesUri = "https://graph.microsoft.com/v1.0/sites/$($site.id)/drives" + +foreach ($drive in Get-GraphCollection -Uri $drivesUri) { + $localLibrary = Join-Path $Destination $drive.name + $escapedDriveId = [Uri]::EscapeDataString([string]$drive.id) + + Write-Host "Downloading library: $($drive.name)" + + $rootChildrenUri = ` + "https://graph.microsoft.com/v1.0/drives/" + + "$escapedDriveId/root/children" + + Copy-GraphFolder ` + -DriveId $drive.id ` + -ChildrenUri $rootChildrenUri ` + -LocalFolder $localLibrary +} + +Disconnect-MgGraph \ No newline at end of file diff --git a/Export-DocuSignCompletedAgreements.ps1 b/Export-DocuSignCompletedAgreements.ps1 new file mode 100644 index 0000000..5abb328 --- /dev/null +++ b/Export-DocuSignCompletedAgreements.ps1 @@ -0,0 +1,378 @@ +<# +.SYNOPSIS +Exports completed DocuSign envelopes, including the Certificate of Completion. + +.DESCRIPTION +Uses the DocuSign eSignature REST API to: + 1. Authenticate with JWT. + 2. List completed envelopes in a date range. + 3. Download each envelope as either an archive ZIP or combined PDF. + 4. Write a CSV export log. + +Prerequisites: + - PowerShell 7 recommended. + - A DocuSign app/integration key. + - RSA private key PEM for the integration. + - Consent granted for scopes: signature impersonation. + - The impersonated user must have access to the target envelopes. + +Consent URL format: + Production: + https://account.docusign.com/oauth/auth?response_type=code&scope=signature%20impersonation&client_id=INTEGRATION_KEY&redirect_uri=https://www.docusign.com + + Demo: + https://account-d.docusign.com/oauth/auth?response_type=code&scope=signature%20impersonation&client_id=INTEGRATION_KEY&redirect_uri=https://www.docusign.com + +.EXAMPLE +.\Export-DocuSignCompletedAgreements.ps1 ` + -IntegrationKey "00000000-0000-0000-0000-000000000000" ` + -UserId "11111111-1111-1111-1111-111111111111" ` + -AccountId "22222222-2222-2222-2222-222222222222" ` + -PrivateKeyPath "C:\Keys\docusign_private.key" ` + -FromDate "2020-01-01" ` + -OutputRoot "D:\DocuSignExport" ` + -OutputFormat ArchiveZip +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$IntegrationKey, + + [Parameter(Mandatory = $true)] + [string]$UserId, + + [Parameter(Mandatory = $true)] + [string]$AccountId, + + [Parameter(Mandatory = $true)] + [string]$PrivateKeyPath, + + [Parameter(Mandatory = $true)] + [datetime]$FromDate, + + [Parameter(Mandatory = $false)] + [datetime]$ToDate = (Get-Date), + + [Parameter(Mandatory = $true)] + [string]$OutputRoot, + + [Parameter(Mandatory = $false)] + [ValidateSet("Production", "Demo")] + [string]$Environment = "Production", + + [Parameter(Mandatory = $false)] + [ValidateSet("ArchiveZip", "CombinedPdf")] + [string]$OutputFormat = "ArchiveZip", + + [Parameter(Mandatory = $false)] + [int]$PageSize = 1000, + + [Parameter(Mandatory = $false)] + [int]$RetryCount = 4 +) + +$ErrorActionPreference = "Stop" + +function ConvertTo-Base64Url { + param([byte[]]$Bytes) + [Convert]::ToBase64String($Bytes).TrimEnd("=").Replace("+", "-").Replace("/", "_") +} + +function ConvertFrom-Base64UrlString { + param([string]$Value) + $base64 = $Value.Replace("-", "+").Replace("_", "/") + switch ($base64.Length % 4) { + 2 { $base64 += "==" } + 3 { $base64 += "=" } + } + [Convert]::FromBase64String($base64) +} + +function Import-RsaPrivateKey { + param([string]$Path) + + $pem = Get-Content -Path $Path -Raw + $rsa = [System.Security.Cryptography.RSA]::Create() + + if ($pem -match "-----BEGIN RSA PRIVATE KEY-----") { + $body = $pem -replace "-----BEGIN RSA PRIVATE KEY-----", "" -replace "-----END RSA PRIVATE KEY-----", "" -replace "\s", "" + $bytes = [Convert]::FromBase64String($body) + [void]$rsa.ImportRSAPrivateKey($bytes, [ref]0) + return $rsa + } + + if ($pem -match "-----BEGIN PRIVATE KEY-----") { + $body = $pem -replace "-----BEGIN PRIVATE KEY-----", "" -replace "-----END PRIVATE KEY-----", "" -replace "\s", "" + $bytes = [Convert]::FromBase64String($body) + [void]$rsa.ImportPkcs8PrivateKey($bytes, [ref]0) + return $rsa + } + + throw "Private key must be PEM formatted with BEGIN RSA PRIVATE KEY or BEGIN PRIVATE KEY." +} + +function New-DocuSignJwtAssertion { + param( + [string]$IntegrationKey, + [string]$UserId, + [string]$AuthServer, + [string]$PrivateKeyPath + ) + + $now = [DateTimeOffset]::UtcNow + $header = @{ + alg = "RS256" + typ = "JWT" + } | ConvertTo-Json -Compress + + $payload = @{ + iss = $IntegrationKey + sub = $UserId + aud = $AuthServer + iat = $now.ToUnixTimeSeconds() + exp = $now.AddMinutes(55).ToUnixTimeSeconds() + scope = "signature impersonation" + } | ConvertTo-Json -Compress + + $encodedHeader = ConvertTo-Base64Url ([Text.Encoding]::UTF8.GetBytes($header)) + $encodedPayload = ConvertTo-Base64Url ([Text.Encoding]::UTF8.GetBytes($payload)) + $unsignedToken = "$encodedHeader.$encodedPayload" + + $rsa = Import-RsaPrivateKey -Path $PrivateKeyPath + try { + $signature = $rsa.SignData( + [Text.Encoding]::UTF8.GetBytes($unsignedToken), + [Security.Cryptography.HashAlgorithmName]::SHA256, + [Security.Cryptography.RSASignaturePadding]::Pkcs1 + ) + } + finally { + $rsa.Dispose() + } + + "$unsignedToken.$(ConvertTo-Base64Url $signature)" +} + +function Invoke-WithRetry { + param( + [scriptblock]$ScriptBlock, + [int]$RetryCount = 4, + [string]$ActionName = "DocuSign API request" + ) + + for ($attempt = 1; $attempt -le ($RetryCount + 1); $attempt++) { + try { + return & $ScriptBlock + } + catch { + $response = $_.Exception.Response + $statusCode = $null + if ($response -and $response.StatusCode) { + $statusCode = [int]$response.StatusCode + } + + $retryable = $statusCode -in @(429, 500, 502, 503, 504) + if (-not $retryable -or $attempt -gt $RetryCount) { + throw + } + + $delaySeconds = [math]::Min(60, [math]::Pow(2, $attempt)) + Write-Warning "$ActionName failed with HTTP $statusCode. Retrying in $delaySeconds seconds. Attempt $attempt of $RetryCount." + Start-Sleep -Seconds $delaySeconds + } + } +} + +function Get-SafeFileName { + param( + [string]$Text, + [int]$MaxLength = 120 + ) + + $invalid = [Regex]::Escape(([IO.Path]::GetInvalidFileNameChars() -join "")) + $safe = [Regex]::Replace($Text, "[$invalid]", "_") + $safe = [Regex]::Replace($safe, "\s+", " ").Trim() + if ([string]::IsNullOrWhiteSpace($safe)) { + $safe = "Envelope" + } + if ($safe.Length -gt $MaxLength) { + $safe = $safe.Substring(0, $MaxLength).Trim() + } + $safe +} + +function Save-Bytes { + param( + [string]$Uri, + [hashtable]$Headers, + [string]$Path, + [int]$RetryCount + ) + + Invoke-WithRetry -RetryCount $RetryCount -ActionName "Download $Path" -ScriptBlock { + Invoke-WebRequest -Uri $Uri -Headers $Headers -OutFile $Path + } | Out-Null +} + +$authServer = if ($Environment -eq "Demo") { "account-d.docusign.com" } else { "account.docusign.com" } +$tokenUri = "https://$authServer/oauth/token" + +Write-Host "Requesting DocuSign access token..." +$assertion = New-DocuSignJwtAssertion ` + -IntegrationKey $IntegrationKey ` + -UserId $UserId ` + -AuthServer $authServer ` + -PrivateKeyPath $PrivateKeyPath + +$tokenResponse = Invoke-WithRetry -RetryCount $RetryCount -ActionName "JWT token request" -ScriptBlock { + Invoke-RestMethod ` + -Method Post ` + -Uri $tokenUri ` + -ContentType "application/x-www-form-urlencoded" ` + -Body @{ + grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer" + assertion = $assertion + } +} + +$headers = @{ + Authorization = "Bearer $($tokenResponse.access_token)" +} + +Write-Host "Discovering account base URI..." +$userInfo = Invoke-WithRetry -RetryCount $RetryCount -ActionName "UserInfo request" -ScriptBlock { + Invoke-RestMethod -Method Get -Uri "https://$authServer/oauth/userinfo" -Headers $headers +} + +$account = $userInfo.accounts | Where-Object { $_.account_id -eq $AccountId } | Select-Object -First 1 +if (-not $account) { + throw "AccountId '$AccountId' was not found for the impersonated user." +} + +$baseUri = $account.base_uri.TrimEnd("/") +$apiBase = "$baseUri/restapi/v2.1/accounts/$AccountId" + +$downloadFolder = Join-Path $OutputRoot "completed-agreements" +$logFolder = Join-Path $OutputRoot "logs" +New-Item -ItemType Directory -Path $downloadFolder -Force | Out-Null +New-Item -ItemType Directory -Path $logFolder -Force | Out-Null + +$timestamp = Get-Date -Format "yyyyMMdd-HHmmss" +$logPath = Join-Path $logFolder "docusign-export-log-$timestamp.csv" + +$documentId = if ($OutputFormat -eq "ArchiveZip") { "archive" } else { "combined" } +$extension = if ($OutputFormat -eq "ArchiveZip") { ".zip" } else { ".pdf" } +$accept = if ($OutputFormat -eq "ArchiveZip") { "application/zip" } else { "application/pdf" } + +$downloadHeaders = @{ + Authorization = "Bearer $($tokenResponse.access_token)" + Accept = $accept +} + +$from = $FromDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") +$to = $ToDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") + +Write-Host "Finding completed envelopes from $from to $to..." +$allEnvelopes = New-Object System.Collections.Generic.List[object] +$startPosition = 0 + +do { + $query = @{ + from_date = $from + to_date = $to + status = "completed" + count = $PageSize + start_position = $startPosition + order_by = "completed" + order = "asc" + } + + $queryString = ($query.GetEnumerator() | ForEach-Object { + "{0}={1}" -f [Uri]::EscapeDataString($_.Key), [Uri]::EscapeDataString([string]$_.Value) + }) -join "&" + + $uri = "$apiBase/envelopes?$queryString" + $page = Invoke-WithRetry -RetryCount $RetryCount -ActionName "Envelope list page starting $startPosition" -ScriptBlock { + Invoke-RestMethod -Method Get -Uri $uri -Headers $headers + } + + if ($page.envelopes) { + foreach ($envelope in $page.envelopes) { + $allEnvelopes.Add($envelope) + } + } + + $returned = if ($page.envelopes) { @($page.envelopes).Count } else { 0 } + Write-Host "Found $returned envelopes on page starting at $startPosition." + + $startPosition += $PageSize + $totalSetSize = [int]$page.totalSetSize +} while ($allEnvelopes.Count -lt $totalSetSize -and $returned -gt 0) + +Write-Host "Total completed envelopes found: $($allEnvelopes.Count)" + +$logRows = New-Object System.Collections.Generic.List[object] +$counter = 0 + +foreach ($envelope in $allEnvelopes) { + $counter++ + $envelopeId = $envelope.envelopeId + $subject = Get-SafeFileName -Text $envelope.emailSubject + + $completedRaw = if ($envelope.completedDateTime) { $envelope.completedDateTime } else { $envelope.statusChangedDateTime } + $completedDate = if ($completedRaw) { + ([datetime]$completedRaw).ToString("yyyy-MM-dd") + } else { + "unknown-date" + } + + $fileName = "$completedDate - $subject - $envelopeId$extension" + $filePath = Join-Path $downloadFolder $fileName + $downloadUri = "$apiBase/envelopes/$envelopeId/documents/$documentId`?certificate=true" + + Write-Progress ` + -Activity "Downloading completed DocuSign agreements" ` + -Status "$counter of $($allEnvelopes.Count): $subject" ` + -PercentComplete (($counter / [math]::Max(1, $allEnvelopes.Count)) * 100) + + $status = "Downloaded" + $errorMessage = "" + + try { + Save-Bytes -Uri $downloadUri -Headers $downloadHeaders -Path $filePath -RetryCount $RetryCount + } + catch { + $status = "Failed" + $errorMessage = $_.Exception.Message + Write-Warning "Failed to download envelope $envelopeId`: $errorMessage" + } + + $logRows.Add([PSCustomObject]@{ + EnvelopeId = $envelopeId + Status = $status + EnvelopeStatus = $envelope.status + CompletedDate = $completedRaw + Subject = $envelope.emailSubject + OutputFormat = $OutputFormat + OutputPath = $filePath + Error = $errorMessage + }) +} + +$logRows | Export-Csv -Path $logPath -NoTypeInformation -Encoding UTF8 + +$downloaded = @($logRows | Where-Object { $_.Status -eq "Downloaded" }).Count +$failed = @($logRows | Where-Object { $_.Status -eq "Failed" }).Count + +Write-Host "" +Write-Host "DocuSign export complete." +Write-Host "Completed envelopes found: $($allEnvelopes.Count)" +Write-Host "Downloaded successfully: $downloaded" +Write-Host "Failed: $failed" +Write-Host "Files saved to: $downloadFolder" +Write-Host "Log saved to: $logPath" + +if ($failed -gt 0) { + Write-Warning "Some envelopes failed. Review the CSV log and rerun after resolving the errors." +} diff --git a/MDM-Enrollment.ps1 b/MDM-Enrollment.ps1 new file mode 100755 index 0000000..4b9557d --- /dev/null +++ b/MDM-Enrollment.ps1 @@ -0,0 +1,23 @@ +#!powershell + +''' + +Created: 2025-01-13 +Modified: 2025-01-15 +Description: This script will enroll the device into MDM. + +''' + +Write-Host -ForegroundColor Red "WARNING: This script will enroll the current device into MDM. This is meant for Add Source Group owned devices only. Do you want to continue? (Y/N)" +$answer = Read-Host + +if ($answer -ne "Y") { + Write-Output "Exiting script - Device will not be enrolled into MDM" + exit +} + +Write-Output "Enrolling device into MDM" + +Start-Process -FilePath 'C:\Windows\system32\deviceenroller.exe' -ArgumentList '/c /AutoEnrollMDM' + +Write-Host -ForegroundColor Green "Device enrolled script has completed. You can now close this window." \ No newline at end of file diff --git a/Remove-ClassicTeams.ps1 b/Remove-ClassicTeams.ps1 new file mode 100644 index 0000000..abfd793 --- /dev/null +++ b/Remove-ClassicTeams.ps1 @@ -0,0 +1,131 @@ +# Remove-ClassicTeams.ps1 +# Removes classic Microsoft Teams and Teams Machine-Wide Installer. +# Leaves new Teams / MSTeams MSIX app installed. + +$ErrorActionPreference = "Continue" + +Write-Output "Starting classic Teams cleanup..." + +# Stop Teams-related processes +$processNames = @( + "Teams", + "ms-teams", + "msteams", + "Microsoft.Teams", + "Update" +) + +foreach ($process in $processNames) { + Get-Process -Name $process -ErrorAction SilentlyContinue | + Stop-Process -Force -ErrorAction SilentlyContinue +} + +Start-Sleep -Seconds 5 + +# Remove Teams Machine-Wide Installer from HKLM uninstall registry +$uninstallPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" +) + +$machineWideInstallers = foreach ($path in $uninstallPaths) { + Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { + $_.DisplayName -eq "Teams Machine-Wide Installer" + } +} + +foreach ($app in $machineWideInstallers) { + Write-Output "Found Teams Machine-Wide Installer: $($app.DisplayName) $($app.DisplayVersion)" + + if ($app.UninstallString -match "\{[A-Fa-f0-9\-]{36}\}") { + $productCode = $Matches[0] + Write-Output "Removing Teams Machine-Wide Installer using MSI product code: $productCode" + Start-Process -FilePath "msiexec.exe" -ArgumentList "/x $productCode /qn /norestart" -Wait + } + elseif ($app.UninstallString) { + Write-Output "Attempting uninstall using uninstall string: $($app.UninstallString)" + Start-Process -FilePath "cmd.exe" -ArgumentList "/c $($app.UninstallString) /quiet /norestart" -Wait + } +} + +# Remove classic Teams per-user installs +$userProfiles = Get-ChildItem "C:\Users" -Directory -ErrorAction SilentlyContinue | Where-Object { + $_.Name -notin @( + "Public", + "Default", + "Default User", + "All Users", + "defaultuser0" + ) +} + +foreach ($profile in $userProfiles) { + $teamsFolder = Join-Path $profile.FullName "AppData\Local\Microsoft\Teams" + $updateExe = Join-Path $teamsFolder "Update.exe" + + if (Test-Path $updateExe) { + Write-Output "Uninstalling classic Teams for profile: $($profile.Name)" + Start-Process -FilePath $updateExe -ArgumentList "--uninstall -s" -Wait -ErrorAction SilentlyContinue + Start-Sleep -Seconds 3 + } + + if (Test-Path $teamsFolder) { + Write-Output "Removing remaining classic Teams folder for profile: $($profile.Name)" + Remove-Item -Path $teamsFolder -Recurse -Force -ErrorAction SilentlyContinue + } +} + +# Remove common classic Teams leftovers +$leftoverPaths = @( + "$env:ProgramData\Microsoft\Teams", + "$env:ProgramData\Microsoft\TeamsMachineInstaller" +) + +foreach ($path in $leftoverPaths) { + if (Test-Path $path) { + Write-Output "Removing leftover path: $path" + Remove-Item -Path $path -Recurse -Force -ErrorAction SilentlyContinue + } +} + +# Confirm new Teams remains installed +$newTeams = Get-AppxPackage -AllUsers | Where-Object { + $_.Name -eq "MSTeams" +} + +if ($newTeams) { + Write-Output "New Teams detected and left installed:" + $newTeams | Select-Object Name, Version, PackageFullName | Format-List | Out-String | Write-Output +} +else { + Write-Output "WARNING: New Teams / MSTeams was not detected after cleanup." +} + +# Final verification for classic Teams entries +$remainingMachineWide = foreach ($path in $uninstallPaths) { + Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { + $_.DisplayName -eq "Teams Machine-Wide Installer" + } +} + +$remainingClassicFolders = foreach ($profile in $userProfiles) { + $teamsFolder = Join-Path $profile.FullName "AppData\Local\Microsoft\Teams" + if (Test-Path $teamsFolder) { + $teamsFolder + } +} + +if ($remainingMachineWide -or $remainingClassicFolders) { + Write-Output "Classic Teams cleanup completed, but some remnants may remain." + if ($remainingMachineWide) { + $remainingMachineWide | Select-Object DisplayName, DisplayVersion | Format-Table | Out-String | Write-Output + } + if ($remainingClassicFolders) { + $remainingClassicFolders | ForEach-Object { Write-Output "Remaining folder: $_" } + } + exit 1 +} +else { + Write-Output "Classic Teams and Teams Machine-Wide Installer cleanup completed successfully." + exit 0 +} \ No newline at end of file diff --git a/Remove-DotNetCore31.ps1 b/Remove-DotNetCore31.ps1 new file mode 100644 index 0000000..b7a87ef --- /dev/null +++ b/Remove-DotNetCore31.ps1 @@ -0,0 +1,71 @@ +# Remove-DotNetCore31.ps1 +# Removes unsupported Microsoft .NET Core / ASP.NET Core / Windows Desktop Runtime / SDK 3.1.x components + +$ErrorActionPreference = "Continue" + +Write-Output "Starting .NET Core 3.1 remediation..." + +$registryPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" +) + +$targetApps = foreach ($path in $registryPaths) { + Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { + ( + $_.DisplayName -match "Microsoft \.NET.*3\.1\." -or + $_.DisplayName -match "Microsoft ASP\.NET Core.*3\.1\." -or + $_.DisplayName -match "Microsoft Windows Desktop Runtime.*3\.1\." + ) -and + $_.UninstallString + } +} + +if (-not $targetApps) { + Write-Output "No .NET Core 3.1 components found. Nothing to remove." + exit 0 +} + +foreach ($app in $targetApps) { + Write-Output "Found: $($app.DisplayName) $($app.DisplayVersion)" +} + +foreach ($app in $targetApps) { + Write-Output "Attempting to remove: $($app.DisplayName) $($app.DisplayVersion)" + + $uninstallString = $app.UninstallString + + if ($uninstallString -match "\{[A-Fa-f0-9\-]{36}\}") { + $productCode = $Matches[0] + Write-Output "Using MSI product code: $productCode" + Start-Process -FilePath "msiexec.exe" -ArgumentList "/x $productCode /qn /norestart" -Wait + } + else { + Write-Output "No MSI product code found for $($app.DisplayName). Skipping." + } +} + +Write-Output "Verifying remaining .NET Core 3.1 components..." + +$remainingApps = foreach ($path in $registryPaths) { + Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { + ( + $_.DisplayName -match "Microsoft \.NET.*3\.1\." -or + $_.DisplayName -match "Microsoft ASP\.NET Core.*3\.1\." -or + $_.DisplayName -match "Microsoft Windows Desktop Runtime.*3\.1\." + ) -and + $_.UninstallString + } +} + +if ($remainingApps) { + Write-Output "Some .NET Core 3.1 components may still remain:" + $remainingApps | ForEach-Object { + Write-Output "$($_.DisplayName) $($_.DisplayVersion)" + } + exit 1 +} +else { + Write-Output ".NET Core 3.1 components removed successfully." + exit 0 +} \ No newline at end of file diff --git a/Remove-SpecificTeamsVersions.ps1 b/Remove-SpecificTeamsVersions.ps1 new file mode 100644 index 0000000..d8f13fa --- /dev/null +++ b/Remove-SpecificTeamsVersions.ps1 @@ -0,0 +1,233 @@ +# Remove-SpecificTeamsVersions.ps1 +# Removes specified vulnerable/old versions of classic Microsoft Teams and Teams Machine-Wide Installer. +# Leaves new Teams / MSTeams MSIX app installed. + +$ErrorActionPreference = "Continue" + +Write-Output "Starting targeted Microsoft Teams cleanup..." + +# Versions to remove +$TargetVersions = @( + "24334.1103.3302.5694", + "25045.406.3471.2143", + "24124.2402.2858.5617", + "24004.1403.2634.2418", + "23195.1506.2253.6778", + "24277.3102.3183.2670", + "24256.2502.3123.1", + "24165.1306.2986.9504", + "25031.702.3408.1909", + "24244.507.3118.4732", + "23119.303.2080.2726", + "1.7.0.7956", + "25044.2208.3471.2155", + "24348.802.3311.5092", + "23335.242.2641.4129" +) + +# Stop Teams-related processes +$ProcessNames = @( + "Teams", + "ms-teams", + "msteams", + "Microsoft.Teams", + "Update" +) + +foreach ($ProcessName in $ProcessNames) { + Get-Process -Name $ProcessName -ErrorAction SilentlyContinue | + Stop-Process -Force -ErrorAction SilentlyContinue +} + +Start-Sleep -Seconds 5 + +# Registry uninstall locations +$UninstallRegistryPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" +) + +# Remove matching Teams Machine-Wide Installer versions +Write-Output "Checking for targeted Teams Machine-Wide Installer versions..." + +$MachineWideInstallers = foreach ($Path in $UninstallRegistryPaths) { + Get-ItemProperty $Path -ErrorAction SilentlyContinue | Where-Object { + $_.DisplayName -eq "Teams Machine-Wide Installer" -and + $TargetVersions -contains $_.DisplayVersion + } +} + +foreach ($App in $MachineWideInstallers) { + Write-Output "Found targeted Teams Machine-Wide Installer: $($App.DisplayVersion)" + + if ($App.UninstallString -match "\{[A-Fa-f0-9\-]{36}\}") { + $ProductCode = $Matches[0] + Write-Output "Removing Teams Machine-Wide Installer using MSI product code: $ProductCode" + + Start-Process -FilePath "msiexec.exe" ` + -ArgumentList "/x $ProductCode /qn /norestart" ` + -Wait + } + elseif ($App.UninstallString) { + Write-Output "Removing Teams Machine-Wide Installer using uninstall string." + + Start-Process -FilePath "cmd.exe" ` + -ArgumentList "/c $($App.UninstallString) /quiet /norestart" ` + -Wait + } + else { + Write-Output "No uninstall string found for Teams Machine-Wide Installer version $($App.DisplayVersion)." + } +} + +# Remove matching classic Teams per-user installs +Write-Output "Checking user profiles for targeted classic Teams versions..." + +$UserProfiles = Get-ChildItem "C:\Users" -Directory -ErrorAction SilentlyContinue | Where-Object { + $_.Name -notin @( + "Public", + "Default", + "Default User", + "All Users", + "defaultuser0" + ) +} + +foreach ($Profile in $UserProfiles) { + $TeamsFolder = Join-Path $Profile.FullName "AppData\Local\Microsoft\Teams" + $UpdateExe = Join-Path $TeamsFolder "Update.exe" + $CurrentExe = Join-Path $TeamsFolder "current\Teams.exe" + + if (-not (Test-Path $TeamsFolder)) { + continue + } + + $DetectedVersion = $null + + if (Test-Path $CurrentExe) { + $DetectedVersion = (Get-Item $CurrentExe).VersionInfo.FileVersion + } + + if (-not $DetectedVersion -and (Test-Path $UpdateExe)) { + $DetectedVersion = (Get-Item $UpdateExe).VersionInfo.FileVersion + } + + if ($TargetVersions -contains $DetectedVersion) { + Write-Output "Found targeted classic Teams version $DetectedVersion in profile: $($Profile.Name)" + + if (Test-Path $UpdateExe) { + Write-Output "Attempting silent uninstall for profile: $($Profile.Name)" + Start-Process -FilePath $UpdateExe ` + -ArgumentList "--uninstall -s" ` + -Wait ` + -ErrorAction SilentlyContinue + + Start-Sleep -Seconds 3 + } + + if (Test-Path $TeamsFolder) { + Write-Output "Removing remaining classic Teams folder for profile: $($Profile.Name)" + Remove-Item -Path $TeamsFolder -Recurse -Force -ErrorAction SilentlyContinue + } + } + else { + Write-Output "Skipping profile $($Profile.Name). Classic Teams version '$DetectedVersion' is not in target list." + } +} + +# Check HKCU-style per-user uninstall entries under user registry hives if loaded +Write-Output "Checking loaded user registry hives for targeted classic Teams uninstall entries..." + +$LoadedUserUninstallPaths = Get-ChildItem "Registry::HKEY_USERS" -ErrorAction SilentlyContinue | ForEach-Object { + "Registry::$($_.Name)\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" +} + +foreach ($Path in $LoadedUserUninstallPaths) { + $UserTeamsApps = Get-ItemProperty $Path -ErrorAction SilentlyContinue | Where-Object { + ( + $_.DisplayName -eq "Microsoft Teams" -or + $_.DisplayName -eq "MicrosoftTeams" + ) -and + $TargetVersions -contains $_.DisplayVersion + } + + foreach ($App in $UserTeamsApps) { + Write-Output "Found targeted per-user Teams uninstall entry: $($App.DisplayName) $($App.DisplayVersion)" + + if ($App.UninstallString) { + Start-Process -FilePath "cmd.exe" ` + -ArgumentList "/c $($App.UninstallString) --uninstall -s" ` + -Wait ` + -ErrorAction SilentlyContinue + } + } +} + +# Verify new Teams remains installed +Write-Output "Checking for new Teams / MSTeams..." + +$NewTeams = Get-AppxPackage -AllUsers -ErrorAction SilentlyContinue | Where-Object { + $_.Name -eq "MSTeams" +} + +if ($NewTeams) { + Write-Output "New Teams / MSTeams is installed and was left in place:" + $NewTeams | Select-Object Name, Version, PackageFullName | Format-List | Out-String | Write-Output +} +else { + Write-Output "WARNING: New Teams / MSTeams was not detected." +} + +# Final verification +Write-Output "Performing final verification..." + +$RemainingMachineWide = foreach ($Path in $UninstallRegistryPaths) { + Get-ItemProperty $Path -ErrorAction SilentlyContinue | Where-Object { + $_.DisplayName -eq "Teams Machine-Wide Installer" -and + $TargetVersions -contains $_.DisplayVersion + } +} + +$RemainingClassicTeamsFolders = foreach ($Profile in $UserProfiles) { + $TeamsFolder = Join-Path $Profile.FullName "AppData\Local\Microsoft\Teams" + $CurrentExe = Join-Path $TeamsFolder "current\Teams.exe" + + if (Test-Path $CurrentExe) { + $Version = (Get-Item $CurrentExe).VersionInfo.FileVersion + + if ($TargetVersions -contains $Version) { + [PSCustomObject]@{ + Profile = $Profile.Name + Version = $Version + Path = $TeamsFolder + } + } + } +} + +if ($RemainingMachineWide -or $RemainingClassicTeamsFolders) { + Write-Output "Targeted Teams cleanup completed, but some targeted versions may still remain." + + if ($RemainingMachineWide) { + Write-Output "Remaining Teams Machine-Wide Installer entries:" + $RemainingMachineWide | + Select-Object DisplayName, DisplayVersion, UninstallString | + Format-Table -AutoSize | + Out-String | + Write-Output + } + + if ($RemainingClassicTeamsFolders) { + Write-Output "Remaining classic Teams folders:" + $RemainingClassicTeamsFolders | + Format-Table -AutoSize | + Out-String | + Write-Output + } + + exit 1 +} +else { + Write-Output "Targeted Teams versions were successfully removed or were not present." + exit 0 +} \ No newline at end of file diff --git a/Sharepoint-List-Export.ps1 b/Sharepoint-List-Export.ps1 new file mode 100755 index 0000000..ce406c6 --- /dev/null +++ b/Sharepoint-List-Export.ps1 @@ -0,0 +1,9 @@ +Install-Module Microsoft.Graph.Authentication -Scope CurrentUser + +# --------------------------- + +& "Test-SharePointDownload.ps1" ` + -SiteUrl "https://zefiromethane.sharepoint.com/sites/zefirocorp" ` + -LibraryName "Documents" ` + -LocalRoot "F:\Zefiro Methane" ` + -OutputDirectory "F:\SharePointValidation" \ No newline at end of file diff --git a/User-Recertification.ps1 b/User-Recertification.ps1 new file mode 100644 index 0000000..bb8e759 --- /dev/null +++ b/User-Recertification.ps1 @@ -0,0 +1,74 @@ +# Export-M365UserCertification.ps1 +# Requires Microsoft Graph PowerShell SDK +# +# To run: .\User-Recertification.ps1 -OutputPath "C:\Users\Matt\OneDrive - Add Source Group\Documents\1- Projects\M365-User-Certification-Export.csv" + +param( + [string]$OutputPath = ".\M365-User-Certification-Export.csv" +) + +# Install if needed: +# Install-Module Microsoft.Graph -Scope CurrentUser + +Import-Module Microsoft.Graph.Users +Import-Module Microsoft.Graph.Identity.DirectoryManagement + +$Scopes = @( + "User.Read.All", + "Directory.Read.All", + "AuditLog.Read.All" +) + +Connect-MgGraph -Scopes $Scopes + +# Build SKU lookup so license GUIDs become readable names +$SkuLookup = @{} +Get-MgSubscribedSku -All | ForEach-Object { + $SkuLookup[$_.SkuId.ToString()] = $_.SkuPartNumber +} + +$Properties = @( + "id", + "displayName", + "userPrincipalName", + "userType", + "accountEnabled", + "assignedLicenses", + "externalUserState", + "signInActivity" +) + +$Users = Get-MgUser -All -Property $Properties + +$Export = foreach ($User in $Users) { + $AssignedLicenses = if ($User.AssignedLicenses.Count -gt 0) { + ($User.AssignedLicenses | ForEach-Object { + $SkuId = $_.SkuId.ToString() + if ($SkuLookup.ContainsKey($SkuId)) { + $SkuLookup[$SkuId] + } + else { + $SkuId + } + }) -join "; " + } + else { + "None" + } + + [PSCustomObject]@{ + Name = $User.DisplayName + UserPrincipalName = $User.UserPrincipalName + UserType = $User.UserType + AccountEnabled = $User.AccountEnabled + AssignedLicenses = $AssignedLicenses + GuestInvitationState = $User.ExternalUserState + LastInteractiveSignInDate = $User.SignInActivity.LastSignInDateTime + } +} + +$Export | + Sort-Object Name | + Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 + +Write-Host "Export complete: $OutputPath" \ No newline at end of file diff --git a/scripts/AddDNSClient.ps1 b/scripts/AddDNSClient.ps1 new file mode 100755 index 0000000..51ed2a0 --- /dev/null +++ b/scripts/AddDNSClient.ps1 @@ -0,0 +1,56 @@ +#Requires -RunAsAdministrator + +<# +.SYNOPSIS + Configures static DNS server addresses on active network adapters. + +.DESCRIPTION + This script retrieves all active (connected) network interfaces on the endpoint and assigns + primary DNS (Quad9) and secondary backup DNS (Cloudflare) servers. + +.NOTES + Created: 2025-03-02 + Modified: 2026-10-04 + + DNS Server Mappings: + - Quad9 Primary: 9.9.9.9 (Malware blocking + DNSSEC) + - Quad9 Secondary: 149.112.112.11 (Malware blocking + DNSSEC) + - Cloudflare Prim: 1.1.1.2 (Malware blocking) + - Cloudflare Sec: 1.0.0.2 (Malware blocking) + + * Note: If adult content blocking is also desired via Cloudflare, + use 1.1.1.3 and 1.0.0.3 instead. +#> + +[CmdletBinding(SupportsShouldProcess = $true)] +param ( + # Array of DNS IPv4 addresses in priority order + [string[]]$DnsServers = @("9.9.9.9", "149.112.112.11", "1.1.1.2", "1.0.0.2") +) + +# 1. Retrieve active/connected network adapters (filters out disabled, virtual, or disconnected NICs) +$ActiveAdapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } + +if (-not $ActiveAdapters) { + Write-Warning "No active network adapters found on this endpoint." + return +} + +# 2. Apply DNS settings to each active interface +foreach ($Adapter in $ActiveAdapters) { + + $InterfaceName = $Adapter.Name + $InterfaceIndex = $Adapter.InterfaceIndex + + if ($PSCmdlet.ShouldProcess("Interface: $InterfaceName (Index: $InterfaceIndex)", "Set DNS servers to $($DnsServers -join ', ')")) { + try { + # Set the static DNS client addresses + Set-DnsClientServerAddress -InterfaceIndex $InterfaceIndex -ServerAddresses $DnsServers -ErrorAction Stop + + Write-Host "Successfully updated DNS for '$InterfaceName' (Index: $InterfaceIndex)" -ForegroundColor Green + } + catch { + Write-Error "Failed to update DNS on interface '$InterfaceName' (Index: $InterfaceIndex). Error: $_" + } + } +} \ No newline at end of file diff --git a/scripts/BitlockerRegFix.ps1 b/scripts/BitlockerRegFix.ps1 new file mode 100755 index 0000000..163d6fb --- /dev/null +++ b/scripts/BitlockerRegFix.ps1 @@ -0,0 +1,90 @@ +#Requires -RunAsAdministrator + +<# +.SYNOPSIS + Triggers the Windows TPM Health Attestation Certificate Retrieval scheduled task. + +.DESCRIPTION + Executes the '\Microsoft\Windows\TPM\Tpm-HASCertRetr' scheduled task to resolve + BitLocker device registration, Health Attestation, or TPM certificate retrieval + issues on endpoints (frequently used in Microsoft Intune environments). + +.NOTES + Created: 2025-01-15 + Modified: 2026-10-04 +#> + +[CmdletBinding(SupportsShouldProcess = $true)] +param ( + [string]$LogDirectory = "C:\IntuneLogs", + [string]$LogFileName = "BitlockerRegFix.log", + [string]$TaskPath = "\Microsoft\Windows\TPM\", + [string]$TaskName = "Tpm-HASCertRetr" +) + +# Build full log file path +$LogPath = Join-Path -Path $LogDirectory -ChildPath $LogFileName + +# Helper function for structured, timestamped logging +function Write-Log { + param ( + [string]$Message, + [ValidateSet("INFO", "WARNING", "ERROR")] + [string]$Level = "INFO" + ) + $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $FormattedMessage = "[$Timestamp] [$Level] $Message" + + # Write to console / verbose stream + if ($Level -eq "ERROR") { + Write-Error $Message + } elseif ($Level -eq "WARNING") { + Write-Warning $Message + } else { + Write-Host $FormattedMessage + } + + # Append to log file safely + try { + if (-not (Test-Path -Path $LogDirectory)) { + New-Item -Path $LogDirectory -ItemType Directory -Force | Out-Null + } + Add-Content -Path $LogPath -Value $FormattedMessage -ErrorAction SilentlyContinue + } + catch { + # Fallback if logging fails + } +} + +# --- Execution Logic --- + +Write-Log "Starting BitLocker/TPM registration fix process..." + +# 1. Verify scheduled task exists on the system +$Task = Get-ScheduledTask -TaskPath $TaskPath -TaskName $TaskName -ErrorAction SilentlyContinue + +if (-not $Task) { + Write-Log "Scheduled task '$TaskPath$TaskName' was not found on this endpoint." -Level "ERROR" + exit 1 +} + +# 2. Trigger the scheduled task +if ($PSCmdlet.ShouldProcess("Scheduled Task: $TaskPath$TaskName", "Execute Task")) { + try { + Write-Log "Executing scheduled task '$TaskPath$TaskName'..." + + # Trigger task run + Start-ScheduledTask -TaskPath $TaskPath -TaskName $TaskName -ErrorAction Stop + + # Brief pause to verify task state + Start-Sleep -Seconds 2 + $UpdatedTask = Get-ScheduledTask -TaskPath $TaskPath -TaskName $TaskName + + Write-Log "Successfully triggered task. Current Task State: $($UpdatedTask.State)" -Level "INFO" + exit 0 + } + catch { + Write-Log "Failed to execute scheduled task '$TaskPath$TaskName'. Error: $_" -Level "ERROR" + exit 1 + } +} \ No newline at end of file diff --git a/asg_website_backup.sh b/scripts/asg_website_backup.sh similarity index 100% rename from asg_website_backup.sh rename to scripts/asg_website_backup.sh diff --git a/website/default.html b/website/default.html new file mode 100755 index 0000000..26d65cd --- /dev/null +++ b/website/default.html @@ -0,0 +1,378 @@ + + + + + +Add Source Group Site + + + + + + +
    + + +
    +
    + + +
    +

    Accounting · Finance · HR

    +

    Experienced accounting professionals, when you need them.

    +

    Add Source Group is a professional services and placement firm. We provide high-caliber resources on an interim or permanent basis and partner with clients across the country to build strong, efficient teams.

    + +
    + +
    +
    4–30+ yearsExperience across our professionals
    +
    Interim or permanentFlexible engagement models
    +
    30–50%Typical savings over peer firms
    +
    + +
    +

    What we do

    Strengthen your Accounting, Finance and HR teams.

    + +
    + +
    +

    Why ASG

    A partner built for long-term value.

    +
    +

    Flexible timing

    Scale up or down as your needs change, with smooth transitions to permanent hires.

    +

    Qualified professionals

    Practitioners with public and corporate accounting experience.

    +

    Consistent teams

    The same people return over time, so they already know your business.

    +

    Reasonable rates

    A low-overhead model means deep experience at a fair price.

    +
    +
    + +

    Build your highly skilled team.

    Whether you need an executive-level leader or an experienced staff member, we can help.

    Get in touch
    + + +
    +

    Services

    How we can help

    Our professionals integrate with your team and serve as technical resources for public and private companies.

    +
    +

    Management Resources

    +

    Interim and long-term leaders for significant growth, new-company start-ups, or major organizational change — ready to quickly provide oversight of critical business functions.

    +

    Supplemental Team Members

    +

    Temporary or long-term professionals for when your team needs more capacity:

    +
    • Open positions and leave coverage
    • Monthly, quarterly and annual deadlines
    • Acquisitions and new system implementations
    • Outsourced accounting, start-ups and wind-downs
    +

    Recruiting & Placement

    +

    We build relationships with qualified professionals and businesses, because understanding the goals of both leads to successful placements. Our dedicated recruiters source through a deep, active network.

    +
    +
    + + +
    +
    +

    Our purpose

    +
    We work each day to help companies be more successful by providing the highest quality resources and services that add value to their business through meaningful partnerships.
    +
    +

    Core beliefs

    What guides us.

    +
    +
    Faith

    We believe in honoring God in all that we do.

    +
    Integrity

    We believe in doing the right thing, the right way, and at the right time.

    +
    Relationships

    We believe that continually building new and existing relationships is essential to who we are and what we do.

    +
    Service

    We believe in helping others, making people and our clients better.

    +
    Initiative

    We believe in taking the initiative and persevering through to the end — proactively looking for opportunities to help our clients and add value.

    +
    Excellence

    We believe in striving for excellence in all that we do.

    +
    +
    + + +
    +

    About us

    Practitioners who have sat in your seat.

    Add Source Group specializes in Accounting, Finance and HR. Our team brings Big 4 and corporate leadership experience to clients across many industries, from day-to-day needs to complex projects.

    +
    +
    Portrait of Russ Lynch

    Russ Lynch

    Managing Partner

    25+ years of corporate finance and accounting leadership. CPA.

    +
    Portrait of Wade Watkins

    Wade Watkins

    Managing Partner

    20+ years of public accounting and client service leadership. CPA.

    +
    Portrait of Ely Jones

    Ely Jones

    Managing Director, Business Development & Recruiting

    CPA (Inactive).

    +
    Portrait of Jenny Sneed

    Jenny Sneed

    Director, Business Development & Recruiting

    CPA (Inactive).

    +
    +
    + + +
    +

    Contact

    Let's build your team.

    Tell us what you need. Whether you're looking for interim support, project help, or your next opportunity, we'd love to hear from you.

    +
    +
    + +

    Need talent or project help?

    +

    contact@addsourcegroup.com

    + Email Us +
    +
    + +

    Looking for an opportunity?

    +

    candidates@addsourcegroup.com

    + Contact Our Recruiting Team +
    +
    +
    + +
    + + + + + + diff --git a/website/favicon.ico b/website/favicon.ico new file mode 100755 index 0000000..af5b84b Binary files /dev/null and b/website/favicon.ico differ diff --git a/website/robots.txt b/website/robots.txt new file mode 100755 index 0000000..de3ad21 --- /dev/null +++ b/website/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://addsourcegroup.com/sitemap.xml \ No newline at end of file diff --git a/website/sitemap.xml b/website/sitemap.xml new file mode 100755 index 0000000..22992ac --- /dev/null +++ b/website/sitemap.xml @@ -0,0 +1,6 @@ + + + + https://addsourcegroup.com/ + + \ No newline at end of file