Initial commit for powershell scripts.

This commit is contained in:
mattspeer committed 2026-10-04 10:22:22 -05:00
1 parent f4e5d9dcbd
commit 4fd95b76d6
18 files changed
+1803

No files matched your search

+1
View File
@@ -0,0 +1 @@
.DS_Store
+28
View File
@@ -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
+209
View File
@@ -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(
"<li class='error'>Unable to read: $(ConvertTo-HtmlEncoded $_.Exception.Message)</li>"
)
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(
"<li><details data-path='$pathHtml'><summary>📁 $nameHtml</summary><ul>"
)
Add-TreeItems -Path $item.FullName -Prefix $nextPrefix
[void]$script:html.AppendLine('</ul></details></li>')
}
elseif ($item.PSIsContainer -and $isLink) {
$target = @($item.Target) -join ', '
[void]$script:textLines.Add(
"$Prefix$connector$($item.Name) [Link → $target]"
)
[void]$script:html.AppendLine(
"<li class='link' data-path='$pathHtml'>🔗 $nameHtml <small>→ $(ConvertTo-HtmlEncoded $target)</small></li>"
)
}
else {
[void]$script:textLines.Add("$Prefix$connector$($item.Name)")
[void]$script:html.AppendLine(
"<li class='file' data-path='$pathHtml'>📄 $nameHtml</li>"
)
}
}
}
$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(@"
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Team Site Documents Directory</title>
<style>
body {
font-family: "Segoe UI", Arial, sans-serif;
margin: 24px;
color: #1f2328;
background: #fff;
}
h1 { margin-bottom: 6px; }
.meta { color: #57606a; margin-bottom: 18px; }
.controls {
position: sticky;
top: 0;
background: #fff;
padding: 10px 0;
border-bottom: 1px solid #d0d7de;
z-index: 2;
}
input {
width: min(600px, 70vw);
padding: 8px;
font-size: 14px;
}
button {
padding: 8px 12px;
margin-left: 6px;
}
ul {
list-style: none;
margin: 4px 0 4px 20px;
padding-left: 10px;
border-left: 1px solid #d8dee4;
}
.tree { border-left: 0; margin-left: 0; }
summary { cursor: pointer; padding: 3px; }
.file, .link { padding: 3px; }
.link { color: #8250df; }
.error { color: #cf222e; }
small { color: #57606a; }
.hidden { display: none !important; }
</style>
</head>
<body>
<h1>Team Site – Documents</h1>
<div class="meta">
Generated $generated<br>
Root: <code>$rootPathHtml</code>
</div>
<div class="controls">
<input id="search" type="search" placeholder="Search folders and files">
<button type="button" onclick="setAll(true)">Expand all</button>
<button type="button" onclick="setAll(false)">Collapse all</button>
</div>
<ul class="tree">
<li>
<details open data-path="$rootPathHtml">
<summary>📁 $rootNameHtml</summary>
<ul>
"@)
Add-TreeItems -Path $root -Prefix ''
[void]$script:html.AppendLine(@"
</ul>
</details>
</li>
</ul>
<script>
function setAll(state) {
document.querySelectorAll('details').forEach(function (item) {
item.open = state;
});
}
document.getElementById('search').addEventListener('input', function () {
const term = this.value.trim().toLowerCase();
const nodes = Array.from(document.querySelectorAll('[data-path]'));
nodes.forEach(function (node) {
node.classList.remove('hidden');
});
if (!term) return;
nodes.forEach(function (node) {
if (!node.dataset.path.toLowerCase().includes(term)) {
node.classList.add('hidden');
}
});
nodes.filter(function (node) {
return node.dataset.path.toLowerCase().includes(term);
}).forEach(function (node) {
node.classList.remove('hidden');
let parent = node.parentElement;
while (parent) {
if (parent.tagName === 'DETAILS') {
parent.open = true;
parent.classList.remove('hidden');
}
parent = parent.parentElement;
}
});
});
</script>
</body>
</html>
"@)
$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
+112
View File
@@ -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
+378
View File
@@ -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."
}
+23
View File
@@ -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."
+131
View File
@@ -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
}
+71
View File
@@ -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
}
+233
View File
@@ -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
}
+9
View File
@@ -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"
+74
View File
@@ -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"
+56
View File
@@ -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: $_"
}
}
}
+90
View File
@@ -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
}
}
File renamed without changes.
+378
View File
@@ -0,0 +1,378 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Add Source Group Site</title>
<link rel="icon" href="/favicon.ico?v=4">
<style>
@font-face {
font-family: 'Jost';
src: url('./fonts/jost.ttf') format('truetype');
font-style: normal;
font-weight: 100 900;
font-display: swap;
}
@font-face {
font-family: 'Cormorant Garamond';
src: url('./fonts/cormorant-garamond.ttf') format('truetype');
font-style: normal;
font-weight: 300 700;
font-display: swap;
}
:root{
--navy:#24507D; --logo-green:#3FC166; --logo-teal:#14B5C6; --green-ink:#259A4F; --btn:#259A4F; --btn-hover:#1E8544;
--ink:#1F2933; --muted:#4A5563; --ground:#EDEAE6; --surface:#FAF9F7; --line:#DAD5CF; --tint:#E6F0E4; --card:#FFFFFF; --head:#24507D; --band:#24507D; --onband:#E3ECF5;
--serif:'Cormorant Garamond',Garamond,Georgia,serif; --sans:'Jost','Futura','Helvetica Neue',Arial,sans-serif;
}
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){
color-scheme:dark; --ink:#E6EEE9; --muted:#AEBBB3; --ground:#141B26; --surface:#0F151F; --line:#2A3444; --tint:#1B3A2A; --card:#172131; --head:#EEF5F0; --green-ink:#5FD084; --band:#1D3C5E; --onband:#B9C6D6;
}}
:root[data-theme="dark"]{
color-scheme:dark; --ink:#E6EEE9; --muted:#AEBBB3; --ground:#141B26; --surface:#0F151F; --line:#2A3444; --tint:#1B3A2A; --card:#172131; --head:#EEF5F0; --green-ink:#5FD084; --band:#1D3C5E; --onband:#B9C6D6;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
@media (prefers-reduced-motion: reduce){html{scroll-behavior:auto}}
body{margin:0;background:var(--surface);color:var(--ink);font-family:var(--sans);font-size:17px;line-height:1.65}
a{color:var(--green-ink)} a:hover{color:var(--head)}
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid var(--logo-green);outline-offset:2px}
.wrap{max-width:1140px;margin:0 auto;padding-inline:clamp(16px,5vw,48px)}
h1,h2,h3{font-family:var(--serif);font-weight:500;color:var(--head);margin:0;text-wrap:balance;line-height:1.08;letter-spacing:.005em}
h1{font-size:clamp(44px,6.4vw,78px);font-weight:400}
h2{font-size:clamp(34px,4.4vw,52px);font-weight:400}
h3{font-size:27px;line-height:1.2;font-weight:500}
p{margin:0}
.eyebrow{font-size:12px;font-weight:500;letter-spacing:.26em;text-transform:uppercase;color:var(--green-ink)}
.lede{font-size:clamp(18px,2vw,21px);color:var(--muted);max-width:60ch}
.muted{color:var(--muted)}
.stack{display:flex;flex-direction:column;gap:16px}
.btn{display:inline-block;font-weight:500;font-size:12px;letter-spacing:.22em;text-transform:uppercase;text-decoration:none;padding:16px 30px;border-radius:0;border:1.5px solid transparent;font-family:inherit;cursor:pointer}
.btn.primary{background:var(--btn);color:#fff}
.btn.primary:hover{background:var(--btn-hover);color:#fff}
.btn.ghost{border-color:var(--head);color:var(--head)}
header.site{position:sticky;top:env(safe-area-inset-top,0px);z-index:5;background:var(--surface);border-bottom:1px solid var(--line)}
header.site .wrap{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:92px;flex-wrap:wrap;padding-block:10px}
.logo{display:flex;align-items:center;gap:10px;text-decoration:none;color:#58585A}
.logo .mark{display:flex;align-items:flex-start;font-weight:700;font-size:44px;line-height:1;letter-spacing:-1px}
.logo .plus{width:24px;height:24px;margin:-9px 0 0 3px;border-radius:5px;background:linear-gradient(180deg,var(--logo-teal),var(--logo-green));color:#fff;font-size:21px;line-height:24px;text-align:center}
.logo .words{font-size:12px;font-weight:600;letter-spacing:.12em;line-height:1.15}
:root[data-theme="dark"] header.site .logo{color:#D5DBD8}
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]) header.site .logo{color:#D5DBD8}}
nav.main{display:flex;gap:24px;align-items:center;flex-wrap:wrap}
nav.main a{text-decoration:none;color:var(--muted);font-weight:400;font-size:13px;letter-spacing:.22em;text-transform:uppercase}
nav.main a:hover{color:var(--head)}
nav.main .btn{padding:11px 18px;font-size:12px;color:#fff}
@media (max-width:640px){nav.main{gap:14px} nav.main a{font-size:12px} nav.main .btn{display:none}}
.stripe{height:5px;background:linear-gradient(90deg,var(--logo-teal),var(--logo-green))}
section{padding-block:clamp(60px,8vw,96px)}
.alt{background:var(--ground)}
.hero{padding-block:24px 0}
.herocard{background:var(--navy);border-radius:2px;padding:clamp(40px,7vw,96px) clamp(24px,6vw,80px);position:relative;overflow:hidden}
.herocard::after{content:"";position:absolute;left:0;right:0;bottom:0;height:6px;background:linear-gradient(90deg,var(--logo-teal),var(--logo-green))}
.hero .stack{gap:24px;max-width:780px}
.hero h1{color:#FBF7F0}
.hero .eyebrow{color:var(--logo-green)}
.hero .lede{color:#E3ECF5}
.hero .btn.ghost{border-color:#FBF7F0;color:#FBF7F0}
.hero .btn.ghost:hover{background:#FBF7F0;color:var(--navy)}
.hero .cta{display:flex;gap:14px;flex-wrap:wrap;margin-top:6px}
.stats{padding-block:0;border-bottom:1px solid var(--line)}
.stats .wrap{display:grid;grid-template-columns:repeat(3,minmax(0,1fr))}
.stat{padding:28px 24px;border-left:1px solid var(--line);display:flex;flex-direction:column;gap:4px}
.stat:first-child{border-left:0;padding-left:0}
.stat b{font-family:var(--serif);font-weight:500;font-size:44px;line-height:1.1;color:var(--green-ink);font-variant-numeric:tabular-nums}
.stat span{font-size:15px;color:var(--muted);line-height:1.4}
@media (max-width:640px){.stats .wrap{grid-template-columns:1fr}.stat{border-left:0;border-top:1px solid var(--line);padding-left:0}.stat:first-child{border-top:0}}
.head{display:flex;flex-direction:column;gap:12px;max-width:720px;margin-bottom:40px}
.grid4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:22px}
.grid3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:22px}
.grid2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:22px}
@media (max-width:980px){.grid4{grid-template-columns:repeat(2,minmax(0,1fr))}.grid3{grid-template-columns:1fr}}
@media (max-width:600px){.grid4,.grid2{grid-template-columns:1fr}}
.svc{display:flex;flex-direction:column;gap:12px;padding:34px 30px;background:var(--card);border:1px solid var(--line);border-radius:2px;text-decoration:none}
.svc p{font-size:16px;color:var(--muted)}
.ico{width:48px;height:48px;border-radius:50%;background:var(--tint);display:flex;align-items:center;justify-content:center}
.ico svg{stroke:var(--green-ink)}
.why{display:flex;flex-direction:column;gap:8px;padding-top:20px;border-top:1px solid var(--logo-green)}
.why h3{font-size:32px;font-weight:400}
.why p{font-size:16px;color:var(--muted)}
.band{background:var(--band);border-radius:2px;padding:clamp(32px,5vw,56px);display:flex;justify-content:space-between;align-items:center;gap:28px;flex-wrap:wrap;position:relative;overflow:hidden}
.band::before{content:"";position:absolute;inset:0 0 auto 0;height:5px;background:linear-gradient(90deg,var(--logo-teal),var(--logo-green))}
.band h2{color:#fff}
.band p{color:var(--onband);max-width:56ch;margin-top:8px}
.band .btn{background:transparent;color:#fff;border-color:#fff}
.band .btn:hover{background:#fff;color:var(--navy)}
.svcrow{display:grid;grid-template-columns:minmax(0,4fr) minmax(0,8fr);gap:32px;padding-block:22px;border-top:1px solid var(--line);align-items:start}
.svcrow:first-child{border-top:0;padding-top:0}
@media (max-width:820px){.svcrow{grid-template-columns:1fr;gap:16px}}
.svcrow .left{display:flex;gap:14px;align-items:center}
#services .ico{width:40px;height:40px}
#services .stack{gap:8px!important}
ul.list{margin:0;padding-left:20px;display:flex;flex-direction:column;gap:4px;color:var(--muted);font-size:17px}
ul.list li::marker{color:var(--logo-green)}
.team{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:24px}
@media (max-width:980px){.team{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (max-width:480px){.team{grid-template-columns:1fr}}
.member{display:flex;flex-direction:column;gap:10px}
.member img{width:100%;aspect-ratio:4/5;object-fit:cover;object-position:center top;border-radius:2px;display:block;max-width:100%}
.member h3{font-size:28px;font-weight:500}
.member .t{font-size:12px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--green-ink);line-height:1.4}
.member p{font-size:15px;color:var(--muted)}
.contactgrid{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,6fr);gap:52px}
@media (max-width:900px){.contactgrid{grid-template-columns:1fr}}
.people{display:flex;flex-direction:column}
.person{padding-block:16px;border-top:1px solid var(--line)}
.person b{display:block;color:var(--head);font-size:17px}
.person span{font-size:15px;color:var(--muted);word-break:break-word}
form.card{background:var(--card);border:1px solid var(--line);border-radius:2px;padding:clamp(24px,4vw,40px);display:flex;flex-direction:column;gap:18px}
.row2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}
@media (max-width:560px){.row2{grid-template-columns:1fr}}
.field{display:flex;flex-direction:column;gap:6px}
.field label{font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--head)}
.field input,.field select,.field textarea{width:100%;font:inherit;font-size:17px;color:var(--ink);background:var(--card);border:1px solid #9AABA1;border-radius:6px;padding:12px 14px}
.field textarea{min-height:130px;resize:vertical}
form.card button{align-self:flex-start;border:0}
.sent{font-size:15px;color:var(--green-ink);font-weight:600}
footer.site{background:var(--band);color:var(--onband);font-size:15px;padding-block:44px 28px}
footer.site .top{display:flex;justify-content:space-between;gap:28px;flex-wrap:wrap;align-items:flex-start}
footer.site .logo{color:#fff}
footer.site nav{display:flex;gap:24px;flex-wrap:wrap}
footer.site a{color:#fff;text-decoration:none}
footer.site a:hover{color:var(--logo-green)}
footer.site .base{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;border-top:1px solid #436E9B;margin-top:32px;padding-top:18px;font-size:14px}
/* ---------- motion ---------- */
header.site{transition:box-shadow .3s ease}
header.site .wrap{transition:min-height .3s ease}
header.site.scrolled{box-shadow:0 6px 24px rgba(20,30,45,.08)}
header.site.scrolled .wrap{min-height:70px}
.logo .mark,.logo .plus,.logo .words{transition:transform .3s ease}
header.site.scrolled .logo{transform-origin:left center}
header.site.scrolled .logo .mark{transform:scale(.86)}
nav.main a:not(.btn){position:relative}
nav.main a:not(.btn)::after{content:"";position:absolute;left:0;right:0;bottom:-6px;height:1px;background:var(--logo-green);transform:scaleX(0);transform-origin:left;transition:transform .35s ease}
nav.main a:not(.btn):hover::after{transform:scaleX(1)}
.btn{transition:background-color .25s ease,color .25s ease,border-color .25s ease,transform .25s ease,box-shadow .25s ease}
.btn:hover{transform:translateY(-2px);box-shadow:0 10px 22px rgba(20,30,45,.14)}
.svc{transition:transform .35s cubic-bezier(.2,.7,.2,1),box-shadow .35s ease,border-color .35s ease}
.svc:hover{transform:translateY(-8px);box-shadow:0 18px 40px rgba(20,30,45,.12);border-color:var(--logo-green)}
.svc .ico{transition:transform .4s cubic-bezier(.3,1.6,.5,1),background-color .3s ease}
.svc:hover .ico{transform:scale(1.14) rotate(-6deg);background:var(--logo-green)}
.svc:hover .ico svg{stroke:#fff}
.why{transition:border-color .3s ease}
.why h3{transition:color .3s ease,transform .3s ease}
.why:hover h3{color:var(--green-ink);transform:translateX(6px)}
.member .ph{overflow:hidden;border-radius:2px}
.member img{transition:transform .8s cubic-bezier(.2,.7,.2,1),filter .5s ease;filter:saturate(.92)}
.member:hover img{transform:scale(1.06);filter:saturate(1.05)}
.person{transition:padding-left .3s ease}
.person:hover{padding-left:10px}
.herocard::after{background-size:200% 100%;animation:sheen 6s linear infinite;background-image:linear-gradient(90deg,var(--logo-teal),var(--logo-green),var(--logo-teal))}
@keyframes sheen{to{background-position:-200% 0}}
@keyframes rise{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:none}}
.anim .hero .eyebrow{animation:rise .8s .05s both cubic-bezier(.2,.7,.2,1)}
.anim .hero h1{animation:rise .9s .15s both cubic-bezier(.2,.7,.2,1)}
.anim .hero .lede{animation:rise .9s .3s both cubic-bezier(.2,.7,.2,1)}
.anim .hero .cta{animation:rise .9s .45s both cubic-bezier(.2,.7,.2,1)}
.anim .reveal{opacity:0;transform:translateY(34px) scale(.97);transition:opacity .7s ease,transform .8s cubic-bezier(.2,.7,.2,1);transition-delay:var(--d,0s)}
.anim .reveal.in{opacity:1;transform:none}
.anim .svc.reveal.in:hover{transform:translateY(-8px)}
@media (prefers-reduced-motion: reduce){
*,*::before,*::after{animation:none!important;transition:none!important}
.anim .reveal{opacity:1;transform:none}
}
/* purpose & beliefs */
.purpose{background:var(--ground)}
.purpose-statement{background:var(--navy);color:#fff;padding:clamp(40px,6vw,80px) clamp(24px,6vw,88px);position:relative;border-radius:2px}
.purpose-statement::before{content:"\201C";position:absolute;top:-8px;left:clamp(12px,3vw,36px);font-family:var(--serif);font-size:clamp(120px,14vw,200px);line-height:1;color:var(--logo-green);opacity:.55}
.purpose-statement blockquote::after {
content: "\201D";
display: inline;
margin-left: 0.08em;
font-family: var(--serif);
font-size: clamp(120px, 14vw, 200px);
line-height: 0;
vertical-align: -0.35em;
color: var(--logo-green);
opacity: .55;
position: relative;
top: clamp(32.5px, 4.25vw, 52.5px);
}
.purpose-statement .eyebrow{color:var(--logo-green);position:relative}
.purpose-statement blockquote{margin:18px 0 0;font-family:var(--serif);font-weight:400;font-size:clamp(26px,3.4vw,42px);line-height:1.25;color:#FBF7F0;max-width:30ch;position:relative;text-wrap:balance}
.beliefs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0;border-top:1px solid var(--line)}
.belief{padding:28px 28px 32px 0;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:8px}
.belief .bn{font-family:var(--serif);font-size:30px;line-height:1.1;color:var(--head)}
.belief p{font-size:16px;color:var(--muted);max-width:38ch}
.belief .bn{transition:color .3s ease,transform .3s ease}
.belief:hover .bn{color:var(--green-ink);transform:translateX(6px)}
@media (max-width:900px){.beliefs{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (max-width:560px){.beliefs{grid-template-columns:1fr}.belief{padding-right:0}}
/* contact cards */
.contact-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px}
@media (max-width:760px){.contact-cards{grid-template-columns:1fr}}
.contact-card{background:var(--card);border:1px solid var(--line);border-radius:2px;padding:clamp(28px,4vw,44px);display:flex;flex-direction:column;align-items:flex-start;gap:14px;transition:transform .35s cubic-bezier(.2,.7,.2,1),box-shadow .35s ease,border-color .35s ease}
.contact-card:hover{transform:translateY(-6px);box-shadow:0 18px 40px rgba(20,30,45,.12);border-color:var(--logo-green)}
.anim .contact-card.reveal.in:hover{transform:translateY(-6px)}
.contact-card .addr{font-family:var(--serif);font-size:clamp(22px,2.4vw,28px);color:var(--head);word-break:break-word;user-select:all}
.contact-card .btn{margin-top:6px}
</style>
</head>
<body>
<header class="site"><div class="wrap">
<a class="logo" href="#home" aria-label="Add Source Group home"><span class="mark">ASG<span class="plus">+</span></span><span class="words">ADD<br>SOURCE<br>GROUP</span></a>
<nav class="main" aria-label="Main"><a href="#home">Home</a><a href="#services">Services</a><a href="#purpose">Our Beliefs</a><a href="#about">About</a><a href="#contact">Contact</a><a class="btn primary" href="#contact">Get in touch</a></nav>
</div></header>
<div class="stripe"></div>
<!-- HOME -->
<section id="home" class="hero"><div class="wrap"><div class="herocard"><div class="stack">
<p class="eyebrow">Accounting · Finance · HR</p>
<h1>Experienced accounting professionals, when you need them.</h1>
<p class="lede">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.</p>
<div class="cta"><a class="btn primary" href="#contact">Find talent</a><a class="btn ghost" href="#services">Our services</a></div>
</div></div></div></section>
<section class="stats"><div class="wrap">
<div class="stat"><b>4–30+ years</b><span>Experience across our professionals</span></div>
<div class="stat"><b>Interim or permanent</b><span>Flexible engagement models</span></div>
<div class="stat"><b>30–50%</b><span>Typical savings over peer firms</span></div>
</div></section>
<section><div class="wrap">
<div class="head"><p class="eyebrow">What we do</p><h2>Strengthen your Accounting, Finance and HR teams.</h2></div>
<div class="grid3">
<a class="svc" href="#services"><span class="ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20V10l8-6 8 6v10"/><path d="M9 20v-6h6v6"/></svg></span><h3>Management Resources</h3><p>Interim and long-term leaders for growth, start-ups and periods of change.</p></a>
<a class="svc" href="#services"><span class="ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="8" r="3.5"/><path d="M2.5 20c.8-3.6 3.4-5.5 6.5-5.5s5.7 1.9 6.5 5.5"/><path d="M16 5.2a3.4 3.4 0 0 1 0 6.1"/><path d="M18 14.8c1.8.8 3 2.6 3.5 5.2"/></svg></span><h3>Supplemental Team Members</h3><p>Experienced professionals to fill gaps, meet deadlines and support projects.</p></a>
<a class="svc" href="#services"><span class="ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="M20 20l-4.2-4.2"/></svg></span><h3>Recruiting &amp; Placement</h3><p>Permanent hires, from staff accountants to CFOs.</p></a>
</div>
</div></section>
<section class="alt"><div class="wrap">
<div class="head"><p class="eyebrow">Why ASG</p><h2>A partner built for long-term value.</h2></div>
<div class="grid4">
<div class="why"><h3>Flexible timing</h3><p>Scale up or down as your needs change, with smooth transitions to permanent hires.</p></div>
<div class="why"><h3>Qualified professionals</h3><p>Practitioners with public and corporate accounting experience.</p></div>
<div class="why"><h3>Consistent teams</h3><p>The same people return over time, so they already know your business.</p></div>
<div class="why"><h3>Reasonable rates</h3><p>A low-overhead model means deep experience at a fair price.</p></div>
</div>
</div></section>
<section><div class="wrap"><div class="band"><div><h2>Build your highly skilled team.</h2><p>Whether you need an executive-level leader or an experienced staff member, we can help.</p></div><a class="btn" href="#contact">Get in touch</a></div></div></section>
<!-- SERVICES -->
<section id="services" class="alt" style="padding-block:clamp(48px,6vw,72px)"><div class="wrap">
<div class="head" style="margin-bottom:24px"><p class="eyebrow">Services</p><h2>How we can help</h2><p class="muted">Our professionals integrate with your team and serve as technical resources for public and private companies.</p></div>
<div>
<div class="svcrow"><div class="left"><span class="ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20V10l8-6 8 6v10"/><path d="M9 20v-6h6v6"/></svg></span><h3>Management Resources</h3></div>
<p class="muted">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.</p></div>
<div class="svcrow"><div class="left"><span class="ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="8" r="3.5"/><path d="M2.5 20c.8-3.6 3.4-5.5 6.5-5.5s5.7 1.9 6.5 5.5"/><path d="M16 5.2a3.4 3.4 0 0 1 0 6.1"/><path d="M18 14.8c1.8.8 3 2.6 3.5 5.2"/></svg></span><h3>Supplemental Team Members</h3></div>
<div class="stack" style="gap:12px"><p class="muted">Temporary or long-term professionals for when your team needs more capacity:</p>
<ul class="list"><li>Open positions and leave coverage</li><li>Monthly, quarterly and annual deadlines</li><li>Acquisitions and new system implementations</li><li>Outsourced accounting, start-ups and wind-downs</li></ul></div></div>
<div class="svcrow"><div class="left"><span class="ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="M20 20l-4.2-4.2"/></svg></span><h3>Recruiting &amp; Placement</h3></div>
<p class="muted">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.</p></div>
</div>
</div></section>
<!-- PURPOSE & BELIEFS -->
<section id="purpose" class="purpose"><div class="wrap">
<div class="purpose-statement">
<p class="eyebrow">Our purpose</p>
<blockquote>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.</blockquote>
</div>
<div class="head" style="margin-top:72px"><p class="eyebrow">Core beliefs</p><h2>What guides us.</h2></div>
<div class="beliefs">
<div class="belief"><span class="bn">Faith</span><p>We believe in honoring God in all that we do.</p></div>
<div class="belief"><span class="bn">Integrity</span><p>We believe in doing the right thing, the right way, and at the right time.</p></div>
<div class="belief"><span class="bn">Relationships</span><p>We believe that continually building new and existing relationships is essential to who we are and what we do.</p></div>
<div class="belief"><span class="bn">Service</span><p>We believe in helping others, making people and our clients better.</p></div>
<div class="belief"><span class="bn">Initiative</span><p>We believe in taking the initiative and persevering through to the end — proactively looking for opportunities to help our clients and add value.</p></div>
<div class="belief"><span class="bn">Excellence</span><p>We believe in striving for excellence in all that we do.</p></div>
</div>
</div></section>
<!-- ABOUT -->
<section id="about"><div class="wrap">
<div class="head"><p class="eyebrow">About us</p><h2>Practitioners who have sat in your seat.</h2><p class="muted">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.</p></div>
<div class="team">
<div class="member"><div class="ph"><img src="data:image/jpeg;base64,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 truncated
<div class="member"><div class="ph"><img src="data:image/jpeg;base64,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 truncated
<div class="member"><div class="ph"><img src="data:image/jpeg;base64,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 truncated
<div class="member"><div class="ph"><img src="data:image/jpeg;base64,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 truncated
</div>
</div></section>
<!-- CONTACT -->
<section id="contact" class="alt"><div class="wrap">
<div class="head" style="max-width:760px"><p class="eyebrow">Contact</p><h2>Let's build your team.</h2><p class="lede">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.</p></div>
<div class="contact-cards">
<div class="contact-card">
<span class="ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="8" r="3.5"/><path d="M2.5 20c.8-3.6 3.4-5.5 6.5-5.5s5.7 1.9 6.5 5.5"/><path d="M16 5.2a3.4 3.4 0 0 1 0 6.1"/><path d="M18 14.8c1.8.8 3 2.6 3.5 5.2"/></svg></span>
<h3>Need talent or project help?</h3>
<p class="addr">contact@addsourcegroup.com</p>
<a class="btn primary" href="mailto:contact@addsourcegroup.com">Email Us</a>
</div>
<div class="contact-card">
<span class="ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="M20 20l-4.2-4.2"/></svg></span>
<h3>Looking for an opportunity?</h3>
<p class="addr">candidates@addsourcegroup.com</p>
<a class="btn primary" href="mailto:candidates@addsourcegroup.com">Contact Our Recruiting Team</a>
</div>
</div>
</div></section>
<div class="stripe"></div>
<footer class="site"><div class="wrap">
<div class="top">
<span class="logo"><span class="mark">ASG<span class="plus">+</span></span><span class="words">ADD<br>SOURCE<br>GROUP</span></span>
<nav aria-label="Footer"><a href="#services">Services</a><a href="#about">About</a><a href="#contact">Contact</a><a href="https://www.linkedin.com/company/add-source-group-llc/">LinkedIn</a></nav>
</div>
<div class="base"><span>© 2026 Add Source Group LLC. All rights reserved.</span><span>Birmingham, Alabama</span></div>
</div></footer>
<script>
(function(){
var reduce = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
var header = document.querySelector('header.site');
function onScroll(){ if(header) header.classList.toggle('scrolled', window.scrollY > 20); }
window.addEventListener('scroll', onScroll, {passive:true}); onScroll();
if (reduce || !('IntersectionObserver' in window)) return;
document.documentElement.classList.add('anim');
var groups = ['.stat','.svc','.why','.belief','.purpose-statement','.svcrow','.member','.contact-card','.head','.band'];
var els = [];
groups.forEach(function(sel){
document.querySelectorAll(sel).forEach(function(el){
var sibs = Array.prototype.filter.call(el.parentNode.children, function(c){ return c.matches(sel); });
el.style.setProperty('--d', (Math.min(sibs.indexOf(el), 5) * 0.09) + 's');
el.classList.add('reveal'); els.push(el);
});
});
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){ e.target.classList.toggle('in', e.isIntersecting); });
}, {threshold: 0.12, rootMargin: '0px 0px -40px 0px'});
els.forEach(function(el){
var r = el.getBoundingClientRect();
if (r.top < innerHeight && r.bottom > 0) el.classList.add('in');
io.observe(el);
});
})();
</script>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://addsourcegroup.com/sitemap.xml
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://addsourcegroup.com/</loc>
</url>
</urlset>