Initial commit for powershell scripts.
This commit is contained in:
1 parent
f4e5d9dcbd
commit
4fd95b76d6
18 files changed
+1803
No files matched your search
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
Executable
+28
@@ -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
|
||||
Executable
+209
@@ -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
|
||||
Executable
+112
@@ -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
|
||||
@@ -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."
|
||||
}
|
||||
Executable
+23
@@ -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."
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Executable
+9
@@ -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"
|
||||
@@ -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"
|
||||
Executable
+56
@@ -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: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+90
@@ -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.
Executable
+378
@@ -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 & 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 & 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,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAUEBAQEAwUEBAQGBQUGCA0ICAcHCBALDAkNExAUExIQEhIUFx0ZFBYcFhISGiMaHB4fISEhFBkkJyQgJh0gISD/2wBDAQUGBggHCA8ICA8gFRIVICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICD/wAARCAMgAoADASIAAhEBAxEB/8QAHQAAAQUBAQEBAAAAAAAAAAAABgIDBAUHAQgACf/EAEoQAAEDAwMCBAQEBAMFBwMCBwECAwQABREGEiExQRMiUWEHFHGBMkKRoRUjscFSYtEWJDPh8AhDcoKSovEXNFMlY7LCc9ImRKP/xAAaAQACAwEBAAAAAAAAAAAAAAADBAABAgUG/8QAMhEAAgIBAwIEBQMFAQEBAQAAAAECAxESITEEQRMiUWFxgZGh8LHB0QUUIzLh8UJSM//aAAwDAQACEQMRAD8A3BrUaTgLSRUtF+jqH48fUUIhPvSwmq1MzoQWqujTg4cSfvUdyTuHCh+tDRBpIKh0JqOWS1HBdOunHqKguu56VGDiwPxGkKUr1oTQRNilOc04lXNRCeetOtq4rGDeSYlVOJPFRQunAqtIofz70nNIKq5uqyDufalJBNJbSVGprbPGcVMFDaEU8OlObMdqaWoJqFiVrKRVxbZKVNpQRz3ofddGPpTKLiI5BCsYqtW5bh3DZToSetVFyfSpBBNUq9SMbcrcCcdST0qhm6lYeXtZd3jpkVtvYHlcFgo5UQK+P4D9KhQ5HijJ71NcUEpPvQ8BAfuv/wBuusT1T/8Acq+9bRdXB4C+axjVGDIUoc1syUejpHgXGRz+fNbJAvSUtAbu1YLaXvAuL5BxzRkxdilAG6nKnhCk0Fep9ShAIDmOKyS63l2W4UhZ2n3605qO6Lfk7Ao4/rQ4Vk9cmtt5Axj3Y6XCSSTzXwXz1zmmNwPSuhX2qghMSvOOafQrnINQkKweaeQs9DVlFgyo7uuK2P4NMBV4cex0HHtWLNqxW/fBWP5HXSOtZlwYn2R6KtlwUwgNrGUjp7UQN3SPtBUvb9aForRUBUiQyrwjihOIys4CdNyjK5DyacE5g9HEn70CeA6noTSdshJ4Uaxg1lmgCSg9x+tLDyTWe+PKR+c0tNwmo6OK/WoTLD4vJHekKfSOpoJF4mAcrJr7+LP58yiahWp+gaCQk96UHknvQYm7uA8mpCbyR1NawitT9AncfSOM1FW6FcZqhRdfGdKQasGHCvBzVMtPJYp/DX1cT+EV2qNn1fVyvqhD6vjX1cNQhyuV01yoQ7X1crtQglRwKhPyNnU1JeVhNDl0leEhZJ4AqFN4HnbihKvxYpv+IoP56xO/a/dZursdkHahW3JNdt2t3H1BK18n3o8IZEZdQ12NocmJKeFVSz5oSCc1QwbyJKBhfJ96VKWVoPNMqldwTvbWxAmXsNOHKuKab1A2rgLzQ3f0LDalJ6jmgpN2caeKFKxg1tRjHYX1zZsKb6n/ABfvUhF7T6ispj3hRA85qyZuucebmipIG5SNLRec+lPC656EVnzVyyM7qlouOceataUZ1yDkXT3pQuZ7Gg1FwPZWadE8/wCKppRNbC/+JK9a+NyP+KhMTuOtL+d96mlE8RhObif8VJ+fV/ioa+dPc10S/er0Fa2Efzx/xV986T3oeEv3pQlZ7/pU0k1svvmye9J+aJ71TCTnvXRI96vSTUXHzR9a++bI71UeP3zXC/2zipgmpnyZGe9OpfHrVGiSCetSUPDAOa4B6TJbeKPWvg4mq3xvcV94/PWqJktQpNIWodqr0yPelF7PeqZpEhR96UlVRg5mnEqrBolpVTqVVFSqnUmtEH93FfA803k0oHmoQsoqQSM1ZpwBVRHeSgcmnHZ6EDg81MomCZIeQhJ9apJM4JJO6oc66BCSVLoTn3kkK2nNClMJGJdzbyhoElYGKE7lqvaFJaO4+1Uk2a68olSzVDIyo9e9ZUskcWO3K/S3938xQHoDTliui1uBC1Z2mqCWcJNRrdLEeenJxk80eO4GSSN3tMvKAd1XLrxU3uB6CgK0TgGgSqimLKDqPxZBo8IZBylgrLpJBZcGayfURJWo+9aXdEne4kfhJrN9Qt7QcnvUsWCq5Z5AKOrbcXgDVsHFbMZNUyTturo9R+tWSFeXtRK+CpFRdCTJBPHFQO/NTrmT4yT7VXE0QCKyc810H05pvPHSvsnOasokIVinkq4HpUUEDvzTqTyOc1CE9lWVpxXpv4OR/Dswcx+I15hinc8gZ7ivWXw0Z+W03GI7pzWZGHvJGvxANoqWvBGKq4j6do5px6YhKgN4zUD6iX4aT2rhZSe1RUzU/wCIU4JQPepgvUhZjpPakGIj0pQkJPelB9J71NJMoYMJJ7UkwB6VNS6k0sOJPeq0l5KpUCmzCIq5KkmknafSs6EXkpI8ZSJJyO9X8YEAVB8vjmrJgDArElgtMnp/CK7XE/hrtZNHK+r6vqhDhrldrlQhw1yumk1CHa7Sc12oQZkfhoSvwzFd9waLZA8lCt7GYrn0NQzLg8u6l8t8ljP5zVMxOWw8MKq61RxfpY/z5oSeVtXxxTC2E0so02w31QUkKXWhRpiZLAUCDxWB2ucW3E5JHatP09c96EpJ4NO1yyhCyOiRcXZne0qslv7K4snxE8c81skgBbZrPNUwN7K1AdK1ZHbKKi8MDY884A3VbR5/A8370GJWWHlNnsasI8ojACqXjPAy60w3Ynn/ABVYtTemFUFsSsEc/vVkzK9/emIyyLyhgLW5eQOetSES/ehpuSePN/zqYiRRUwLRfplHsaWJXvVIl/jrSi/71ZWC6ErnrShLA/NVJ4/bNd8frg1ZReCWeOadTKGOtUAkc9aWmQcDk1CsBAmV79KWJWe9UKZHYGnBIOODmrKLz5n3r75n1VVMJJ454rokZ71ZCYEEdCaeSFAV0AZp1IrzJ6rAjcqklwjrT+3jpSS3z0qZJgbDpzTyXM96QGuelOob57VREOpVzT6DxTSUU8lGKmC8oeQqn0mmEjFPJrWCsjoPNdUcDmvk4zSXinbVNFpkZ+X4YODVDLvmzcN3I96cur+xCueKBJkpRcPmoEmFiWku6uSFnK+O1V7r+R1NV4eB9q4p3jrQcBk0lhHXnAck1WPuYBNPOuE5qtku4BwfvRYow2V8p45ODVK89sXuScHPFTZa+DzVK+rco9aaihaTD/Tl8DzAZWrzprQrdNAQnKqwuyurakhQJAzR/DuhShPn601AVmHEl1DqVnrWc6kUFKUARxRALnlKxuzQpdXC7uVQ7WgtSAZ7CbqrA4IqahflqDMO26D6U+2rCa1XwSZBuJy4k8Cq4HnrU24nlNVwOD3oqAC84OK5nmk5z9653zVlDyT0AzTqVcjvUdJ5zj7U6k5qELi1IL1wZRj8SgK9gaOY8CxRUYwdgryfpNj5i/RkYz5gea9g2Jvw4LKcdEgVl8mFvMIEKKUcGhm8TpDcsBCz05olJ/l0JXIhc5ee3FAmxpRTYyi7zU/mzT6NQyk9U/vUHZSCjND1M26l6Fy3qd0fiSalt6pRjzAihgt5pPh+1X4jK8FBq1qaMrqrFTW79FV/3gH3rOyivtqgfKSK0rGZdXozTU3dg9HB+tOJuTR6OCsvC30jyrV+tKEqWno6qtK0z4cvU0tmWFySArP0q/ir3AVmOl5Uh6a6HV7sVpMH8KfpVOWouKa5LhP4a7XE/hrtUEPq5X1fVCHK4a6aTUIcNJrppNQh2u0muioQbf8AwUL3oZjrHqKKXR5KG7unLCvpUMy4PLmsEbNRSRjgmguV1NHuuUbNRO8dRQNJTwod6OuBSPBGjulDnWjfT88pcTzQDkhz9qvrRKKHU+1GrlhgboZRt8V4Pxkq68VRXyMHGFcU5YZYcYCc54qdcG97SsU9yhFGBX1gxp5IzgmorL2MUUaxg43rA6c0EMuc4zikJLEsD8HmIQMv89elWLD+PpQ+y70/tU9l3vWoyKkgiakE96ntv8YzQ8090qe09nvTEZC8ol4l73pwO9s1Vod706Hc0TIJosPF96SXe+ai78jtX285q8lYJYepxL1VxXSg50OamSsFoHvenQ971WJdOKdS7781rJWCxDx7UoOe/NQUue5pYX3q8mcBiAelPJAFQVym08ZphdzQn8wFcGNbZ6RzSLnyjmklaB1IofcvCB+aob19Qn84oyoYF3oKfHbH5hXPmmx35oIe1A2n/vAPvUFzU7QPLg/Wt/24N9SjRxObA4xShcEeoFZirVbY/wC9FJOrmgP+LWlQjD6k1EXFA/MK+/iaAOV1lC9Xt/8A5OfTNR3NZpHRVb8GKMPqGbG1ckqcwFU67NBSfNisgterS9JwM0WIu6nkjB60C1RiMUTlMn3eQFNLwaB5S8rPJq/myCtk80OujKjXMm9zpRWwyhXalHJ70kp618TgGhm0NucCqiYsYIyas3l4SeaopjvXFEiZkVMtzrzVelKnF4HNPPhbrpSO9TosTYATTcF2FZPuKiMeEAQKsUPlHOftTRASKhypAaQSTz2o6F2XbE0qfUjPOKRLOWzVTbHVLmZPcVaSSNhJoFnIxVwBly4uCT14pxCjt9qbu3Exs9DXG1ccn7USszMiXAnCc81X5GfT61YTvwDjjNV2R9aKAZ3rX1cz69K+7VChY9adR0pgD70832qyGifDaL8xqJkkZANesLYnawge1ebfhHF33Eu46V6UhDCE+1ZMwXmbJzzm1vOcUFyJO+S4f8xomuL4bjKVngCgEvErJJ6mlrORyvktg8K+8UVVh44618Xz60IMWZcHrxX28VVl/wB66JFQrJZ7ga+4qvEj3pYfqyZJuBSVAVH+Y9674/vmoQIdLD/f3vtWlwfwprL9JuLine 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>
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Executable
+4
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://addsourcegroup.com/sitemap.xml
|
||||
Executable
+6
@@ -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>
|
||||
Reference in new issue
Block a user