First full commit

This commit is contained in:
jcolebrand 2023-05-30 22:51:22 -07:00
parent ac2a7b7ef4
commit c34cead304
1996 changed files with 166521 additions and 0 deletions

View File

@ -0,0 +1,183 @@
Function Clear-BadPesterUserAccounts {
<#
.SYNOPSIS
Cleans up all incorrectly created user accounts on the system from Pester.
.EXAMPLE
Clear-BadPesterUserAccounts
.OUTPUTS
Returns the list of folder names (from C:\Users) that are "invalid".
If the return list is empty then there were none to remove.
This is useful for post-facto additional resource cleanup. In the case of a non-WhatIf, this will be an ephemeral response, so catch it while you can.
.PARAMETER AllPossibleDomainsToSearch
What domains do we want to search? Defaults to [fh, corp]
.PARAMETER WhatIf
Used to test before removing for validation
#>
param(
$AllPossibleDomainsToSearch = @("fh","corp"),
[switch]$WhatIf
)
$logLead = (Get-LogLeadName)
# This means we are on Windows
# At the very least, we can't get the CIM Instances to remove users if they exist
if ($null -eq (Get-Command Get-CimInstance -ErrorAction SilentlyContinue)) {
Write-Host "$logLead : Can't CimInstance on this host, nothing to do"
return
}
Write-Host "$logLead : Cleaning bad pester user accounts [Dry run? $WhatIf]"
# Get local user accounts
# Get IIS user accounts or IIS app pool names (it defaults the identity to the name if no identity specified)
# Get all domain GMSA accounts
# Get all domain user accounts
#
# Get all c:\users folders
# Diff the names
# Remove the users
# Annoying bug...
if ($null -ne (Get-Module Carbon)) {
# Carbon stomps all over our Get-ADDomainController amongst other things
Remove-Module Carbon -Force
}
$userAccountsToExclude = @()
$foldersToKeep = @()
$foldersToRemove = @()
# The system can't determine about some of these, they are special folders
# This is where any such folders go
# Try to make them dynamic before adding them here.
# Public is a very special child unrelated to anything else.
$userAccountsToExclude += "Public"
if ($null -ne (Get-Module -ListAvailable -Name WebAdministration)) {
Get-Module WebAdministration | Remove-Module -Force
Import-Module WebAdministration
$appPools = @(Get-ChildItem IIS:\AppPools)
foreach($appPool in $appPools) {
if (![string]::IsNullOrWhiteSpace($appPool.processModel.userName)) {
$userAccountsToExclude += $appPool.processModel.userName
} else {
$userAccountsToExclude += $appPool.Name
}
}
}
$localUsers = @(Get-LocalUser)
foreach($localUser in $localUsers) {
$userAccountsToExclude += $localUser.Name
}
if ($null -eq (Get-Module -ListAvailable -Name ActiveDirectory)) {
# This is truly an impossible condition to hit. So if we hit it, hit it hard.
throw 'how in the name of sanity is this server not setup to query the domain???'
}
Import-Module ActiveDirectory
foreach($domainName in $AllPossibleDomainsToSearch) {
$dc = Get-ADDomainController -DomainName $domainName -Discover -NextClosestSite
$dcHostname = $dc.Hostname[0]
if ([string]::IsNullOrWhiteSpace($dcHostname)) {
Write-Warning "$logLead : Could not find a hostname for $domainName - Can you access that domain from here?"
continue
}
$domainServiceAccounts = @(Get-ADServiceAccount -Filter "*" -Server $dcHostname)
foreach($domainServiceAccount in $domainServiceAccounts) {
$userAccountsToExclude += $domainServiceAccount.SamAccountName
}
$domainUserAccounts = @(Get-ADUser -Filter "*" -Server $dcHostname)
foreach($domainUserAccount in $domainUserAccounts) {
$userAccountsToExclude += $domainUserAccount.SamAccountName
}
}
$userFolders = @(Get-ChildItem -Directory -Path C:\Users)
foreach($folder in $userFolders) {
if ($userAccountsToExclude -contains $folder.Name) {
$foldersToKeep += $folder.Name
} else {
$foldersToRemove += $folder.Name
}
}
$allCimAccounts = @(Get-CimInstance -ClassName Win32_UserProfile)
$teamCity = (Test-IsTeamCityProcess)
if ($teamCity) {
Write-Host "##teamcity[blockOpened name='Display accounts for followup']"
} else {
Write-Host "================================="
}
if (!(Test-IsCollectionNullOrEmpty $foldersToKeep)) {
$tcBlurb = "Keep these accounts"
if ($teamCity) {
Write-Host "##teamcity[blockOpened name='$tcBlurb']"
} else {
Write-Host "$logLead : Found these folders to KEEP"
}
foreach($folder in $foldersToKeep) {
if ($teamCity) {
Write-Host "$logLead : KEEP C:\Users\$folder"
} else {
Write-Verbose "$logLead : KEEP C:\Users\$folder"
}
}
if ($teamCity) {
Write-Host "##teamcity[blockClosed name='$tcBlurb']"
} else {
Write-Host "================================="
}
}
if (!(Test-IsCollectionNullOrEmpty $foldersToRemove)) {
$tcBlurb = "Remove these accounts"
if ($teamCity) {
Write-Host "##teamcity[blockOpened name='$tcBlurb']"
} else {
Write-Host "$logLead : Found these folders to REMOVE"
}
foreach($folder in $foldersToRemove) {
Write-Host "$logLead : REMOVE C:\Users\$folder"
$matchingCimInstances = @($allCimAccounts.Where({$_.LocalPath -eq "C:\Users\$folder"}))
foreach($instance in $matchingCimInstances) {
if ($instance.Sid.StartsWith("S-1-5-82")) {
Write-Host "$logLead : $folder is an IIS-AppPool SID"
}
}
$matchingCimInstances | Remove-CimInstance -WhatIf:$WhatIf
}
if ($teamCity) {
Write-Host "##teamcity[blockClosed name='$tcBlurb']"
} else {
Write-Host ""
}
}
if ($teamCity) {
Write-Host "##teamcity[blockClosed name='Display accounts for followup']"
} else {
Write-Host "================================="
}
return $foldersToRemove
}

View File

@ -0,0 +1,42 @@
Function Get-Aliases {
<#
.SYNOPSIS
Collects all alias names from files in a folder. Assumes Test-FunctionNames properly ran and validated target function names.
.EXAMPLE
Get-Aliases .\Alkami.PowerShell.IIS\Public
.PARAMETER FolderPath
The name of the folder to examine all files under.
#>
[CmdletBinding()]
Param (
[String]$FolderPath
)
process {
$aliases = @()
$verbs = Get-Verb | Select-Object -ExpandProperty Verb
$files = (Get-ChildItem -Path $FolderPath *.ps1)
foreach($file in $files) {
if ($file.BaseName.ToLower().EndsWith(".tests") -or $file.BaseName.ToLower().EndsWith(".test")) {
Write-Verbose "skipping function names of test $file"
} else {
$lines = (Get-Content $file.FullName)
foreach($line in $lines) {
if (($line.Trim().ToLower().StartsWith("set-alias")) -or ($line.Trim().ToLower().StartsWith("new-alias"))) {
$candidateLine = $line -replace ';','' -replace '-name','' -replace '-value','' -replace '-force','' -replace '-scope:global','' -replace '-Scope Global',''
$splits = ($candidateLine.Trim() -split '\s+')
if ($splits.length -ne 3) {
Write-Warning "Could not parse line [$line] for Set-Alias, found [$splits]"
} else {
$aliases += $splits[1]
}
}
}
}
}
return $aliases
}
}

View File

@ -0,0 +1,32 @@
Function Get-BuildConfigs {
<#
.SYNOPSIS
Get the build configs from a single csproj
#>
[CmdletBinding()]
Param(
$csprojPath
)
process {
if (!(Test-Path $csprojPath)) {
Write-Warning "can't test on an empty path [$csprojPath]"
return
}
$returnValues = @()
$xml = [Xml](Get-Content $csprojPath)
$propertyGroups = @($xml.Project.PropertyGroup)
foreach($propertyGroup in $propertyGroups) {
if ($null -ne $propertyGroup.Condition) {
$value = $propertyGroup.Condition.Replace("'",'').Split('==')[-1].Split('|')[0].Trim()
if (!([string]::IsNullOrWhiteSpace($value))) {
$returnValues += $value
}
}
}
return $returnValues
}
}

View File

@ -0,0 +1,58 @@
Function Get-ContentFromFilesInPath {
<#
.SYNOPSIS
Collects all content from valid files for inclusion into the PSM1
.DESCRIPTION
Collects all content from valid files for inclusion into the PSM1
* Skips test files
* Adds a header line saying where the file came from during compile
The reason for these files to match their names is about process, not a functional concern.
.EXAMPLE
Get-ContentFromFilesInPath .\Alkami.PowerShell.IIS\
.PARAMETER FolderPath
The name of the folder to examine all files under.
#>
[CmdletBinding()]
Param (
[String]$FolderPath
)
process {
$functionLines = @()
$prefixPath = (Split-Path $FolderPath -leaf)
$files = (Get-ChildItem (Join-Path $FolderPath "*.ps1"))
foreach($file in $files) {
if ($file.BaseName.ToLower().EndsWith(".tests") -or $file.BaseName.ToLower().EndsWith(".test")) {
Write-Verbose "skipping function names of test $file"
}
elseif ($file.BaseName.ToLower() -eq "variabledeclarations") {
$content = Get-Content $file.FullName
$functionLines += @("## Function from $file")
if ($null -ne $content -and $content[0] -match "SuppressMessageAttribute" -and $content[1] -match "param\(\)") {
Write-Host "Skipping PSScriptAnalyzer and Param Declaration from $($file.FullName)" -ForegroundColor Green
$functionLines += ($content | Select-Object -Skip 2)
} else {
$functionLines += $content
}
}
else {
$functionLines += @("## Function from $file")
$functionLines += (Get-Content $file.FullName)
}
$functionLines += ""
}
return $functionLines
}
}

View File

@ -0,0 +1,57 @@
Function Get-ContentFromFormatFilesInPath {
<#
.SYNOPSIS
Collects all content from valid files for inclusion into the PSM1
.DESCRIPTION
Collects all content from valid files for inclusion into the PSM1
* Skips test files
* Adds a header line saying where the file came from during compile
The reason for these files to match their names is about process, not a functional concern.
.EXAMPLE
Get-ContentFromFilesInPath .\Alkami.PowerShell.IIS\
.PARAMETER FolderPath
The name of the folder to examine all files under.
#>
[CmdletBinding()]
Param (
[String]$FolderPath
)
process {
$functionLines = @()
$prefixPath = (Split-Path $FolderPath -leaf)
$files = (Get-ChildItem (Join-Path $FolderPath "*.ps1xml"))
[Xml]$defaultXml = [Xml]@"
<?xml version="1.0" encoding="utf-8"?>
<Configuration>
</Configuration>
"@
$defaultXml.DocumentElement.AppendChild($defaultXml.CreateElement("SelectionSets")) | Out-Null
$defaultXml.DocumentElement.AppendChild($defaultXml.CreateElement("ViewDefinitions")) | Out-Null
$defaultXml.DocumentElement.AppendChild($defaultXml.CreateElement("Controls")) | Out-Null
$defaultXml.DocumentElement.AppendChild($defaultXml.CreateElement("DefaultSettings")) | Out-Null
foreach($file in $files) {
[Xml]$xml = [Xml](Get-Content $file.FullName)
foreach($node in $xml.DocumentElement.SelectionSets.ChildNodes) {
$defaultXml.DocumentElement.SelectSingleNode("SelectionSets").AppendChild($defaultXml.ImportNode($node,$true)) | Out-Null
}
foreach($node in $xml.DocumentElement.Controls.ChildNodes) {
$defaultXml.DocumentElement.SelectSingleNode("Controls").AppendChild($defaultXml.ImportNode($node,$true)) | Out-Null
}
foreach($node in $xml.DocumentElement.ViewDefinitions.ChildNodes) {
$defaultXml.DocumentElement.SelectSingleNode("ViewDefinitions").AppendChild($defaultXml.ImportNode($node,$true)) | Out-Null
}
foreach($node in $xml.DocumentElement.DefaultSettings.ChildNodes) {
$defaultXml.DocumentElement.SelectSingleNode("DefaultSettings").AppendChild($defaultXml.ImportNode($node,$true)) | Out-Null
}
}
return $defaultXml
}
}

View File

@ -0,0 +1,31 @@
Function Get-FunctionNames {
<#
.SYNOPSIS
Collects all function names from filenames. Assumes Test-FunctionNames properly ran.
.EXAMPLE
Get-FunctionNames .\Alkami.PowerShell.IIS\Public
.PARAMETER FolderPath
The name of the folder to examine all files under.
#>
[CmdletBinding()]
Param (
[String]$FolderPath
)
process {
$functionNames = @()
$verbs = Get-Verb | Select-Object -ExpandProperty Verb
$files = (Get-ChildItem -Path $FolderPath *.ps1)
foreach($file in $files) {
if ($file.BaseName.ToLower().EndsWith(".tests") -or $file.BaseName.ToLower().EndsWith(".test")) {
Write-Verbose "skipping function names of test $file"
} else {
$functionNames += $file.BaseName
}
}
return $functionNames
}
}

View File

@ -0,0 +1,41 @@
Function Get-MSBuildPath {
<#
.SYNOPSIS
Get the local machine MSBuild path in a way that it can be easily consumed
.EXAMPLE
Get-MSBuildPath
C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\MSBuild.exe
#>
[CmdletBinding()]
Param()
process {
if ($script:isSet_GetMSBuildPath) {
return $script:value_GetMSBuildPath
}
$msBuildPath = ""
if (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe") {
$msBuildPath = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe"
} elseif (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\MSBuild.exe") {
$msBuildPath = "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\MSBuild.exe"
} elseif (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\MSBuild.exe") {
$msBuildPath = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\MSBuild.exe"
} elseif (Test-Path "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe") {
$msBuildPath = "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe"
} elseif (Test-Path "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe") {
$msBuildPath = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe"
} else {
$buildVersion = (Get-ChildItem HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions -Name | Measure -Maximum).Maximum
$MSBuildVersionRegEntry = (Get-ItemProperty -LiteralPath "HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\$($buildVersion).0\" -Name "msbuild.exe")
if ($null -ne $MSBuildVersionRegEntry) {
$msBuildPath = $MSBuildVersionRegEntry."msbuild.exe"
}
}
if (!([string]::IsNullOrEmpty($msBuildPath))) {
$script:isSet_GetMSBuildPath = $true
$script:value_GetMSBuildPath = $msBuildPath
}
return $msBuildPath
}
}

View File

@ -0,0 +1,29 @@
Function Join-PS1XMLFromFiles {
<#
.SYNOPSIS
Uses Get-ContentFromFormatFilesInPath to join the file contents appropriately
.DESCRIPTION
Collects all content from valid files for inclusion into the PS1XML file
.EXAMPLE
Join-PS1XMLFromFiles .\Alkami.PowerShell.IIS\Public\ .\Alkami.PowerShell.IIS\Alkami.PowerShell.IIS.ps1xml
.PARAMETER PublicPath
The name of the folder to examine all files under.
.PARAMETER Ps1xmlFilePath
The file to overwrite with the contents of the build process.
#>
[CmdletBinding()]
Param (
[String]$PublicPath,
$Ps1xmlFilePath
)
process {
# Get all of the xml nodes from the format files
$nodes = @(Get-ContentFromFormatFilesInPath $PublicPath)
Set-Content -Path $Ps1xmlFilePath -Value $nodes.OuterXml -Force
}
}

View File

@ -0,0 +1,41 @@
Function Join-PSM1FromFiles {
<#
.SYNOPSIS
Uses Get-ContentFromFilesInPath to join the file contents appropriately
.DESCRIPTION
Collects all content from valid files for inclusion into the PSM1
* Skips test files
* Adds a header line saying where the file came from during compile
The reason for these files to match their names is about process, not a functional concern.
.EXAMPLE
Join-PSM1FromFiles .\Alkami.PowerShell.IIS\Public\ .\Alkami.PowerShell.IIS\Private\ .\Alkami.PowerShell.IIS\Alkami.PowerShell.IIS.psm1
.PARAMETER PublicPath
The name of the folder to examine all files under.
.PARAMETER PrivatePath
The name of the folder to examine all files under. Tested for existence, may not be valid.
.PARAMETER Psm1FilePath
The file to overwrite with the contents of the build process.
#>
[CmdletBinding()]
Param (
[String]$PublicPath,
[String]$PrivatePath,
$Psm1FilePath
)
process {
## Put all of the content from the public files in a long array with a file-header
$functionLines = @(Get-ContentFromFilesInPath $PublicPath)
if (Test-Path $PrivatePath) {
## Put all of the content from the private files in the array with a file-header, after the public functions
$functionLines += @(Get-ContentFromFilesInPath $PrivatePath)
}
Set-Content -Path $Psm1FilePath -Value $functionLines -Force
}
}

View File

@ -0,0 +1,24 @@
## This file tries to load functions if they haven't been loaded into scope yet.
## We know if things are in the scope if the magic function Get-ContentFromFilesInPath has been loaded into the current session scope
## The way that this is loaded is from this file or manually
## This file was chosen because it doesn't live in any module for building the powershell modules
## The use of this file is as such:
## . $PSScriptRoot\.build\Load-Includes.ps1
## The use-case of this file and the unusual style is that this is just to include things for execution in the build system
## This is not a typical style of how to load things. This is a micro-op.
$script:buildFilesLoaded = $script:buildFilesLoaded -or ($null -ne ${function:Get-ContentFromFilesInPath})
if (!($script:buildFilesLoaded)) {
$buildScriptsFolder = $PSScriptRoot
if (Test-Path $buildScriptsFolder) {
$buildScripts = (Get-ChildItem *.ps1 -Path $buildScriptsFolder -Exclude "Load-Includes.ps1" -Recurse)
foreach($script in $buildScripts) {
Write-Verbose "[Clean-Project] - Including the build files: $script"
. $script.FullName
}
$script:buildFilesLoaded = $true
}
}

View File

@ -0,0 +1,86 @@
Function Test-FunctionNames {
<#
.SYNOPSIS
Test that the name of the function/filter/workflow in the file match the name of the file and for verb match
.DESCRIPTION
Test that the name of the function/filter/workflow in the file match the name of the file and for verb match
* This will look in public/private folders
* This will examine for verb match to Get-Verb as part of the testing
* This ignores any names that end with .tests.ps1 or .test.ps1
* This will check .tests?.ps1 files that they start with a reference to Load-PesterModules
* If they don't, it will emit a warning per file
The reason for these files to match their names is about process, not a functional concern.
.EXAMPLE
Test-FunctionNames .\Alkami.PowerShell.IIS\
.PARAMETER FolderPath
The name of the folder to examine all files under
#>
[CmdletBinding()]
Param (
[String]$FolderPath
)
process {
$verbs = Get-Verb | Select-Object -ExpandProperty Verb
$badMatch = @()
$files = (Get-ChildItem (Join-Path $FolderPath "*.ps1"))
foreach($file in $files) {
if ($file.FullName -match '(scratch|bak)') {
Write-Verbose "Skipping $($file.FullName) for (scratch|bak)"
continue
}
$content = (Get-Content $file)
if ($file -match '\.Tests\.ps1' -or $file -match '\.Test\.ps1') {
$firstLine = $content[0]
if (([string]::IsNullOrWhitespace($firstLine) -or $firstLine -notmatch 'PesterModules') -and $firstline -notmatch "param") {
Write-Warning "[$file] does not start with load-pestermodules or parameter declarations. You're gonna have a bad time."
} elseif ($firstLine -match "param" -and ($null -eq ($content | Where-Object {$_ -match "PesterModules"}))) {
Write-Warning "[$file] has a parameter declaration but does not include load-pestermodules. You're gonna have a bad time."
}
Write-Verbose "skipping testing of test $file"
} else {
$functionCandidate = $file.BaseName
$keywordStartsLine = 0
$passing = $false
foreach($line in $content) {
## Make sure the line doesn't start with whitespace and that people didn't put a bunch of spaces between 'keyword' and 'function-name'
$line = ($line -replace '\s+',' ' -replace '^function','' -replace '^filter','' -replace '^workflow','').Trim()
if ($line.StartsWith($functionCandidate, [StringComparison]::InvariantCultureIgnoreCase)) {
$passing = $true
## we found the line we want
break
}
$keywordStartsLine += 1
}
# Make sure that the function name uses a good verb-naming pattern
$verb = $functionCandidate.Split("-")[0]
if($verbs -notcontains $verb) {
$passing = $false
Write-Warning "Function $functionCandidate (current verb: [$verb]) does not use a valid PowerShell verb. See: Get-Verb"
}
if ($keywordStartsLine -gt 1) {
Write-Verbose "The file [$file] doesn't start with the keyword and function-name"
}
if (!$passing) {
$badMatch += $functionCandidate
}
}
}
if ($badMatch.Length -gt 0) {
$badMatch
throw "The previous files failed their test constraints"
}
}
}

View File

@ -0,0 +1,23 @@
Function Test-IsTeamCityProcess {
<#
.SYNOPSIS
Determines if the Current Process is a TeamCity Agent Process
.NOTES
Will not work except when running in a direct agent process. Checks for TeamCity agent environment
variables to exist.
#>
## Duplicates some of Alkami.DevOps.Common\Public\Test-IsTeamCityProcess.ps1
[CmdletBinding()]
[OutputType([System.Boolean])]
Param()
$teamCityJREVariable = $ENV:TEAMCITY_JRE
if ([String]::IsNullOrEmpty($teamCityJREVariable)) {
Write-Verbose "[Test-IsTeamCityProcess] TEAMCITY_JRE User Environment Variable Not Found"
return $false
}
return $true
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,65 @@
Function Update-AliasesInPSD1 {
<#
.SYNOPSIS
Concatenates all the aliases from Get-Aliases for insertion into the PSD1 file referenced.
.EXAMPLE
Update-AliasesInPSD1 .\Alkami.PowerShell.IIS\Public .\Alkami.PowerShell.IIS\Alkami.PowerShell.IIS.psd1
.PARAMETER FolderPath
The name of the folder to examine all files under.
.PARAMETER Psd1FilePath
The file to be rewritten. (This is called out in case more than one .psd1 file exists in a folder.)
#>
[CmdletBinding()]
Param (
[String]$FolderPath,
[String]$Psd1FilePath
)
process {
$aliasNames = @(Get-Aliases $FolderPath) | Sort-Object
if ($aliasNames.Count -eq 0) {
return
}
$toBeJoinedAliases = @()
foreach($name in $aliasNames) {
$toBeJoinedAliases += "'$name'"
}
$joinedAliases = $toBeJoinedAliases -Join ','
$contents = (Get-Content $Psd1FilePath)
$newContents = @()
$foundAliasesLine = $false
foreach($line in $contents) {
if ($line.Trim().StartsWith("AliasesToExport")) {
$splits = $line -split '='
$line = $splits[0].TrimEnd() + " = $joinedAliases"
$foundAliasesLine = $true
}
$newContents += $line
}
if (!$foundAliasesLine) {
$newContents = @()
## We couldn't find an existing aliases line in that PSD1, so we need to add one.
## We are gonna add it after FunctionsToExport
foreach($line in $contents) {
$newContents += $line
if ($line.Trim().StartsWith("FunctionsToExport")) {
$splits = $line -split '='
$newLine = $splits[0].TrimEnd().Replace('FunctionsToExport','AliasesToExport') + " = $joinedAliases"
$newContents += $newLine
}
}
}
Set-Content -Path $Psd1FilePath -Value $newContents
}
}

View File

@ -0,0 +1,61 @@
Function Update-FormatsToProcessInPSD1 {
<#
.SYNOPSIS
Updates the PSD1 FormatsToProcess entry
.EXAMPLE
Update-FormatsToProcessInPSD1 .\Alkami.PowerShell.IIS\Public .\Alkami.PowerShell.IIS\Alkami.PowerShell.IIS.psd1
.PARAMETER FolderPath
The name of the folder to examine all files under.
.PARAMETER Psd1FilePath
The file to be rewritten. (This is called out in case more than one .psd1 file exists in a folder.)
#>
[CmdletBinding()]
Param (
[String]$FolderPath,
[String]$Psd1FilePath
)
process {
# In the future, may want to ship all the format files separately
# Currently there is Get-ContentFromFormatFilesInPath and Join-PS1XMLFromFiles that smacks these two together
# So for now, we just want to write out the name of the built ps1xml to the psd1
# The names are the same, so that's easy.
$contents = (Get-Content $Psd1FilePath)
$newContents = @()
$projectName = [System.IO.Path]::GetFileNameWithoutExtension($psd1Filepath)
$magicWord = 'FormatsToProcess'
$magicConcat = " = `"$projectName.ps1xml`""
$foundFormatsLine = $false
foreach($line in $contents) {
if ($line.Trim().StartsWith($magicWord)) {
$splits = $line -split '='
$line = $splits[0].TrimEnd() + $magicConcat
$foundFormatsLine = $true
}
$newContents += $line
}
if (!$foundFormatsLine) {
$newContents = @()
## We couldn't find an existing formats line in that PSD1, so we need to add one.
## We are gonna add it after FunctionsToExport
$duplicatableTag = 'FunctionsToExport'
foreach($line in $contents) {
$newContents += $line
if ($line.Trim().StartsWith($duplicatableTag)) {
$splits = $line -split '='
$newLine = $splits[0].TrimEnd().Replace($duplicatableTag,$magicWord) + $magicConcat
$newContents += $newLine
}
}
}
Set-Content -Path $Psd1FilePath -Value $newContents
}
}

View File

@ -0,0 +1,43 @@
Function Update-FunctionNamesInPSD1 {
<#
.SYNOPSIS
Concatenates all the function names from Get-FunctionNames for insertion into the PSD1 file referenced.
.EXAMPLE
Update-FunctionNamesInPSD1 .\Alkami.PowerShell.IIS\Public .\Alkami.PowerShell.IIS\Alkami.PowerShell.IIS.psd1
.PARAMETER FolderPath
The name of the folder to examine all files under.
.PARAMETER Psd1FilePath
The file to be rewritten. (This is called out in case more than one .psd1 file exists in a folder.)
#>
[CmdletBinding()]
Param (
[String]$FolderPath,
[String]$Psd1FilePath
)
process {
$functionNames = @(Get-FunctionNames $FolderPath) | Sort-Object
$toBeJoinedFunctionNames = @()
foreach($name in $functionNames) {
$toBeJoinedFunctionNames += "'$name'"
}
$joinedFunctionNames = $toBeJoinedFunctionNames -Join ','
$contents = (Get-Content $Psd1FilePath)
$newContents = @()
foreach($line in $contents) {
if ($line.Trim().StartsWith("FunctionsToExport")) {
$splits = $line -split '='
$line = $splits[0].TrimEnd() + " = $joinedFunctionNames"
}
$newContents += $line
}
Set-Content -Path $Psd1FilePath -Value $newContents
}
}

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-16"?>
<package>
<metadata>
<id>Alkami.DevOps.Certificates</id>
<version>$version$</version>
<title>Alkami Platform Modules - DevOps - Certificates</title>
<authors>Alkami Technologies</authors>
<owners>Alkami Technologies</owners>
<projectUrl>https://extranet.alkamitech.com/display/ORB/Alkami.DevOps.Certificates</projectUrl>
<iconUrl>https://www.alkami.com/files/alkamilogo75x75.png</iconUrl>
<licenseUrl>http://alkami.com/files/orblicense.html</licenseUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Installs the DevOps Certificates module for use with PowerShell.</description>
<releaseNotes />
<tags>PowerShell</tags>
<copyright>Copyright (c) 2018 Alkami Technologies</copyright>
<dependencies>
<dependency id="Alkami.PowerShell.Common" version="3.2.6" />
<dependency id="Alkami.PowerShell.Services" version="3.1.7" />
<dependency id="Alkami.Ops.Common" version="3.0.3" />
<dependency id="Alkami.PowerShell.IIS" version="3.4.0" />
<dependency id="Alkami.Ops.SecretServer" version="3.0.2" />
</dependencies>
</metadata>
<files>
<file src="AlkamiManifest.xml" target="AlkamiManifest.xml" />
<file src="tools\ChocolateyInstall.ps1" target="tools\ChocolateyInstall.ps1" />
<file src="tools\ChocolateyUninstall.ps1" target="tools\ChocolateyUninstall.ps1" />
<!-- <file src="public\*.ps1" target="module\public" /> -->
<!-- <file src="private\*.ps1" target="module\private" /> -->
<file src="*.psd1" target="module\" />
<file src="*.psm1" target="module\" />
</files>
</package>

View File

@ -0,0 +1,21 @@
@{
RootModule = 'Alkami.DevOps.Certificates.psm1'
ModuleVersion = '3.20.11'
GUID = 'bf9b5927-92e8-4eff-b8f3-c19cc554e4c2'
Author = 'SRE'
CompanyName = 'Alkami Technologies, Inc.'
Copyright = '(c) 2018 Alkami Technologies, Inc. All rights reserved.'
Description = 'A set of functions used to deploy the ORB application'
PowerShellVersion = '5.0'
RequiredModules = 'Alkami.PowerShell.Common','Alkami.PowerShell.Services','Alkami.Ops.Common','Alkami.PowerShell.IIS','Alkami.Ops.SecretServer'
FunctionsToExport = 'Compress-Certificates','Export-Certificates','Export-CertificatesToFileSystem','Get-ExpiringCertificates','Get-PrivateKeyPermissions','Get-SecretServerConnection','Import-Certificates','Import-PfxCertificateWithPermissions','Import-PodFromSecretServer','Publish-PodToSecretServer','Read-AppTierCertificates','Read-Certificates','Read-WebTierCertificates','Remove-Certificate','Save-CertificatesToDisk','Update-CertBindings'
AliasesToExport = 'Load-AppTierCertificates','Load-Certificates','Load-WebTierCertificates'
PrivateData = @{
PSData = @{
Tags = @('powershell', 'module', 'deploy', 'deployment')
ProjectUri = 'Https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Certificate+Module'
IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png'
}
}
HelpInfoURI = 'Https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Certificate+Module'
}

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{90fa52ce-26c5-44f0-a0c5-0ac3355e6fdc}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>MyApplication</RootNamespace>
<AssemblyName>MyApplication</AssemblyName>
<Name>Alkami.DevOps.Certificates</Name>
<PreBuildScript>..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.DevOps.Certificates")</PreBuildScript>
<PostBuildScript>Invoke-Pester;</PostBuildScript>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Folder Include="Private\" />
<Folder Include="Public\" />
<Folder Include="tools\" />
</ItemGroup>
<ItemGroup>
<Compile Include="Alkami.DevOps.Certificates.psd1" />
<Compile Include="Private\Export-CertChain.ps1" />
<Compile Include="Private\Export-CertificateToFileSystem.ps1" />
<Compile Include="Private\Get-Cert.ps1" />
<Compile Include="Private\Get-CertificateChain.ps1" />
<Compile Include="Private\Get-CertificateExportInfo.ps1" />
<Compile Include="Private\Get-CertificateExportName.ps1" />
<Compile Include="Private\Get-CertificateStoreName.ps1" />
<Compile Include="Private\SecretServerConnection.ps1" />
<Compile Include="Private\SecretServerConnection.tests.ps1" />
<Compile Include="Public\Export-Certificates.tests.ps1" />
<Compile Include="Public\Export-CertificatesToFileSystem.ps1" />
<Compile Include="Public\Get-PrivateKeyPermissions.ps1" />
<Compile Include="Public\Get-SecretServerConnection.ps1" />
<Compile Include="Public\Import-Certificates.tests.ps1" />
<Compile Include="Public\Import-PodFromSecretServer.ps1" />
<Compile Include="Public\Read-AppTierCertificates.ps1" />
<Compile Include="Public\Read-Certificates.ps1" />
<Compile Include="Public\Read-WebTierCertificates.ps1" />
<Compile Include="Public\Publish-PodToSecretServer.ps1" />
<Compile Include="Public\Save-CertificatesToDisk.ps1" />
<Compile Include="Public\Update-CertBindings.tests.ps1" />
<Compile Include="tools\chocolateyInstall.ps1" />
<Compile Include="tools\chocolateyUninstall.ps1" />
<Compile Include="Private\Confirm-Cert.ps1" />
<Compile Include="Private\Export-Cert.ps1" />
<Compile Include="Public\Compress-Certificates.ps1" />
<Compile Include="Public\Export-Certificates.ps1" />
<Compile Include="Public\Get-ExpiringCertificates.ps1" />
<Compile Include="Private\Import-Cert.ps1" />
<Compile Include="Public\Import-Certificates.ps1" />
<Compile Include="Private\Set-CertPermissions.ps1" />
<Compile Include="Public\Update-CertBindings.ps1" />
<Compile Include="Private\VariableDeclarations.ps1" />
</ItemGroup>
<ItemGroup>
<Content Include="Alkami.DevOps.Certificates.nuspec" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Target Name="Build" />
<Import Project="$(MSBuildExtensionsPath)\PowerShell Tools for Visual Studio\PowerShellTools.targets" Condition="Exists('$(MSBuildExtensionsPath)\PowerShell Tools for Visual Studio\PowerShellTools.targets')" />
</Project>

View File

@ -0,0 +1,12 @@
<?xml version="1.0"?>
<packageManifest>
<version>1.0</version>
<general>
<creatorCode>Alkami</creatorCode>
<element>Alkami.DevOps.Certificates</element>
<componentType>SREModule</componentType>
</general>
<moduleManifest>
<status>Production</status>
</moduleManifest>
</packageManifest>

View File

@ -0,0 +1,46 @@
function Confirm-Cert {
<#
.SYNOPSIS
Validates that Certificate is provided.
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]$certName,
[parameter(Mandatory=$true)]
[System.Security.Cryptography.X509Certificates.StoreName]$storeName
)
try
{
$OriginalErrorActionPreference = $ErrorActionPreference;
$ErrorActionPreference = "Continue";
Write-Output ("Validating certificate $certName");
[Alkami.Ops.Common.Cryptography.CertificateHelper]::ValidateCertificate(
$certName,
$storeName,
[System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine);
Write-Output ("Certificate $certName Passed Validation");
}
catch [Alkami.Ops.Common.Exceptions.InvalidCertificateException]
{
Write-Warning ("Certificate validation failed");
Write-Host (" Error: " + $_.Exception.Message);
Write-Host (" Name: " + $_.Exception.CertificateName);
Write-Host (" Thumbprint: " + $_.Exception.CertificateThumbPrint);
Write-Host (" Effective Date: " + $_.Exception.EffectiveDateTime);
Write-Host (" Expiration Date: " + $_.Exception.ExpirationDateTime + "`n");
}
finally
{
$ErrorActionPreference = $OriginalErrorActionPreference;
}
}

View File

@ -0,0 +1,37 @@
function Export-Cert {
<#
.SYNOPSIS
Exports a Certificate.
#>
[CmdletBinding()]
[OutputType([System.Object])]
Param(
[parameter(Mandatory=$true)]
[string]$exportPath,
[parameter(Mandatory=$false)]
[string]$exportPassword,
[parameter(Mandatory=$true)]
[System.Security.Cryptography.X509Certificates.StoreName]$storeName
)
if ($exportPassword)
{
return ,[Alkami.Ops.Common.Cryptography.CertificateHelper]::ExportAllCertificates(
$storeName,
[System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine,
$exportPath,
$exportPassword
);
}
return ,[Alkami.Ops.Common.Cryptography.CertificateHelper]::ExportAllCertificates(
$storeName,
[System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine,
$exportPath
);
}

View File

@ -0,0 +1,36 @@
function Export-CertChain {
<#
.SYNOPSIS
Exports a Certificate's Chain.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $True)]
[ValidateNotNull()]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert,
[Parameter(Mandatory = $True)]
[string]$ExportStorePath,
[Parameter(Mandatory = $True)]
[string]$ExportCertPath,
$ADGroups
)
$certName = $exportCertPath.Split("\") | Select-Object -Last 1
$chain = Get-CertificateChain $cert $exportStorePath
$chainInfo = [System.Collections.ArrayList]::new()
foreach ($chainCert in $chain) {
$chainCertStore = Get-CertificateStoreName $chainCert
if (!$chainCertStore) {
Write-Warning "Chain is broken for cert $certName and thumbprint $($chainCert.thumbprint)"
break
}
$exportChainPath = $exportCertPath, "ChainedCertificates", $chainCertStore -join "\"
$exportInfo = Export-CertificateToFileSystem $chainCert $exportChainPath -IsChainExport $true -ADGroups $ADGroups
if ($null -eq $exportInfo) {break}
[void]$chainInfo.Add($exportInfo)
}
return $chainInfo
}

View File

@ -0,0 +1,48 @@
function Export-CertificateToFileSystem {
<#
.SYNOPSIS
Exports a Certificate to a Store.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $True)]
[ValidateNotNull()]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert,
[Parameter(Mandatory = $True)]
[ValidateNotNull()]
[string]$ExportStorePath,
[bool]$IsChainExport = $false,
[string[]]$ADGroups
)
$certName = Get-CertificateExportName $cert
$exportCertPath = if ($IsChainExport) {$exportStorePath}else {Join-Path $exportStorePath $certName}
$exportInfo = Get-CertificateExportInfo $cert $exportCertPath
if ($exportInfo.certExportType -eq [System.Security.Cryptography.X509Certificates.X509ContentType]::Unknown) {return $null}
if (-Not (Test-Path $exportCertPath -PathType Container)) {
New-Item $exportCertPath -ItemType Directory | Out-Null
}
$exportInfo.certName = $certName
$exportInfo.certPassword = ([char[]]([char]33..[char]95) + ([char[]]([char]97..[char]126)) + 0..9 | Sort-Object {Get-Random})[0..128] -join ''
$exportInfo.ADGroups = $ADGroups
if ($exportInfo.certExportType -eq [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx) {
$password = $exportInfo.certPassword | ConvertTo-SecureString -AsPlainText -Force
try {
Export-PfxCertificate -Cert $cert -ProtectTo $ADGroups -FilePath $exportInfo.exportCertFile -Password $password
}
catch {
Write-Warning "Certificate $certName with thumbprint $($cert.Thumbprint) but could not be exported
$($_.Exception.Message)"
return $null
}
}
else {
$certBytes = $cert.Export($exportInfo.certExportType)
[void][io.file]::WriteAllBytes($exportInfo.exportCertFile, $certBytes)
}
return $exportInfo
}

View File

@ -0,0 +1,36 @@
function Get-Cert {
<#
.SYNOPSIS
Fetches a Certificate.
#>
[CmdletBinding()]
param(
[string]$Thumbprint,
[string]$StoreName,
[string]$FriendlyName
)
$certStore = @{ }
$certStores = Get-ChildItem Cert:\LocalMachine\ | ForEach-Object { "Cert:\LocalMachine\$($_.Name)" }
if ($StoreName) {
$certStores = $certStores | Where-Object { $_ -Match "\\$StoreName" }
}
foreach ($store in $certStores) {
Get-ChildItem $store | Where-Object { $_.NotAfter -gt (Get-Date) } | ForEach-Object {
if ($certStore.ContainsKey($_.Thumbprint)) {
$certStore[$_.Thumbprint].Add($_)
} else {
$list = [System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Certificate]]::new()
$list.Add($_)
$certStore.Add($_.Thumbprint, $list)
}
}
}
if ($Thumbprint) {
return $certStore[$Thumbprint]
} elseif ($FriendlyName) {
return $certStore.Values | Where-Object { $_.FriendlyName -match $FriendlyName }
}
return $certStore.Values
}

View File

@ -0,0 +1,14 @@
function Get-CertificateChain {
<#
.SYNOPSIS
Fetches a Certificate's Chain.
#>
[CmdletBinding()]
param(
[Parameter()]
$Cert
)
$chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
[void]$chain.Build($cert)
return $chain.ChainElements.Certificate | Select-Object -Skip 1
}

View File

@ -0,0 +1,37 @@
function Get-CertificateExportInfo {
<#
.SYNOPSIS
Fetches a Certificate's Export Information.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert,
[Parameter(Mandatory = $true)]
[string]$ExportCertPath)
$exportInfo = [PSCustomObject]@{
CertExportType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx
ExportCertFile = Join-Path $exportCertPath "$certName.pfx"
ExportCertPath = $exportCertPath
CertPassword = ""
ADGroups = ""
CertName = ""
ExpirationDate = $cert.NotAfter
Thumbprint = $cert.Thumbprint
}
if ($cert.HasPrivateKey) {
if (!$cert.PrivateKey.CspKeyContainerInfo.Exportable) {
Write-Warning "Certificate $certName with thumbprint $($cert.Thumbprint) has a private key but is marked as unexportable.
This certificate will not be exported"
$exportInfo.certExportType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Unknown
}
}
else {
$exportInfo.certExportType = [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert
$exportInfo.exportCertFile = Join-Path $exportCertPath "$certName.cer"
}
return $exportInfo
}

View File

@ -0,0 +1,19 @@
function Get-CertificateExportName {
<#
.SYNOPSIS
Fetches a Certificate's Export Name.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
$Cert
)
$canonicalName = ($cert.Subject.Trim().Split(",") | Where-Object {$_ -match "CN="} | Select-Object -First 1 ) -replace "CN=", ""
$invalidFileNameChars = [IO.Path]::GetInvalidFileNameChars() -join ''
$validFileNameCN = ($canonicalName -replace ("[{0}]" -f [RegEx]::Escape($invalidFileNameChars)))
$certName = if ($validFileNameCN) { $validFileNameCN } else { $cert.Thumbprint }
return $certName.Trim()
}

View File

@ -0,0 +1,15 @@
function Get-CertificateStoreName {
<#
.SYNOPSIS
Fetches a Certificate's Store Name.
#>
[CmdletBinding()]
param(
[Parameter()]
$Cert
)
$psParent = Get-Cert -Thumbprint $cert.Thumbprint | Select-Object -ExpandProperty PSParentPath
if (!$psParent) {return}
$storeName = $psParent.Split("\") | Select-Object -Last 1
return $storeName
}

View File

@ -0,0 +1,33 @@
function Import-Cert {
<#
.SYNOPSIS
Imports a Certificate into a Store.
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]$certFullName,
[parameter(Mandatory=$true)]
[System.Security.Cryptography.X509Certificates.StoreName]$storeName,
[Parameter(Mandatory=$false)]
[string]$importPassword
)
if ($importPassword)
{
return [Alkami.Ops.Common.Cryptography.CertificateHelper]::LoadCertificateToStore(
$certFullName,
$storeName,
[System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine,
$importPassword);
}
return [Alkami.Ops.Common.Cryptography.CertificateHelper]::LoadCertificateToStore(
$certFullName,
$storeName,
[System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine);
}

View File

@ -0,0 +1,195 @@
# Ignore Measure-HelpSynopsis warnings in the PSScript Analyzer
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('Alkami.PowerShell.PSScriptAnalyzerRules\Measure-HelpSynopsis', '', Scope = 'Class')]
# Ignore Measure-HelpSynopsis warnings in the PSScript Analyzer - PS Classes can't do CmdletBinding, afaict
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('Alkami.PowerShell.PSScriptAnalyzerRules\Measure-CmdletBinding', '', Scope = 'Class')]
Class SecretServerConnection {
[string]$api
[string]$site
[string]$userName
[string]$domain
[string]$password
[string]$tokenRoute
[ScriptBlock]$commonFilters = { "?filter.includeRestricted=true&filter.searchtext=$searchString&filter.folderId=$folderId&filter.includeSubFolders=true" }
[ScriptBlock]$updateEndpoint
[System.Collections.Generic.Dictionary[[String], [String]]]$commonHeader
$token
SecretServerConnection() { }
SecretServerConnection([string]$site, [string]$userName, [string]$password) {
$this.site = $site
$this.tokenRoute = "$site/oauth2/token"
$this.api = $site, "api/v1" -join "/"
$this.updateEndpoint = { "/secrets/$secretId/fields/$fieldToUpdate" }
$this.userName = $userName
$this.password = $password
$this.commonHeader = [System.Collections.Generic.Dictionary[[String], [String]]]::new()
}
[void]Authenticate() {
$this.Authenticate($False)
}
[void]Authenticate([bool]$UseTwoFactor) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$creds = @{
username = $this.username
password = $this.password
grant_type = "password"
}
$headers = $null
If ($UseTwoFactor) {
$headers = @{
"OTP" = (Read-Host -Prompt "Enter your OTP for 2FA: ")
}
}
try {
$response = Invoke-RestMethod $this.tokenRoute -Method Post -Body $creds -Headers $headers
$this.token = $response.access_token;
if ($this.commonHeader.Count -gt 0) { $this.commonHeader.Clear() }
$this.commonHeader.Add("Authorization", "Bearer $($this.token)")
} catch {
throw $_
}
}
[object]GetSecretByName([string]$secretName, [string]$folderId) {
# $searchString is used in $commonFilters above. This "not used" warning is a lie.
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False Positive')]
$searchString = $secretName
$filters = $this.commonFilters.Invoke()
Write-Debug "$($this.api)/secrets/lookup$filters"
$result = Invoke-RestMethod "$($this.api)/secrets$filters" -Headers $this.commonHeader
return $result
}
[object]GetSecretById([string]$secretId) {
Write-Debug "$($this.api)/secrets/$secretId"
$response = Invoke-RestMethod "$($this.api)/secrets/$secretId" -Headers $this.commonHeader
return $response
}
[object]GetSecretByFolderId([string]$folderId) {
$parameters = "?filter.folderId=$folderId"
$response = Invoke-RestMethod "$($this.api)/secrets/$parameters" -Headers $this.commonHeader
return $response.records
}
[object]GetSecretTemplateById([int]$templateId, [int]$folderId) {
$secret = Invoke-RestMethod "$($this.api)/secrets/stub?filter.secrettemplateid=$templateId&filter.folderId=$folderId" -Headers $this.commonHeader
return $secret
}
[int]GetSecretTemplateIdByName([string]$templateName) {
$searchString = "?filter.searchText=$templateName"
$secret = Invoke-RestMethod -Method Get "$($this.api)/secret-templates$searchString" -Headers $this.commonHeader
if ($secret.records) {
$record = $secret.records | Where-Object { $_.name -eq $templateName }
return $record.id;
}
return -1
}
[int]CreateSecret([object]$secret, [int]$folderId, [string]$secretName) {
$secret.name = $secretName
$secret.siteId = 1
$secret.folderId = $folderId
# Get Secret Template first, set up the template with various items and their values and then pass it to create secret
$requestCreateSecretParams = $secret | ConvertTo-Json
$secret = Invoke-RestMethod "$($this.api)/secrets/" -Method Post -Body $requestCreateSecretParams -Headers $this.commonHeader -ContentType "application/json"
return $secret.id
}
[object]UpdateField([string]$secretId, [string]$fieldToUpdate, [string]$newValue) {
$body = @{ value = $newValue } | ConvertTo-Json
$response = Invoke-RestMethod -Method Put -Uri "$($this.api)$($this.updateEndpoint.Invoke())" -Headers $this.commonHeader -ContentType "application/json" -Body $body
return $response
}
[object]GetField([string]$secretId, [string]$field) {
$response = Invoke-RestMethod -Method Get -Uri "$($this.api)$($this.updateEndpoint.Invoke())" -Headers $this.commonHeader
return $response.Records
}
[void]UploadFile([int]$secretId, [string]$fieldToUpdate, [string]$filePath) {
$fileName = Get-ChildItem $filePath | Select-Object -ExpandProperty Name
$requestUploadFileParams = @{
fileName = $fileName;
fileAttachment = [IO.File]::ReadAllBytes($filePath)
} | ConvertTo-Json
Invoke-RestMethod -Method Put -Uri "$($this.api)$($this.updateEndpoint.Invoke())" -Headers $this.commonHeader -Body $requestUploadFileParams -ContentType "application/json"
}
[bool]DownloadFile([int]$secretId, [string]$fieldToupdate, [string]$filePath) {
# invokeing $this.updateEndpoint.Invoke() sets variables hidden above in SecretServerConnection
Write-Debug "$($this.api)$($this.updateEndpoint.Invoke())"
try {
Invoke-RestMethod -Method Get -Uri "$($this.api)$($this.updateEndpoint.Invoke())" -Headers $this.commonHeader -OutFile $filePath | Out-Null
return $true
} catch {
Write-Warning "An error occurred while downloading a file."
Write-Warning -Message "StatusCode: $($_.Exception.Response.StatusCode.value__)"
Write-Warning -Message "StatusDescription: $($_.Exception.Response.StatusDescription)"
return $false
}
}
[int]AddFolder([int]$parentFolderId, [string]$folderName, [bool]$inheritPermissions, [bool]$inheritSecretPolicy) {
$folderStub = Invoke-RestMethod "$($this.api)/folders/stub" -Method GET -Headers $this.commonHeader -ContentType "application/json"
$folderStub.folderName = $folderName
$folderStub.folderTypeId = 1
$folderStub.inheritPermissions = $inheritPermissions
$folderStub.inheritSecretPolicy = $inheritSecretPolicy
$folderStub.parentFolderId = $parentFolderId
$folderArgs = $folderStub | ConvertTo-Json
$folderAddResult = Invoke-RestMethod "$($this.api)/folders" -Method POST -Body $folderArgs -Headers $this.commonHeader -ContentType "application/json"
return $folderAddResult.id
}
[int]AddFolder([int]$parentFolderId, [string]$folderName) {
return $this.AddFolder($parentFolderId, $folderName, $true, $true)
}
[object]GetFolderById([int]$folderId) {
$folderGetResult = Invoke-RestMethod "$($this.api)/folders/$folderId" -Method GET -Headers $this.commonHeader -ContentType "application/json"
return $folderGetResult.records
}
[object]GetChildFolders([int]$parentId) {
$parameters = "?filter.parentFolderId=$parentId"
$folderGetResult = Invoke-RestMethod "$($this.api)/folders/$parameters" -Method GET -Headers $this.commonHeader -ContentType "application/json"
return $folderGetResult.records
}
[object]GetFolderIdByName([string]$folderName) {
$searchFilter = "?filter.searchText=$folderName"
$searchResults = Invoke-RestMethod "$($this.api)/folders$searchFilter" -Method GET -Headers $this.commonHeader -ContentType "application/json"
return $searchResults.Records.Id
}
[object]GetFolderIdByName([string]$folderName, [int]$parentFolderId) {
$searchString = $folderName
$folderId = $parentFolderId
$filters = "?filter.includeRestricted=true&filter.parentFolderId=$folderId&filter.searchtext=%$searchString"
Write-Debug $filters
$searchResults = Invoke-RestMethod "$($this.api)/folders$filters" -Method GET -Headers $this.commonHeader -ContentType "application/json"
return $searchResults.Records.Id
}
}

View File

@ -0,0 +1,61 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$global:functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
#Import-Module $functionPath -Force
$moduleForMock = ""
InModuleScope -ModuleName Alkami.DevOps.Certificates -ScriptBlock {
Write-Host "InModuleScope - Overriding SUT: $($global:functionPath)"
Import-Module $global:functionPath -Force
$moduleForMock = ""
$inScopeModuleForAssert = "Alkami.DevOps.Certificates"
Describe "SecretServerConnection" {
Context "When Calling GetSecretByName" {
Mock Invoke-RestMethod {
#Write-Warning "Mocked Invoke-RestMethod"
if ($uri -like "*CertName*123*") {
return New-Object psobject -Property @{
Name = "I'm a fake Secret"
}
} else {
return $null
}
} -ModuleName $moduleForMock
It "Returns Secrets Which Match The Supplied Name And FolderId" {
$connection = [SecretServerConnection]::new()
$result = $connection.GetSecretByName("CertName", 123)
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Invoke-RestMethod
$result.Name | should -BeLike "I'm a fake Secret"
}
It "Does Not Return Secrets Which Do Not Match The Supplied Name" {
$connection = [SecretServerConnection]::new()
$result = $connection.GetSecretByName("BadName", 123)
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Invoke-RestMethod
$result | should -BeNullOrEmpty
}
It "Does Not Return Secrets Which Do Match The Supplied Name But Not the Folder Id" {
$connection = [SecretServerConnection]::new()
$result = $connection.GetSecretByName("CertName", 456)
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Invoke-RestMethod
$result | should -BeNullOrEmpty
}
}
}
}

View File

@ -0,0 +1,42 @@
function Set-CertPermissions {
<#
.SYNOPSIS
Assigns Certificate Permissions for a user.
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[string]$certThumprint,
[Parameter(Mandatory=$true)]
[string]$user
)
$logLead = Get-LogLeadName
$certObj = Get-ChildItem "Cert:\LocalMachine\my\$certThumprint"
$rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($CertObj)
if ($rsaCert.key -and $rsaCert.key.UniqueName) {
$fileName = $rsaCert.key.UniqueName
$directoryRsaMachineKeys = Join-Path "C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\" $fileName
$directoryCryptoKeys = Join-Path "C:\ProgramData\Microsoft\Crypto\Keys\" $fileName
if (Test-Path $directoryRsaMachineKeys) {
$path = $directoryRsaMachineKeys
} elseif (Test-Path $directoryCryptoKeys) {
$path = $directoryCryptoKeys
} else {
Write-Error "$logLead : Did not find an associated ACL File for $certThumbprint."
}
} else {
Write-Error "$logLead : Unable to determine Unique Key Name for $certThumprint"
}
$permissions = Get-Acl -Path $path
$rule = New-Object Security.AccessControl.FileSystemAccessRule $user, "FullControl", Allow
$permissions.AddAccessRule($rule)
Set-Acl -Path $path -AclObject $permissions
}

View File

@ -0,0 +1,21 @@
function Compress-Certificates {
<#
.SYNOPSIS
Combine Certificates into a .zip File.
#>
[CmdletBinding()]
param(
$Certificates,
$TempFolder
)
#Prepare certificate folders by zipping them.
foreach ($certificate in $Certificates) {
$zipFileName = $certificate.Name.Trim() + ".zip"
$CompressedDir = $TempFolder, $zipFileName -join "\"
Remove-Item $CompressedDir -Force -ErrorAction SilentlyContinue
Remove-Item (Join-Path $certificate.Folder $zipFileName) -Force -ErrorAction SilentlyContinue
[System.IO.Compression.ZipFile]::CreateFromDirectory($certificate.Folder,
$CompressedDir, [System.IO.Compression.CompressionLevel]::Optimal, $false)
Move-Item $CompressedDir $certificate.Folder -Force
}
}

View File

@ -0,0 +1,153 @@
function Export-Certificates {
<#
.SYNOPSIS
Exports certificates from a machine.
.PARAMETER exportPassword
The password used to secure the certificate with
.PARAMETER exportPath
The path the certificates are exported to. If no path is defined, the current working directory is used
.PARAMETER skipRootCerts
When this flag is supplied it will skip the exporting of certificates in the 'Root' store
.PARAMETER skipPersonalCerts
When this flag is supplied it will skip the exporting of certificates in the 'My' store
.PARAMETER skipTrustedCerts
When this flag is supplied it will skip the exporting of certificates in the 'Trusted' store
.PARAMETER skipIACerts
When this flag is supplied it will skip the exporting of certificates in the 'CertificateAuthority' store
#>
[CmdletBinding()]
Param(
[parameter(Mandatory=$false)]
[string]$exportPassword,
[Parameter(Mandatory=$false)]
[string]$exportPath = $PWD,
[Parameter(Mandatory=$false)]
[switch]$skipRootCerts,
[Parameter(Mandatory=$false)]
[switch]$skipPersonalCerts,
[Parameter(Mandatory=$false)]
[switch]$skipTrustedCerts,
[Parameter(Mandatory=$false)]
[switch]$skipIACerts
)
if (!$skipPersonalCerts.IsPresent -and !$exportPassword)
{
throw "Export Password cannot be null"
}
if ($skipRootCerts.IsPresent -and $skipPersonalCerts.IsPresent -and $skipTrustedCerts.IsPresent -and $skipIACerts.IsPresent)
{
throw "All Skip Switches cannot be set"
}
if (!(Test-Path $exportPath))
{
[System.IO.Directory]::CreateDirectory($exportPath) | Out-Null
}
## Removing because of issues mocking. This shouldn't be an issue.
# Clear-Host
[System.Reflection.Assembly]::LoadWithPartialName("System.Security.Cryptography") | Out-Null
## TODO: Don't just blindly set the $ErrorActionPreference
$ErrorActionPreference = "Stop"
[Collections.Generic.List[Alkami.Ops.Common.Exceptions.CertificateExportException]]$exportErrors = @()
if (!($skipPersonalCerts.IsPresent))
{
Write-Host "Exporting Personal Certs"
$pfxExportPath = (Join-Path $exportPath "Personal")
if (!(Test-Path $pfxExportPath))
{
Write-Host "Creating directory at $pfxExportPath"
[System.IO.Directory]::CreateDirectory($pfxExportPath) | Out-Null
}
$errors = Export-Cert -exportPath $pfxExportPath $exportPassword -storeName ([System.Security.Cryptography.X509Certificates.StoreName]::My)
$exportErrors.AddRange($errors)
}
if (!($skipIACerts.IsPresent))
{
Write-Host "Exporting IA Certs"
$iaExportPath = (Join-Path $exportPath "IA")
if (!(Test-Path $iaExportPath))
{
[System.IO.Directory]::CreateDirectory($iaExportPath) | Out-Null
}
$errors = Export-Cert -exportPath $iaExportPath -storeName ([System.Security.Cryptography.X509Certificates.StoreName]::CertificateAuthority)
$exportErrors.AddRange($errors)
}
if (!($skipRootCerts.IsPresent))
{
Write-Host "Exporting Root Certs"
$rootExportPath = (Join-Path $exportPath "Root")
if (!(Test-Path $rootExportPath))
{
[System.IO.Directory]::CreateDirectory($rootExportPath) | Out-Null
}
$errors = Export-Cert -exportPath $rootExportPath -storeName ([System.Security.Cryptography.X509Certificates.StoreName]::Root)
$exportErrors.AddRange($errors)
}
if (!($skipTrustedCerts.IsPresent))
{
Write-Host "Exporting Trusted Certs"
$trustedPeopleExportPath = (Join-Path $exportPath "TrustedPeople")
if (!(Test-Path $trustedPeopleExportPath))
{
[System.IO.Directory]::CreateDirectory($trustedPeopleExportPath) | Out-Null
}
$errors = Export-Cert -exportPath $trustedPeopleExportPath -storeName ([System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople)
$exportErrors.AddRange($errors)
}
foreach ($exportError in $exportErrors)
{
[Alkami.Ops.Common.Exceptions.CertificateExportException]$strongError = $exportError
Write-Warning ("{0}" -f $strongError.Message)
Write-Warning ("`tError: {0}" -f $strongError.BaseExceptionMessage.TrimEnd())
Write-Warning ("`tName: {0}" -f $strongError.CertificateName)
Write-Warning ("`tThumbprint: {0}" -f $strongError.CertificateThumbPrint)
Write-Warning ("`tSubject: {0}" -f $strongError.Subject.Trim())
Write-Output `n
}
}

View File

@ -0,0 +1,128 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
# Yes. I hate this at least as much as you do. If you can find a way around it, please show me.
$global:functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
$compiledModuleForMock = "Alkami.DevOps.Certificates"
$exportPassword = "Test"
$exportPath = "c:\temp\CertificateTest"
Remove-FileSystemItem -Path $exportPath -Force -Recurse -ErrorAction SilentlyContinue | Out-Null
New-Item -ItemType Directory $exportPath -Force | Out-Null
InModuleScope -ModuleName Alkami.DevOps.Certificates -ScriptBlock {
Write-Host "InModuleScope - Overriding SUT: $($global:functionPath)"
Import-Module $global:functionPath -Force
$inScopeModuleForAssert = "Alkami.DevOps.Certificates"
$exportPassword = "Test"
$exportPath = "c:\temp\CertificateTest"
$moduleForMock = ""
Mock -CommandName Export-Cert {
[Collections.Generic.List[Alkami.Ops.Common.Exceptions.CertificateExportException]] $emptyArr = @()
return ,$emptyArr
} -ModuleName $moduleForMock
#prevents the mehtod under test from clearing the screen during testing.
Mock -ModuleName $moduleForMock -CommandName Clear-Host {}
Mock -CommandName Write-Host -MockWith {} -ModuleName $moduleForMock
Describe "Export-Certificates" {
Context "When there are bad inputs when calling Export-Certificates" {
It "Throws Exception if all skip flags set" {
{ Export-Certificates $exportPassword -skipPersonalCert -skipRootCerts -skipTrustedCert -skipIACert } | Should Throw
}
}
Context "When the parameters are valid" {
It "Doesnt Require Password if not exporting Personal Certificates" {
{ Export-Certificates -skipPersonalCerts } | Should -Not -Throw
}
It "Creates Path if it doesn't exist" {
$path = 'c:\temp\badPath'
Export-Certificates $exportPassword -exportPath $path
$path | Should -Exist
}
It "Creates Personal Directory" {
Export-Certificates $exportPassword -exportPath $exportPath
Join-Path $exportPath "Personal" | Should -Exist
}
It "Creates IA Directory" {
Export-Certificates $exportPassword -exportPath $exportPath
Join-Path $exportPath "IA" | Should -Exist
}
It "Creates Root Directory" {
Export-Certificates $exportPassword -exportPath $exportPath
Join-Path $exportPath "Root" | Should -Exist
}
It "Creates TrustedPeople Directory" {
Export-Certificates $exportPassword -exportPath $exportPath
Join-Path $exportPath "TrustedPeople" | Should -Exist
}
It "Calls Export-Certs 4 times when no filters are used" {
Export-Certificates $exportPassword -exportPath $exportPath
#$result | Should be {}
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Export-Cert -Times 4 -Exactly -Scope It
}
It "Calls Export-Certs 3 times when skipRootCerts filter used" {
Export-Certificates $exportPassword -exportPath $exportPath -skipRootCerts
#$result | Should be {}
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Export-Cert -Times 3 -Exactly -Scope It
}
It "Calls Export-Certs 3 times when skipPersonalCerts filter used" {
Export-Certificates $exportPassword -exportPath $exportPath -skipPersonalCerts
#$result | Should be {}
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Export-Cert -Times 3 -Exactly -Scope It
}
It "Calls Export-Certs 3 times when skipTrustedCerts filter used" {
Export-Certificates $exportPassword -exportPath $exportPath -skipTrustedCerts
#$result | Should be {}
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Export-Cert -Times 3 -Exactly -Scope It
}
It "Calls Export-Certs 3 times when skipIACerts filter used" {
Export-Certificates $exportPassword -exportPath $exportPath -skipIACerts
#$result | Should be {}
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Export-Cert -Times 3 -Exactly -Scope It
}
}
}
}

View File

@ -0,0 +1,167 @@
function Export-CertificatesToFileSystem {
<#
.SYNOPSIS
Exports certificates from desired stores (My, CertificateAuthority, Root, and TrustedPeople by default)
onto the file system.
.DESCRIPTION
This script will export all public certificates and private keys in a given store to the file system. The
certificate chain is also exported in an individualized folder along with the certificate so that you
can delineate what certificates are needed for the one exported.
A password is generated for private keys, if ADGroups are given they are assigned to the private keys as well.
This script also returns a model of the data exported. With this model the file system data is more
easily manipulated and was originally intended to be used by a module for uploading these certificates
and their chains to individual secrets in secret server.
.PARAMETER PodName
String
Used to create a leaf folder with in which the certificates will be exported and set as a property
on the model returned as a result of this cmdlet.
.PARAMETER CertRoot
String
Location that this cmdlet will be working out of. This is set to LocalMachine but could be set to Personal
or other certificate providers.
.PARAMETER StoresToExport
StoreName[]
All certificates in these stores will be exported along with their certificate chains and private keys.
.PARAMETER ADGroups
String[]
Any ADGroups or UserAccounts that are given will be assigned to private keys exported. These users
will be able to import the private keys without a password, although one is generated and set
by this cmdlet regardless
.EXAMPLE
Export-CertificatesToFileSystem -PodName "Pod 1.1.1"
Base usage, will export all certificates in the default stores and their private keys to the Default Export Root
Folder under leaf folder 'Pod 1.1.1'. A password will be generated for the private keys and returned to the
caller in the model object.
.EXAMPLE
Export-CertificatesToFileSystem -PodName "Pod 7.0.5" -ADGroups "corp\JSmith","corp\CCarter" -RootExportFolder "C:\Exports"
Will export all certificates in the default stores and their private keys to the C:\Exports
Folder under leaf folder 'Pod 7.0.5'. Users JSmith and CCarter will be able to import the private keys
without the password, however passwords generated for these certificates will be returned with the
model object that this cmdlet produces.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $True)]
[string]$PodName,
[Parameter(Mandatory = $False)]
[string]$CertRoot = "Cert:\LocalMachine\",
[Parameter(Mandatory = $False)]
[string]$RootExportFolder = "c:\temp\CertExports",
[Parameter(Mandatory = $False)]
$StoresToExport = @([System.Security.Cryptography.X509Certificates.StoreName]::My,
[System.Security.Cryptography.X509Certificates.StoreName]::CertificateAuthority,
[System.Security.Cryptography.X509Certificates.StoreName]::Root,
[System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople),
[Parameter(Mandatory = $False)]
[string[]]$ADGroups = ""
)
begin {
$logLead = Get-LogLeadName
$Pod = [PSObject]@{
PodName = $PodName
ExportFolder = (Join-Path $RootExportFolder $PodName)
StoresToExport = $StoresToExport
CertRoot = $CertRoot
Stores = @{ }
}
if (Test-Path -Path $Pod.ExportFolder) {
Write-Host "$logLead : Removing items from $($Pod.ExportFolder) before beginning"
Remove-FileSystemItem -Path $Pod.ExportFolder -Recurse -Force -ErrorAction SilentlyContinue | Out-Null
}
if (-not (Test-Path -PathType Container -Path $Pod.ExportFolder)) {
New-Item $Pod.ExportFolder -ItemType Directory | Out-Null
}
}
process {
foreach ($storeName in $Pod.StoresToExport) {
$store = [PSObject]@{
Name = $storeName
ExportStorePath = Join-Path $Pod.ExportFolder $storeName
StorePath = Join-Path $Pod.CertRoot $storeName
Certificates = @{ }
}
# Powershell's Paths don't match the X509 enum. So here we do a poor man's lookup. If we ever need more stores than just the CA, this needs to be expanded to something more robust and clean.
if ($store.Name -eq "CertificateAuthority") {
$store.StorePath = "Cert:\LocalMachine\CA"
}
$Pod.Stores.Add($storeName, $store)
$Pod.Stores | Add-Member -MemberType NoteProperty -Name $storeName -Value $store
New-Item $store.ExportStorePath -ItemType Directory | Out-Null
#foreach cert in store
try {
$certificates = Get-ChildItem $store.StorePath;
}
catch {
Write-Warning "Cannot find Certificates in $storeName"
}
if ($certificates.Count -gt 0) {
foreach ($cert in $certificates) {
try {
$exportInfo = Export-CertificateToFileSystem -cert $cert -exportStorePath $store.ExportStorePath -ADGroups $ADGroups
if ($null -eq $exportInfo) { continue }
$chainInfo = Export-CertChain -cert $cert -exportStorePath $store.ExportStorePath -exportCertPath $exportInfo.exportCertPath -ADGroups $ADGroups
$CertificateChain = [PSObject]@{
Folder = Join-Path $exportInfo.ExportCertPath "ChainedCertificates"
Certificates = @{ }
}
foreach ($chainedCert in $chainInfo) {
$CertificateChain.Certificates.Add($chainedCert.certName, $chainedCert)
$CertificateChain.Certificates | Add-Member -MemberType NoteProperty -Name $chainedCert.certName -Value $chainedCert
}
$certificate = [PSObject]@{
Name = $exportInfo.certName
FilePath = $exportInfo.exportCertFile
Folder = $exportInfo.exportCertPath
Password = $exportInfo.certPassword
CertificateChain = $CertificateChain
ADGroups = $exportInfo.ADGroups
ExpirationDate = $exportInfo.ExpirationDate
Thumbprint = $exportInfo.Thumbprint
}
$store.Certificates.Add($certificate.Name, $certificate)
$store.Certificates | Add-Member -MemberType NoteProperty -Name $certificate.Name -Value $certificate
}
catch {
Write-Error "Certificate has failed to export
Name: $($certificate.Name), FriendlyName: $($cert.FriendlyName), Thumprint: $($cert.Thumbprint), Store: $storeName
$($_.Exception) $($_.Message) $($_.ScriptStackTrace)"
}
}
}
}
}
end {
return $Pod
}
}

View File

@ -0,0 +1,69 @@
function Get-ExpiringCertificates {
<#
.SYNOPSIS
Gets certificates that will expire soon.
.DESCRIPTION
Takes a list of machines and connects to their certificate stores, compares the expiration date
to a configureable threshold date. If the expiration date is less than the threshold date the
certificate is returned in a list.
.PARAMETER ComputerName
[string[]]One or more computers on which to get expired certificates from.
.PARAMETER ExpirationThreshold
[int] An amount of days you wish to set the threshold.
Note* Can be negative. Defaults to 30
.EXAMPLE
Get-ExpiringCertificates "Server1","Server2"
Will connect to these servers in parallel, and retrieve certificates that are due to expire within 30 days or less from now.
.EXAMPLE
Get-ExpiringCertificates "Server1","Server2" -Threshold 90
Will connect to these servers in parallel, and retrieve certificates that are due to expire within 90 days or less from now.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[Alias("Servers","Machines")]
[string[]]$ComputerName,
[Parameter(Mandatory=$false)]
[int]$ExpirationThreshold = 30
)
begin{
#Ensure there are machines to connect to
$sessions = New-PSSession $ComputerName -ErrorAction SilentlyContinue;
$Unreachable = $ComputerName | Where-Object {$sessions.ComputerName -notcontains $_}
if($Unreachable){Write-Host "Could not connect to $Unreachable";}
if(!$sessions){throw "Could not connect to any machines";}
}
process{
$ScriptBlock = {
param($ExpirationThreshold);
$personalStore = [System.Security.Cryptography.X509Certificates.StoreName]::My;
$machineStore = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine;
$certificates = [Alkami.Ops.Common.Cryptography.CertificateHelper]::GetAllCertificates($personalStore, $machineStore, $env:COMPUTERNAME);
$expirationThresholdDate = (Get-Date).AddDays($ExpirationThreshold);
#Filter certificates by threshold date
$expiredCertificates = $certificates | Where-Object {$_.notAfter -lt $expirationThresholdDate} | `
Select-Object @{N="Machine";E={$env:COMPUTERNAME}},@{N="ExpirationDate";E={$_.NotAfter}},`
@{N="DaysRemaining";E={(New-TimeSpan -start (get-date) -end $_.notAfter | Select-Object -ExpandProperty days)}},Thumbprint,FriendlyName,Subject;
if($expiredCertificates){Write-Output $expiredCertificates;}
}
#Connect to machines and execute
$expiredCertificates = Invoke-Command -Session $sessions -ScriptBlock $ScriptBlock -ArgumentList $ExpirationThreshold;
Remove-PSSession $sessions;
return $expiredCertificates;
}
}

View File

@ -0,0 +1,37 @@
function Get-PrivateKeyPermissions {
<#
.SYNOPSIS
Fetch Private Key Permissions.
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate
)
$logLead = (Get-LogLeadName);
try {
$rsaKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificate)
}
catch {
Write-Warning "$logLead : Exception Occurred While Reading Private Key Details for Certificate with Thumbprint $($certificate.Thumbprint): $($_.Exception.Message.Trim())"
return $null
}
$rsaKeyFileName = $rsaKey.Key.UniqueName
$rsaKeyPath = "${env:ALLUSERSPROFILE}\Microsoft\Crypto\RSA\MachineKeys\$rsaKeyFileName"
if (Test-Path $rsaKeyPath) {
return (Get-Acl -Path $rsaKeyPath).Access
}
else {
Write-Warning "$logLead : Unable to Find Private Key for Certificate with Thumbprint $($certificate.Thumbprint) at $rsaKeyPath"
return $null
}
}

View File

@ -0,0 +1,26 @@
function Get-SecretServerConnection {
<#
.SYNOPSIS
Exports a [SecretServerConnection] which can be used from Powershell's commandline. Largely for testing purposes.
.PARAMETER Site
The uri for the Secret Server to connect to.
.PARAMETER userName
Secret Server username.
.PARAMETER password
Secret Server password.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Site,
[Parameter(Mandatory = $true)]
[string]$UserName,
[Parameter(Mandatory = $true)]
[string]$Password
)
return [SecretServerConnection]::new([string]$Site, [string]$UserName, [string]$Password);
}

View File

@ -0,0 +1,194 @@
function Import-Certificates {
<#
.SYNOPSIS
Imports certificates onto a machine.
.PARAMETER importPassword
The password used to import the certificate with. Only used when Personal store certificates are imported.
.PARAMETER importPath
The path the certificates are imported from. If no path is defined, the current working directory is used.
.PARAMETER usersWhoNeedRights
Optional list of users to grant rights for certificates which have private keys. If not supplied, defaults to IIS_IUSRS + $defaultUsersWhoNeedRights array.
.PARAMETER securityGroup
The Security Group for each GMSA account (pod6, pod8). When not supplied, defaults to the value in the machine.config on the current server.
If not found in machine.config, throws.
.PARAMETER skipRootCerts
When this flag is supplied it will skip the importing of certificates in the 'Root' store.
.PARAMETER skipPersonalCerts
When this flag is supplied it will skip the importing of certificates in the 'My' store.
.PARAMETER skipTrustedCerts
When this flag is supplied it will skip the importing of certificates in the 'Trusted' store.
.PARAMETER skipIACerts
When this flag is supplied it will skip the importing of certificates in the 'CertificateAuthority' store.
#>
[CmdletBinding()]
Param(
[parameter(Mandatory = $false)]
[string]$importPassword,
[Parameter(Mandatory = $false)]
[string]$importPath = $PWD,
[Parameter(Mandatory = $false)]
[string[]]$usersWhoNeedRights = @("IIS_IUSRS"),
[Parameter(Mandatory = $false)]
[string]$securityGroup,
[Parameter(Mandatory = $false)]
[hashtable]$certPasswordList = @{ },
[Parameter(Mandatory = $false)]
[switch]$skipRootCerts,
[Parameter(Mandatory = $false)]
[switch]$skipPersonalCerts,
[Parameter(Mandatory = $false)]
[switch]$skipTrustedCerts,
[Parameter(Mandatory = $false)]
[switch]$skipIACerts
)
$unsignedCertFileIncludeFilter = @("*.cer", "*.crt")
# Modify this to add/remove accounts as defaults when usersWhoNeedRights is not supplied by the user
# Will be formatted as FH\<securityGroup>.<account>
$defaultUsersWhoNeedRights = @("radium$", "nag$", "dbms$", "micro$")
if (!$skipPersonalCerts.IsPresent -and !$importPassword) {
throw "Import Password cannot be null";
}
if ($skipRootCerts.IsPresent -and $skipPersonalCerts.IsPresent -and $skipTrustedCerts.IsPresent -and $skipIACerts.IsPresent) {
throw "All Skip Switches cannot be set";
}
if (!(Test-Path $importPath)) {
throw "Import Path not found";
}
if ( !($PSBoundParameters.ContainsKey('securityGroup')) ) {
Write-Host "securityGroup was not supplied by the user. Attempting to pull securityGroup from Environment.UserPrefix in machine.config."
if ($securityGroup = Get-AppSetting Environment.UserPrefix) {
Write-Host "securityGroup read from machine.config as: $securityGroup"
} else {
throw("securityGroup was not supplied by the user and could not be found in the machine config. Please specify the security group (i.e. pod6, pod8) and rerun this function.")
}
}
if ( !($PSBoundParameters.ContainsKey('usersWhoNeedRights')) ) {
Write-Host "usersWhoNeedRights was not supplied by the user. Using default accounts."
$defaultUsersWhoNeedRights = foreach ($defaultUserWhoNeedsRights in $defaultUsersWhoNeedRights) {
"fh\$securityGroup.$defaultUserWhoNeedsRights"
}
$usersWhoNeedRights += $defaultUsersWhoNeedRights
}
$rootImportPath = (Join-Path $importPath "Root");
if (!($skipRootCerts.IsPresent) -and (Test-Path $rootImportPath)) {
Write-Host "Importing Root Certs";
$certs = Get-ChildItem $rootImportPath -Recurse -include $unsignedCertFileIncludeFilter -Exclude "WMSVC*";
foreach ($cert in $certs) {
Import-Cert $cert.FullName ([System.Security.Cryptography.X509Certificates.StoreName]::Root);
}
}
$iaImportPath = (Join-Path $importPath "IA");
if (!($skipIACerts.IsPresent) -and (Test-Path $iaImportPath)) {
Write-Host "Importing IA Certs";
$certs = Get-ChildItem $iaImportPath -Recurse -include $unsignedCertFileIncludeFilter -Exclude "WMSVC*";
foreach ($cert in $certs) {
Import-Cert $cert.FullName ([System.Security.Cryptography.X509Certificates.StoreName]::CertificateAuthority);
}
}
$trustedImportPath = (Join-Path $importPath "TrustedPeople");
if (!($skipTrustedCerts.IsPresent) -and (Test-Path $trustedImportPath)) {
Write-Host "Importing Trusted Certs";
$certs = Get-ChildItem $trustedImportPath -Recurse -include $unsignedCertFileIncludeFilter -Exclude "WMSVC*";
foreach ($cert in $certs) {
Import-Cert $cert.FullName ([System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople);
}
}
$pfxImportPath = (Join-Path $importPath "Personal");
if (!($skipPersonalCerts.IsPresent) -and (Test-Path $pfxImportPath)) {
Write-Host "Importing Personal Certs";
$certs = Get-ChildItem $pfxImportPath -Recurse -include @("*.pfx", "*.cer") -Exclude "WMSVC*";
# Add any additional Service Users to $usersWhoNeedRights if they're assigned to a Windows Service
# and are running under the environment's gMSA Security Group
$ServiceUserAccounts = Get-CIMInstance Win32_Service | Where-Object { $_.StartName -match $securityGroup }
if ($ServiceUserAccounts) {
$usersWhoNeedRights += $ServiceUserAccounts.StartName
}
$usersWhoNeedRights = $usersWhoNeedRights | Sort-Object | Get-Unique
foreach ($cert in $certs) {
if ($cert.Extension -eq ".pfx") {
$currentCertPassword = $importPassword;
if ($certPasswordList.ContainsKey($cert.Name)) {
$currentCertPassword = certPasswordList.Get_Item($cert.Name);
}
Import-Cert $cert.FullName ([System.Security.Cryptography.X509Certificates.StoreName]::My) $currentCertPassword;
Confirm-Cert $cert.Name ([System.Security.Cryptography.X509Certificates.StoreName]::My);
$x509cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($cert.FullName, $currentCertPassword);
$usersWhoNeedRights | ForEach-Object {
$user = $_;
Write-Host ("Granting {0} rights to PK for certificate {1}" -f $user, $targetCert.Name);
Set-CertPermissions $x509cert.Thumbprint $user;
}
} else {
$currentCertPassword = $importPassword;
if ($certPasswordList.ContainsKey($cert.Name)) {
$currentCertPassword = certPasswordList.Get_Item($cert.Name);
}
Import-Cert $cert.FullName ([System.Security.Cryptography.X509Certificates.StoreName]::My);
Confirm-Cert $cert.Name ([System.Security.Cryptography.X509Certificates.StoreName]::My);
}
}
}
}

View File

@ -0,0 +1,130 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$global:functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
$exportPassword = "Test"
$exportPath = "c:\temp\CertificateTest"
$usersWhoNeedRights = @("testuser1", "testuser2")
Remove-FileSystemItem -Path $exportPath -Force -Recurse -ErrorAction SilentlyContinue | Out-Null
New-Item -ItemType Directory $exportPath -Force | Out-Null
InModuleScope -ModuleName Alkami.DevOps.Certificates -ScriptBlock {
Write-Host "InModuleScope - Overriding SUT: $($global:functionPath)"
Import-Module $global:functionPath -Force
$inScopeModuleForAssert = "Alkami.DevOps.Certificates"
$moduleForMock = ""
$exportPassword = "Test"
$exportPath = "c:\temp\CertificateTest"
$usersWhoNeedRights = @("testuser1", "testuser2")
Describe "Import-Certificates" {
Mock -CommandName Write-Host -ModuleName $moduleForMock -MockWith {}
Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith {}
Mock -CommandName Get-AppSetting -ModuleName $moduleForMock -MockWith { return $null }
Context "When there are bad inputs when calling Import-Certificates" {
It "Throws Exception if all skip flags set" {
{ Import-Certificates $exportPassword -skipPersonalCert -skipRootCerts -skipTrustedCert -skipIACert -securityGroup "pod1" } | Should -Throw
}
It "Throws Exception if path doesn't exist" {
{ Import-Certificates $exportPassword -importPath 'C:\BadPath' -securityGroup "pod1" } | Should -Throw
}
It "Throws Exception if securityGroup is not supplied and it is not found in machine.config" {
{ Import-Certificates $exportPassword } | Should -Throw
}
}
Context "When Inputs are correct" {
Mock -ModuleName $moduleForMock Join-Path { return "C:\temp\testpath" }
Mock -ModuleName $moduleForMock Test-Path { return $true }
Mock -ModuleName $moduleForMock -CommandName Import-Cert { }
Mock -ModuleName $moduleForMock Confirm-Cert { } -Verifiable
Mock -ModuleName $moduleForMock Get-ChildItem { return @{ FullName = "c:\temp\testpath\Test.pfx"; Name = "Test.pfx"; Extension = ".pfx"} }
Mock -ModuleName $moduleForMock Get-AlkamiServices { @{ Name = "Alkami.Radium"} }
Mock -ModuleName $moduleForMock Get-CIMInstance { @{ StartName = "podtest.user"} }
Mock -ModuleName $moduleForMock Set-CertPermissions {}
Mock -ModuleName $moduleForMock New-Object { @{ Thumbprint = "ABCDEFG"} }
It "Doesnt Require Password if not exporting Personal Certificates" {
{ Import-Certificates -skipPersonalCerts -securityGroup "pod1" } | Should -Not -Throw
}
It "Calls Import-Cert when importing personal certs" {
Import-Certificates $exportPassword -skipRootCerts -skipTrustedCerts -skipIACerts -securityGroup "pod1"
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Import-Cert -Times 1 -Exactly -Scope It
}
It "Calls Confirm-Cert when importing personal certs" {
Import-Certificates $exportPassword -skipRootCerts -skipTrustedCerts -skipIACerts -securityGroup "pod1"
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Confirm-Cert -Times 1 -Exactly -Scope It
}
It "Calls Set-CertPermissions for default users + test WMI user when usersWhoNeedRights is not supplied" {
Import-Certificates $exportPassword -skipRootCerts -skipTrustedCerts -skipIACerts -securityGroup "pod1"
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Set-CertPermissions -Times 5 -Exactly -Scope It
}
It "Calls Set-CertPermissions for supplied users + test WMI user when usersWhoNeedRights is supplied" {
Import-Certificates $exportPassword -skipRootCerts -skipTrustedCerts -skipIACerts -usersWhoNeedRights $usersWhoNeedRights -securityGroup "pod1"
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Set-CertPermissions -Times 2 -Exactly -Scope It
}
It "Calls Set-CertPermissions for default users + test WMI user when usersWhoNeedRights is not supplied and additional users found in services" {
Mock -ModuleName $moduleForMock Get-CIMInstance { @( @{StartName = "pod1.user"}, @{StartName = "podtest.user"} ) }
Import-Certificates $exportPassword -skipRootCerts -skipTrustedCerts -skipIACerts -securityGroup "pod1"
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Set-CertPermissions -Times 6 -Exactly -Scope It
}
It "Calls Set-CertPermissions for supplied users + test WMI user when usersWhoNeedRights is supplied and additional users found in services" {
Mock -ModuleName $moduleForMock Get-CIMInstance { @( @{StartName = "pod1.user"}, @{StartName = "podtest.user"} ) }
Import-Certificates $exportPassword -skipRootCerts -skipTrustedCerts -skipIACerts -usersWhoNeedRights $usersWhoNeedRights -securityGroup "pod1"
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Set-CertPermissions -Times 3 -Exactly -Scope It
}
It "Calls Import-Cert when importing root certs" {
Import-Certificates $exportPassword -skipPersonalCerts -skipTrustedCerts -skipIACerts -securityGroup "pod1"
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Import-Cert -Times 1 -Exactly -Scope It
}
It "Calls Import-Cert when importing trusted certs" {
Import-Certificates $exportPassword -skipPersonalCerts -skipRootCerts -skipIACerts -securityGroup "pod1"
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Import-Cert -Times 1 -Exactly -Scope It
}
It "Calls Import-Cert when importing IA certs" {
Import-Certificates $exportPassword -skipPersonalCerts -skipRootCerts -skipTrustedCerts -securityGroup "pod1"
Assert-MockCalled -ModuleName $inScopeModuleForAssert -CommandName Import-Cert -Times 1 -Exactly -Scope It
}
}
}
}

View File

@ -0,0 +1,98 @@
function Import-PfxCertificateWithPermissions {
<#
.SYNOPSIS
Import a PFX certificate with appropriate user permissions without needing the folder structure as specified in Import-Certificates.
.PARAMETER ImportPassword
The password used to import the certificate.
.PARAMETER PathToPfxCertificate
The path to a specified PFX file.
.PARAMETER UsersWhoNeedRights
Optional list of users to grant rights for certificates which have private keys. If not supplied, then the resulting Import-Certificates call will assign the default users
.EXAMPLE
Import-PfxCertificateWithPermissions -ImportPassword 'PASSWORD_GOES_HERE' -PathToPfxCertificate "\\10.0.16.67\c`$\temp\mccu.com.pfx"
[Import-PfxCertificateWithPermissions] : Copied cert to C:\Users\ccoane\AppData\Local\Temp\2\904bf9e6-4be5-4b8e-805b-03817b6dd198\Personal
Importing Personal Certs
Validating certificate mccu.com.pfx
Certificate mccu.com.pfx Passed Validation
Granting fh\dev.dbms$ rights to PK for certificate
Granting fh\dev.micro$ rights to PK for certificate
Granting FH\dev.micro$ rights to PK for certificate
Granting fh\dev.nag$ rights to PK for certificate
Granting fh\dev.radium$ rights to PK for certificate
Granting iis_iusrs rights to PK for certificate
[Import-PfxCertificateWithPermissions] : Removed temporary directory C:\Users\ccoane\AppData\Local\Temp\2\904bf9e6-4be5-4b8e-805b-03817b6dd198
// If you need to make a modification to the default user list used by Import-Certificates e.g. PFX also requires access granted to fh\xxxx.bank$
$SecurityGroup = Get-AppSetting -appSettingKey "Environment.UserPrefix"
Import-PfxCertificateWithPermissions -ImportPassword 'PASSWORD_GOES_HERE' -PathToPfxCertificate "\\10.0.16.67\c`$\temp\mccu.com.pfx" -UsersWhoNeedRights @("iis_iusrs", "fh\$SecurityGroup.radium$", "fh\$SecurityGroup.nag$", "fh\$SecurityGroup.dbms$", "fh\$SecurityGroup.micro$", "fh\$SecurityGroup.bank$")
[Import-PfxCertificateWithPermissions] : Copied cert to C:\Users\ccoane\AppData\Local\Temp\2\a7ad2893-884b-4604-bed2-2c2d6bd597da\Personal
Importing Personal Certs
Validating certificate mccu.com.pfx
Certificate mccu.com.pfx Passed Validation
Granting fh\dev.bank$ rights to PK for certificate
Granting fh\dev.dbms$ rights to PK for certificate
Granting FH\dev.micro$ rights to PK for certificate
Granting fh\dev.micro$ rights to PK for certificate
Granting fh\dev.nag$ rights to PK for certificate
Granting fh\dev.radium$ rights to PK for certificate
Granting iis_iusrs rights to PK for certificate
[Import-PfxCertificateWithPermissions] : Removed temporary directory C:\Users\ccoane\AppData\Local\Temp\2\a7ad2893-884b-4604-bed2-2c2d6bd597da
#>
[CmdletBinding()]
Param(
[parameter(Mandatory=$true)]
[string]$ImportPassword,
[Parameter(Mandatory=$true)]
[string]$PathToPfxCertificate,
[Parameter(Mandatory=$false)]
[string[]]$UsersWhoNeedRights
)
$logLead = (Get-LogLeadName)
# Verify Pfx is valid and exists/accessible
if (!($PathToPfxCertificate -Like "*.pfx")) {
Write-Error "$logLead : This function is expecting a certificate with a .pfx extension as the value for `$PathToPfxCertificate. Provided value is: $PathToPfxCertificate"
return
}
if (!(Test-Path $PathToPfxCertificate)) {
Write-Error "$logLead : Unable to reach the specified file from this server. Verify the path is correct and accessible to this server. Provided value is: $PathToPfxCertificate"
return
}
try {
$randomPath = Join-Path $Env:Temp $(New-Guid)
# Copy PFX to a randomly created folder in appropriate Import-Certificates folder structure
$tempFolderPersonalPath = New-Item -Path $randomPath -ItemType Directory -Name "Personal" -Force
Copy-Item -Path $PathToPfxCertificate -Destination $tempFolderPersonalPath
Write-Host "$logLead : Copied cert to $tempFolderPersonalPath"
# If user provided an argument for $UsersWhoNeedRights
if ($UsersWhoNeedRights) {
Import-Certificates -importPassword $ImportPassword -importPath $randomPath -usersWhoNeedRights $UsersWhoNeedRights
} else {
Import-Certificates -importPassword $ImportPassword -importPath $randomPath
}
} catch {
Write-Error "$logLead : $_"
} finally {
# Delete the randomly created folder if it exists
if (Test-Path -Path $randomPath) {
Remove-Item -Path $randomPath -Recurse -Force
Write-Host "$logLead : Removed temporary directory $randomPath"
}
}
return $randomPath
}

View File

@ -0,0 +1,52 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$importPasswordTest = "PASSWORD_GOES_HERE"
$pathToCertificateIsADirectory = "C:\Temp\Personal"
$pathToCertificateIsNotAPfxFile = "C:\Temp\Personal\Certificate.crt"
$pathToCertificateIsNotAccessible = "C:\Temp\Personal\CertificateThatShouldNotBeAccessibleUnlessYouMakeThisFileToSpiteMe.pfx"
$pathToCertificatePfxFile = "C:\Temp\Personal\Certificate.pfx"
$WriteErrorIncorrectPfxArgument = "This function is expecting a certificate with a .pfx extension as the value for"
$WriteErrorInaccessiblePfxFile = "Unable to reach the specified file from this server"
Describe "Import-PfxCertificateWithPermissions" {
Mock -CommandName Copy-Item -ModuleName Alkami.DevOps.Certificates -MockWith { }
Mock -CommandName Import-Certificates -ModuleName Alkami.DevOps.Certificates -MockWith { }
Mock -CommandName Write-Error -ModuleName Alkami.DevOps.Certificates -MockWith { }
Context "When there are bad inputs when calling Import-PfxCertificateWithPermissions" {
Mock -CommandName Test-Path -ModuleName Alkami.DevOps.Certificates -MockWith { return $false }
It "Writes Error if path argument has a folder path and does not point to a PFX file" {
Import-PfxCertificateWithPermissions -ImportPassword $importPasswordTest -PathToPfxCertificate $pathToCertificateIsADirectory
Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Certificates -ParameterFilter { $Message -match $WriteErrorIncorrectPfxArgument }
}
It "Writes Error if path argument has a file path but does not point to a PFX file" {
Import-PfxCertificateWithPermissions -ImportPassword $importPasswordTest -PathToPfxCertificate $pathToCertificateIsNotAPfxFile
Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Certificates -ParameterFilter { $Message -match $WriteErrorIncorrectPfxArgument }
}
It "Writes Error if path argument does not point to an accessible PFX file" {
Import-PfxCertificateWithPermissions -ImportPassword $importPasswordTest -PathToPfxCertificate $pathToCertificateIsNotAccessible
Assert-MockCalled -CommandName Write-Error -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Certificates -ParameterFilter { $Message -match $WriteErrorInaccessiblePfxFile }
}
}
Context "When Inputs are correct" {
Mock -CommandName Test-Path -ModuleName Alkami.DevOps.Certificates -MockWith { return $true }
It "Assert reaches Import-Certificates " {
Import-PfxCertificateWithPermissions -ImportPassword $importPasswordTest -PathToPfxCertificate $pathToCertificatePfxFile
Assert-MockCalled -ModuleName Alkami.DevOps.Certificates Import-Certificates -Times 1 -Exactly -Scope It
}
It "Assert randomly created folder doesn't exist after function completion " {
$folder = Import-PfxCertificateWithPermissions -ImportPassword $importPasswordTest -PathToPfxCertificate $pathToCertificatePfxFile
$folder | Should -Not -Exist
}
}
}

View File

@ -0,0 +1,184 @@
function Import-PodFromSecretServer {
<#
.SYNOPSIS
Import Pod Certificates from Secret Server.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
$PodName,
[Parameter(Mandatory = $false, ParameterSetName = "credentials")]
[string]$SecretServerUrl ="https://alkami.secretservercloud.com",
[Parameter(Mandatory = $false, ParameterSetName = "credentials")]
$SecretServerUserName,
[Parameter(Mandatory = $false, ParameterSetName = "credentials")]
$SecretServerPassword,
[Parameter(Mandatory = $false)]
[ValidateSet("Web", "App")]
$ServerType,
[Parameter(Mandatory = $false, HelpMessage = "ADGroups that need permission to these certificates")]
[string[]]$ADGroups,
[Parameter(Mandatory = $false)]
$TempDirectory = "c:\temp\importTempDir",
[Parameter(Mandatory = $false)]
[string]$CertRoot = "Cert:\LocalMachine\",
[Parameter(Mandatory = $false)]
[switch]$UsePassword,
[Parameter(Mandatory = $false)]
[ValidateSet("Production", "Staging")]
[string]$EnvironmentType
)
begin {
Import-AWSModule # SSM
New-Item -Path $TempDirectory -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
switch ($EnvironmentType) {
"Production" {$RootFolderName = "Production-CertApi"}
"Staging" {$RootFolderName = "Staging-CertApi"}
}
# Get all parmeters which weren't supplied.
if(!$EnvironmentType)
{
$EnvironmentType = Get-AppSetting Environment.Type
}
if(!$PodName)
{
$PodName = Get-AppSetting Environment.Name
}
if(!$ServerType)
{
$ServerType = Get-AppSetting Environment.Server
}
if((!$SecretServerUserName -or !$SecretServerPassword))
{
if(Test-IsAws)
{
if(!$SecretServerUserName) {
$SecretServerUserName = (Get-SSMParameter -Name "secret_server_api_username" -WithDecryption $true).Value
}
if(!$SecretServerPassword) {
$SecretServerPassword = (Get-SSMParameter -Name "secret_server_api_password" -WithDecryption $true).Value
}
}
else {
Write-Error "Secret credentials must be manually provided if this is run on a non-AWS machine. They cannot be retrieved from SSM."
}
}
$secretServer = [SecretServerConnection]::new($SecretServerUrl, $SecretServerUserName, $SecretServerPassword)
$UseTwoFactor = $false
$secretServer.Authenticate($UseTwoFactor)
$NameOfCertificateField = "pfx-file"
$NameOfPasswordField = "Import Password"
Add-Type -Assembly System.IO.Compression.FileSystem
}
process {
$startTime = Get-Date
Write-Debug "Getting folder info"
$environmentFolderId = $secretServer.GetFolderIdByName($RootFolderName)
$podFolderId = $secretServer.GetFolderIdByName($PodName, $environmentFolderId)
$subFolderId = $secretServer.GetFolderIdByName($ServerType, $podFolderId) # Should be at an App/Mic/Web folder by this point. These folder names aren't unique, hence getting the environment folder id above.
$storeFolders = $secretServer.GetChildFolders($subFolderId)
$timeElapsed = New-TimeSpan -Start $startTime -End (Get-Date)
Write-Debug "Folder info retreived - $timeElapsed"
foreach ($storeFolder in $storeFolders) {
if ($storeFolder.FolderName -eq "CertificateAuthority") {$sanitizedStore = "CA"} else {$sanitizedStore = $storeFolder.FolderName} # Powershell's Paths don't match the X509 enum. So here we do a poor man's lookup. If we ever need more stores than just the CA, this needs to be expanded to something more robust and clean.
$TempStoreDirectory = Join-Path $TempDirectory $storeFolder.FolderName
$installedCerts = (Get-Cert -StoreName $sanitizedStore).thumbprint # Get certs in the current store.
Write-Debug "Getting Secrets and downloading certificate chain"
$secretInfoList = $secretServer.GetSecretByFolderId($storeFolder.id)
$secretInfoList | Where-Object {$installedCerts -notcontains $_.name.split("-")[-1]}
foreach ($secretInfo in $secretInfoList) {
$secret = $secretServer.GetSecretById($secretInfo.id)
# Don't import expired certs
$certExpirationDate = $secret.items | Where-Object {$_.fieldName -eq "ExperationDate"}
if ((Get-Date $certExpirationDate.itemValue) -gt (Get-Date)) {
if ($installedCerts) { #Don't die if there are no certs
$secretThumbprint = $secret.items | Where-Object {$_.fieldName -eq "Thumbprint"} | Select-Object -ExpandProperty itemValue
if ($installedCerts.Contains($secretThumbprint)) {
Write-Verbose "Certificate with thumbprint $secretThumbprint already installed"
continue
}
}
if ($UsePassword) {
$TempPassword = $secret.items | Where-Object {$_.fieldName -eq $NameOfPasswordField} | Select-Object -ExpandProperty itemValue
$Password = ConvertTo-SecureString $TempPassword -AsPlainText -Force
}
$folder = Join-Path $TempStoreDirectory $secret.Name
Remove-Item $folder -Force -ea SilentlyContinue -Recurse
New-Item $folder -ItemType Directory -Force -ea SilentlyContinue | Out-Null
$zipFolder = $folder, "$($secret.Name).zip" -join "\"
if ($secretServer.DownloadFile($secret.Id, $NameOfCertificateField, $zipFolder)) {
$timeElapsed = New-TimeSpan -Start $startTime -End (Get-Date)
Write-Debug "downloaded... - $timeElapsed"
[System.IO.Compression.ZipFile]::ExtractToDirectory($zipFolder, $folder)
Write-Debug "unzipping complete - $timeElapsed"
$certificateFiles = Get-ChildItem $folder -Include "*.cer", "*.pfx" -Recurse
$certList = [System.Collections.Arraylist]::new()
foreach ($file in $certificateFiles) {
$certStore = if ($file.Directory.Name -Match $file.BaseName) {$sanitizedStore}else {$file.Directory.Name}
[void]$certList.Add(([PSObject]@{
file = $file
certStore = Join-path $CertRoot $certStore
}))
}
foreach ($cert in $certList) {
if ($cert.file.extension -eq ".cer") {
try {
Import-Certificate -FilePath $cert.file.FullName -CertStoreLocation $cert.certStore | Out-null
}
catch {
Write-Warning "Failed to import Cert with filename $($cert.file.FullName)"
Write-Warning -Message "Error: $($_.Exception.ErrorRecord)"
Write-Warning -Message "Stack Trace: $($_.Exception.StackTrace)"
}
}
else {
try {
$installedCert = Import-PfxCertificate -FilePath $cert.file.FullName -CertStoreLocation $cert.certStore -Exportable -Password $Password
foreach ($ADGroup in $ADGroups.Split("{,}")) {
Write-Host "Setting Permissions"
Set-CertPermissions -certThumprint $installedCert.Thumbprint -user $ADGroup | Out-Null
}
}
catch [System.ComponentModel.Win32Exception] {
Write-Warning "Failed to import Cert with filename $($cert.file.FullName)"
Write-Warning -Message "Error: $($_.Exception.Message)"
Write-Warning -Message "Stack Trace: $($_.Exception.StackTrace)"
}
catch {
Write-Warning "Failed to import Cert with filename $($cert.file.FullName)"
Write-Warning -Message "Error: $($_.Exception.ErrorRecord)"
Write-Warning -Message "Stack Trace: $($_.Exception.StackTrace)"
}
}
}
}
else {
Write-Warning -Message "File could not be downloaded for certificate with thumbprint $($secretThumbprint)."
}
}
}
}
}
}

View File

@ -0,0 +1,148 @@
function Publish-PodToSecretServer {
<#
.SYNOPSIS
Publish Pod's Certificates to Secret Server.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string[]]$ADGroups,
[Parameter(Mandatory = $false)]
[PSObject]$Pod,
[Parameter (Mandatory = $false)]
[string]$PodName,
[Parameter(Mandatory = $false)]
[string]$SecretServerUserName,
[Parameter(Mandatory = $false)]
[string]$SecretServerPassword,
[Parameter(Mandatory = $false)]
[string]$SecretServerUrl ="https://alkami.secretservercloud.com",
[Parameter(Mandatory = $false)]
[string]$TempFolder = "C:\Temp",
[Parameter(Mandatory = $false)]
[ValidateSet("Production", "Staging")]
[string]$EnvironmentType,
[Parameter(Mandatory = $false)]
[string]$SubFolder
)
begin {
Import-AWSModule # SSM
switch ($EnvironmentType) {
"Production" {$RootFolderName = "Production-CertApi"}
"Staging" {$RootFolderName = "Staging-CertApi"}
}
# Get all parmeters which weren't supplied.
if(!$EnvironmentType)
{
$EnvironmentType = Get-AppSetting Environment.Type
}
if(!$PodName)
{
$PodName = Get-AppSetting Environment.Name
}
if(!$SubFolder)
{
$SubFolder = Get-AppSetting Environment.Server
}
if((!$SecretServerUserName -or !$SecretServerPassword))
{
if(Test-IsAws)
{
if(!$SecretServerUserName) {
$SecretServerUserName = (Get-SSMParameter -Name "secret_server_api_username" -WithDecryption $true).Value
}
if(!$SecretServerPassword) {
$SecretServerPassword = (Get-SSMParameter -Name "secret_server_api_password" -WithDecryption $true).Value
}
}
else {
Write-Error "Secret credentials must be manually provided if this is run on a non-AWS machine. They cannot be retrieved from SSM."
}
}
if (!$Pod) {
$PodName = "$PodName-CertApi" # Tack on something to make the folder name distinct from manually created folders.
$Pod = Export-CertificatesToFileSystem -PodName $PodName -ADGroups $ADGroups
}
$secretServer = [SecretServerConnection]::new($SecretServerUrl, $SecretServerUserName, $SecretServerPassword)
$UseTwoFactor = $false
$secretServer.Authenticate($UseTwoFactor)
Add-Type -Assembly System.IO.Compression.FileSystem
}
process {
#Zip certficates and their chain
$certificatesFromAllStores = $Pod.Stores.Values.Certificates.Values
Compress-Certificates $certificatesFromAllStores $TempFolder
#Create folder for this pod
$rootFolderId = $SecretServer.GetFolderIdByName($RootFolderName)
$podFolderId = $secretServer.GetFolderIdByName($Pod.PodName, $rootFolderId)
if (!$podFolderId) {
$podFolderId = $secretServer.AddFolder($rootFolderId, $Pod.PodName)
}
$subFolderId = $secretServer.GetFolderIdByName($Subfolder, $podFolderId)
if (!$subFolderId) {
$subFolderId = $secretServer.AddFolder($podFolderId, $Subfolder)
}
foreach ($store in $Pod.Stores.Keys) {
$storeFolderId = $secretServer.GetFolderIdByName($store, $subFolderId)
if (!$storeFolderId) {
$storeFolderId = $secretServer.AddFolder($subFolderId, $store)
}
$certificates = $Pod.Stores.$store.Certificates.Values
foreach ($certificate in $certificates) {
try {
#Process
#Get a new secret template
$templateId = $secretServer.GetSecretTemplateIdByName("CertificateStore")
$secret = $SecretServer.GetSecretTemplateById($templateId, $storeFolderId)
# Configure the secret
# Each of these two line blocks creates a *reference* to a specific item in the secret.items list. That reference is then set to the correct
# certificate field value, which updates the $secret.items[x].itemValue value. Clear as mud?
$certName = $secret.items | Where-Object {$_.fieldName -eq "Certificate Name"}
$certName.itemValue = $certificate.Name
$certPassword = $secret.items | Where-Object {$_.fieldName -eq "Import Password"}
$certPassword.itemValue = $certificate.Password
$certNotes = $secret.items | Where-Object {$_.fieldName -eq "Notes"}
$certNotes.itemValue = "ADGroups with access to this certificate: $($certificate.ADGroups)"
$certThumbprint = $secret.items | Where-Object {$_.fieldName -eq "Thumbprint"}
$certThumbprint.itemValue = $certificate.Thumbprint
$certExpirationDate = $secret.items | Where-Object {$_.fieldName -eq "ExperationDate"} # Yes, Expiration is mispelled, because Thycotic.
$certExpirationDate.itemValue = $certificate.ExpirationDate.ToShortDateString()
$secretName = $certificate.Name, $certificate.thumbprint, $store.ToString()[0] -join "-"
$secretId = $secretServer.CreateSecret($secret, $storeFolderId, $secretName)
#Upload certificate zip folder
Write-Output "Publishing certificate $($certificate.Name)"
$zipFilePath = $certificate.Folder, ($certificate.Name.Trim() + ".zip") -join "\"
$secretServer.UploadFile($secretId, "pfx-File", $zipFilePath)
}
catch [InvalidOperationException] {
Write-Warning "There was a problem publishing a certificate."
Write-Warning $_
}
}
}
}
}

View File

@ -0,0 +1,26 @@
function Read-AppTierCertificates {
<#
.SYNOPSIS
Reads App Tier Certificates.
#>
[CmdletBinding()]
Param(
[string]$baseFolder,
[Hashtable[]]$certificatesTable
)
$logLead = (Get-LogLeadName);
[string[]]$usersWhoNeedRights = $null
$usersWhoNeedRights += "IIS_IUSRS"
foreach ($service in (Get-AppTierServices) | Where-Object {$_.User -notmatch "SYSTEM|REPLACEME"}) {
$usersWhoNeedRights += $service.User
}
Write-Verbose ("$logLead : Users who need rights read as {0}" -f ($usersWhoNeedRights -join ","))
Read-Certificates $baseFolder $certificatesTable $usersWhoNeedRights
}
Set-Alias -name Load-AppTierCertificates -value Read-AppTierCertificates;

View File

@ -0,0 +1,100 @@
function Read-Certificates {
<#
.SYNOPSIS
Reads Certificates.
#>
[CmdletBinding()]
Param(
[string]$baseFolder,
[Hashtable[]]$certificatesTable,
[string[]]$usersWhoNeedRights
)
$logLead = (Get-LogLeadName);
$localMachineStoreLocation = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine
$trustedPeopleStore = [System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople
$personalStore = [System.Security.Cryptography.X509Certificates.StoreName]::My
$rootStore = [System.Security.Cryptography.X509Certificates.StoreName]::Root
$folderInfo = @(
@{ Name = "Root"; Store = $rootStore; Path = (Join-Path $baseFolder "ROOT"); ImportPrivateKey = $false; },
@{ Name = "Personal"; Store = $personalStore; Path = (Join-Path $baseFolder "Personal"); ImportPrivateKey = $true; },
@{ Name = "TrustedPeople"; Store = $trustedPeopleStore; Path = (Join-Path $baseFolder "TrustedPeople"); ImportPrivateKey = $false; }
)
# Loop through each folder
foreach ($folder in $folderInfo) {
Write-Output ("$logLead : Loading Certificates from {0}" -f $folder.Path)
$certificates = $certificatesTable | Where-Object {$_.FileName.ToUpperInvariant().StartsWith($folder.Path.ToUpperInvariant())}
# Loop through each certificate found
foreach ($certificate in $certificates) {
if (!(Test-Path $certificate.FileName)) {
Write-Output ("$logLead : Could not locate certificate on disk at {0}" -f $certificate.FileName)
continue;
}
if ($certificate.FileName.EndsWith(".pfx")) {
# Create an X509Certificate2 Object
$x509Cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(
$certificate.FileName,
$certificate.Password
)
if ($folder.ImportPrivateKey) {
Write-Output ("$logLead : Loading certificate {0} (with private key) to store {1}" -f $certificate.FileName, $folder.Store.ToString())
# Load the Certificate and Private Key from File
[Alkami.Ops.Common.Cryptography.CertificateHelper]::LoadCertificateToStore(
$certificate.FileName,
$folder.Store,
$localMachineStoreLocation,
$certificate.Password
)
# Grant rights to the private key
$usersWhoNeedRights | ForEach-Object {
$serviceAccount = $_
Write-Output ("$logLead : Granting rights to private key for user {0}" -f $serviceAccount)
[Alkami.Ops.Common.Cryptography.CertificateHelper]::GrantRightsToPrivateKeys(
$x509Cert.Thumbprint,
$folder.Store,
$localMachineStoreLocation,
$serviceAccount)
}
}
else {
Write-Output ("$logLead : Loading certificate {0} (without private key) to store {1}" -f $certificate.FileName, $folder.Store.ToString())
# Load only the Certificate from File
[Alkami.Ops.Common.Cryptography.CertificateHelper]::LoadCertificateFromPFXToStore(
$certificate.FileName,
$folder.Store,
$localMachineStoreLocation
)
}
}
elseif ($certificate.FileName.EndsWith(".cer")) {
Write-Output ("$logLead : Loading certificate {0} to store {1}" -f $certificate.FileName, $folder.Store.ToString())
# Load the Certificate from File
[Alkami.Ops.Common.Cryptography.CertificateHelper]::LoadCertificateToStore(
$certificate.FileName,
$folder.Store,
$localMachineStoreLocation
)
}
else {
Write-Output ("$logLead : Filetype for {0} does not have any logic associated with it. No action taken" -f $certificate.FileName)
}
}
}
}
Set-Alias -name Load-Certificates -value Read-Certificates;

View File

@ -0,0 +1,23 @@
function Read-WebTierCertificates {
<#
.SYNOPSIS
Reads Web Tier Certificates.
#>
[CmdletBinding()]
Param(
[string]$baseFolder,
[Hashtable[]]$certificatesTable,
[string[]]$usersWhoNeedRights
)
$logLead = (Get-LogLeadName);
[string[]]$usersWhoNeedRights = $null
$usersWhoNeedRights += "IIS_IUSRS"
Write-Verbose ("$logLead : Users who need rights read as {0}" -f ($usersWhoNeedRights -join ","))
Read-Certificates $baseFolder $certificatesTable $usersWhoNeedRights
}
Set-Alias -name Load-WebTierCertificates -value Read-WebTierCertificates;

View File

@ -0,0 +1,157 @@
function Remove-Certificate {
<#
.SYNOPSIS
Deletes a specified Certificate
.DESCRIPTION
This function helps locate a specific Certificate to delete based on where it is located and by a distinguishing characteristic.
Otherwise a Certificate object can be inputted into the function to delete as well.
For the function to work as intended, one of the three must be provided...
FriendlyName [string]
Thumbprint [string]
Certificate [object]
.PARAMETER <StoreLocation>
[string] Specifying the CurrentUser or LocalComputer when navigating Certificate locations
.PARAMETER <Store>
[string] Specifying the folder in which to search for a specific Certificate
.PARAMETER <FriendlyName>
[string] A descriptory text to distinguish different Certificates. Not always provided by the Certificate
.PARAMETER <Thumbprint>
[string] A specific code that singles out a Certificate
.PARAMETER <Certificate>
[object] This is an item (certificate) that has already been selected to run this function against
.EXAMPLE
Remove-Certificate -Store TrustedPublisher -FriendlyName 'Alkami Issued Token'
.EXAMPLE
Get-Item 'Cert:\LocalMachine\TrustedPublisher\E7EAC1F158CB26C5D2B061B9085D65F7CCC4ADAC' | Remove-Certificate
#>
[CmdletBinding(DefaultParameterSetName = 'Certificate', SupportsShouldProcess = $true)]
param (
[Parameter(Mandatory, ParameterSetName = "Certificate", ValueFromPipeline)]
[object]$Certificate,
[Parameter(ParameterSetName = "Thumbprint")]
[string]$Thumbprint,
[Parameter(ParameterSetName = "FriendlyName")]
[string]$FriendlyName,
[Parameter()]
[ValidateSet('CurrentUser','LocalMachine')]
[string]$StoreLocation = 'LocalMachine',
[Parameter()]
[ArgumentCompleter({
$possibleValues = @('My','TrustedPeople','CA','UserDS','Root','TrustedPublisher', 'AddressBook','Remote Desktop')
return $possibleValues | ForEach-Object { $_ } # Runs through the foreach loop while the user is tabbing through the different fields
})][string]$Store,
[Parameter()]
[switch]$Force
)
$logLead = Get-LogLeadName
# If the certificate is populated, and is not a string or is explicitly a certificate that data is used for input
if ($null -ne $Certificate ) {
$certType = $Certificate.GetType().Name
if ($certType -eq "String") {
Write-Error "$logLead : Certificate parameter was passed as an unrecognizable or unacceptable type: [$certType]. Exiting."
throw New-Object System.ArgumentException "Certificate parameter is expected to be a certificate object."
}
if ($certType -ne "X509Certificate2") {
Write-Warning "$logLead : Certificate is of type [$certType]. This is not explicitly an X509Certificate2, but it's not a string either. Proceeding."
}
$CertificatePSPath = $Certificate.PSPath
if (![string]::IsNullOrWhiteSpace($CertificatePSPath)) {
# Splitting the path to get the $Store and $StoreLocation
$pathSplits = $Certificate.PSParentPath -split '::'
if ($pathSplits[0] -eq "Microsoft.PowerShell.Security\Certificate") {
$CertLocationAndStore = $pathSplits[1] -split "\\"
# Overwriting user input with Certificate input for Store and StoreLocation
$CertStoreLocation = $CertLocationAndStore[0]
$CertStore = $CertLocationAndStore[1]
if (![string]::IsNullOrWhiteSpace($StoreLocation)) {
if ($CertStoreLocation -ne $StoreLocation) {
Write-Warning "$logLead : The Certificate's Store Location [$CertStoreLocation] does not match the inputted Store Location [$StoreLocation]"
Write-Warning "$logLead : Using the Certificate provided Store Location [$CertStoreLocation]"
$StoreLocation = $CertStoreLocation
}
if (![string]::IsNullOrWhiteSpace($Store)) {
if ($CertStore -ne $Store) {
Write-Warning "$logLead : The Certificate's Store [$CertStore] does not match the inputted Store [$Store]"
Write-Warning "$logLead : Using the Certificate provided Store [$CertStore]"
$Store = $CertStore
}
}
}
}
}
$Thumbprint = $Certificate.Thumbprint
}
$certBasePath = (Join-Path (Join-Path "cert:\" $StoreLocation) $Store)
# Testing to see if the $Store provided is an actual directory
if (!(Test-Path $certBasePath)) {
Write-Warning "$logLead : The specified store [$Store] does not exist on the store location [$StoreLocation]"
Write-Warning "$logLead : No work to do, stopping"
return
}
$certificates = @()
if (![string]::IsNullOrWhiteSpace($Thumbprint)) {
# Finding cert based on given Thumbprint
$certificates = @(Get-ChildItem $certBasePath -Recurse | Where-Object { $_.Thumbprint -eq $Thumbprint})
if (Test-IsCollectionNullOrEmpty $certificates) {
Write-Warning "$logLead : No certificate could be found with the thumbprint [$Thumbprint] in the specified store and location [$StoreLocation\$Store]"
Write-Warning "$logLead : No work to do, stopping"
return
}
} elseif(Test-StringIsNullOrWhiteSpace -value $FriendlyName) {
$foundCertificates = @(Get-ChildItem $certBasePath -Recurse | Where-Object { $_.FriendlyName -eq $FriendlyName })
Write-Warning "$logLead : Received no thumbprint, and an empty string for FriendlyName. This has resulted in inadvertent mass certificate removal in the past."
Write-Warning "$logLead : The following is the list of certificates which would be removed, along with their thumbprints."
Write-Warning "$logLead : If you truly intended to remove these certificates, call this function with each thumbprint explicitly:"
$foundCertificates
return
} else {
# Finding cert based on given FriendlyName
$certificates = @(Get-ChildItem $certBasePath -Recurse | Where-Object { $_.FriendlyName -eq $FriendlyName })
if (Test-IsCollectionNullOrEmpty $certificates) {
Write-Warning "$logLead : No certificate could be found with the friendly name [$FriendlyName] in the specified store and location [$StoreLocation\$Store]"
Write-Warning "$logLead : No work to do, stopping"
return
}
}
if ($Force -or $PSCmdlet.ShouldProcess("Do you want to delete [$($certificates.Count)] certificate(s) with thumbprint(s) [$($certificates.Thumbprint)] in store(s) [$($certificates.PSPath)]")) {
foreach ($cert in $certificates) {
# Checking to see if the Certificate has a Private Key
if ($cert.HasPrivateKey -eq $true) {
Write-Host "$logLead : Private Key Found"
# Removal of Certification and Private Key
Write-Host "$logLead : Removed $($Cert.PSPath) [$($cert.FriendlyName)] and corresponding Private Key"
Remove-Item -Path $Cert.PSPath -DeleteKey -Force
} else {
# If there is no private key, continue to delete the Certificate
Write-Host "$logLead : Private Key Not Found"
Write-Host "$logLead : Removing $($Cert.PSPath) $($cert.FriendlyName)"
Remove-Item -Path $Cert.PSPath -Force
}
}
}
}

View File

@ -0,0 +1,91 @@
function Save-CertificatesToDisk {
<#
.SYNOPSIS
Saves Certificates to Disk.
#>
[CmdletBinding()]
Param(
[Alkami.Ops.SecretServer.Model.Certificate]$cert,
[ref]$savedCertificates,
[string]$downloadFolder
)
$logLead = (Get-LogLeadName);
$rootCertFolder = Join-Path $downloadFolder "ROOT"
$personalCertFolder = Join-Path $downloadFolder "Personal"
$trustedPeopleFolder = Join-Path $downloadFolder "TrustedPeople"
if (!([System.IO.Directory]::Exists($rootCertFolder))) {
Write-Verbose ("$logLead : Creating root cert folder {0}" -f $rootCertFolder)
New-Item $rootCertFolder -ItemType Directory -Force | Out-Null
}
if (!([System.IO.Directory]::Exists($personalCertFolder))) {
Write-Verbose ("$logLead : Creating personal cert folder {0}" -f $personalCertFolder)
New-Item $personalCertFolder -ItemType Directory -Force | Out-Null
}
if (!([System.IO.Directory]::Exists($trustedPeopleFolder))) {
Write-Verbose ("$logLead : Creating trusted people folder {0}" -f $trustedPeopleFolder)
New-Item $trustedPeopleFolder -ItemType Directory -Force | Out-Null
}
if ($cert.Name -like "*entrust*" -or $cert.Name -like "*identityguard*") {
# Entrust must go in to Trusted People and Root
Write-Verbose ("$logLead : Downloading Entrust certificate to {0}" -f $rootCertFolder)
$savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($rootCertFolder)); Password = ""; }
Write-Verbose ("$logLead : Downloading Entrust certificate to {0}" -f $trustedPeopleFolder)
$savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($trustedPeopleFolder)); Password = ""; }
}
elseif ($cert.Name -like "*root*") {
# If the certificate name contains "root" we will assume it's a root certificate
Write-Verbose ("$logLead : Downloading Root certificate to {0}" -f $rootCertFolder)
$savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($rootCertFolder)); Password = ""; }
}
elseif ($cert.FileName -match "Alkami.+(Issued|Mutual|RPSTS)") {
# Certs for Web <-> App Communication go in TrustedPeople and Personal
Write-Verbose ("$logLead : Downloading Alkami certificate {0} to {1}" -f $cert.FileName, $trustedPeopleFolder)
$savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($trustedPeopleFolder)); Password = $cert.Password; }
Write-Verbose ("$logLead : Downloading Alkami certificate {0} to {1}" -f $cert.FileName, $personalCertFolder)
$savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($personalCertFolder)); Password = $cert.Password; }
}
elseif ($cert.FileName.EndsWith(".zip")) {
# Client Certs are saved in Secret as ZIP files
# We need to unzip to Personal
Write-Verbose ("$logLead : Downloading certificate ZIP file {0} to {1}" -f $cert.FileName, $downloadFolder)
$downloadedZIP = $cert.SaveFileToDisk($downloadFolder)
$randomFolderName = [System.IO.Path]::GetRandomFileName().Split('.') | Select-Object -First 1
$unzipFolder = Join-Path $personalCertFolder $randomFolderName
if (!([System.IO.Directory]::Exists($unzipFolder))) {
Write-Verbose ("$logLead : Creating temporary unzip folder {0}" -f $unzipFolder)
New-Item $unzipFolder -ItemType Directory -Force | Out-Null
}
Write-Verbose ("$logLead : Unzipping ZIP file contents to {0}" -f $unzipFolder)
[System.IO.Compression.ZipFile]::ExtractToDirectory($downloadedZIP, $unzipFolder)
$savedCertificates.Value += @{FileName = (Get-ChildItem $unzipFolder -Recurse -Include *.PFX | Sort-Object -Property LastWriteTimeUtc -Descending | Select-Object -First 1 -ExpandProperty FullName); Password = $cert.Password; }
}
elseif ($cert.FileName -like "*trusted*") {
# If the filename contains "trusted" we will assume it's a trusted people certificate
Write-Verbose ("$logLead : Downloading certificate {0} to {1}" -f $cert.FileName, $trustedPeopleFolder)
$savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($trustedPeopleFolder)); Password = ""; }
}
elseif ($cert.FileName.EndsWith(".cer")) {
# Any other .CER files will be saved to ROOT
Write-Verbose ("$logLead : Downloading certificate {0} to {1}" -f $cert.FileName, $rootCertFolder)
$savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($rootCertFolder)); Password = ""; }
}
elseif ($cert.FileName.EndsWith(".pfx")) {
# All .PFX files will be saved to Personal
Write-Verbose ("$logLead : Downloading certificate with private key {0} to {1}" -f $cert.FileName, $personalCertFolder)
$savedCertificates.Value += @{FileName = ($cert.SaveFileToDisk($personalCertFolder)); Password = $cert.Password; }
}
else {
Write-Output ("$logLead : Unable to determine what to do with certificate {0} with SecretID {1}" -f $cert.FileName, $cert.Id)
}
}

View File

@ -0,0 +1,80 @@
function Update-CertBindings {
<#
.SYNOPSIS
Updates all sites in IIS using a certificate to a new certificate if the existing certificate's thumbprint matches the value passed in.
.PARAMETER existingCertThumbprint
The existing Cert Thumprint. This must be passed in with the spaces "10 11 14 be"
.PARAMETER replacementCertThumbprint
The replacement Cert Thumprint. This must be passed in with the spaces "10 11 14 be"
#>
[CmdletBinding()]
Param(
[parameter(Mandatory=$true)]
[ValidateNotNullorEmpty()]
[string]$existingCertThumbprint,
[parameter(Mandatory=$true)]
[ValidateNotNullorEmpty()]
[string]$replacementCertThumbprint
)
$existingCertByteArray = $existingCertThumbprint.Split(" ") | ForEach-Object { [CONVERT]::toint16($_,16)}
$existingCertThumbprint = $existingCertThumbprint -replace " "
$existingCert = Get-ChildItem -PATH "CERT:\\LocalMachine\My\$existingCertThumbprint" -Recurse
if (!$existingCert)
{
throw "Unable to find existing cert in the store with the thumbprint $existingCertThumbprint"
}
$replacementCertByteArray = $replacementCertThumbprint.Split(" ") | ForEach-Object { [CONVERT]::toint16($_,16)}
$replacementCertThumbprint = $replacementCertThumbprint -replace " "
$replacementCert = Get-ChildItem -PATH "CERT:\\LocalMachine\My\$replacementCertThumbprint" -Recurse
if (!$replacementCert)
{
throw "Unable to find replacement cert in the store with the thumbprint $replacementCertThumbprint"
}
$serverManager = New-Object Microsoft.Web.Administration.ServerManager
foreach ($site in $serverManager.sites) {
$applicableBindings = $site.Bindings | Where-Object {$null -ne $_.CertificateHash}
if ($applicableBindings.Count -eq 0)
{
Write-Host ("Site {0} does not have any existing certificate bindings." -f $site.Name)
}
else
{
foreach ($binding in $applicableBindings)
{
$hash = $binding.CertificateHash
#Write-Host ("Certificate Hash for site {0} is $hash" -f $binding.CertificateHash)
if (@(Compare-Object $hash $existingCertByteArray -sync 0).Length -eq 0)
{
Write-Host ("Updating binding for site {0}" -f $site.Name)
$existingBinding = $binding
$existingBinding.CertificateHash = $replacementCertByteArray
Save-IISServerManagerChanges $serverManager
}
elseif (@(Compare-Object $hash $replacementCertByteArray -sync 0).Length -eq 0)
{
Write-Host ("The binding for site {0} is already using the new certificate" -f $site.Name)
}
else
{
Write-Host ("The binding cert hash did not match the old or new certificate for site {0}." -f $site.Name)
}
}
}
}
}

View File

@ -0,0 +1,133 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
$exportPassword = "Test"
$exportPath = "c:\temp\CertificateTest"
Remove-Item $exportPath -Force -Recurse -ErrorAction SilentlyContinue | Out-Null
New-Item -ItemType Directory $exportPath -Force | Out-Null
Describe "Update-CertBindings" {
BeforeEach {
Mock -ModuleName $moduleForMock Get-ChildItem { return @{ PsParentPath="Microsoft.PowerShell.Security\Certificate::LocalMachine\My" }} -ParameterFilter { $Path -and $Path -eq "CERT:\\LocalMachine\My\0102030405" }
Mock -ModuleName $moduleForMock Get-ChildItem { return @{ PsParentPath="Microsoft.PowerShell.Security\Certificate::LocalMachine\My" }} -ParameterFilter { $Path -and $Path -eq "CERT:\\LocalMachine\My\1011121314" }
Mock -ModuleName $moduleForMock Get-ChildItem { return $null} -ParameterFilter { $PsParentPath -and !$PsParentPath -eq "CERT:\\0102030405" }
Mock -ModuleName $moduleForMock Save-IISServerManagerChanges {}
}
Context "When there are bad inputs when calling Update-CertBindings" {
It "Throws Exception if all skip flags set" {
{ Update-CertBindings '' "thumbprint" } | Should Throw
}
It "Throws Exception if path doesn't exist" {
{ Update-CertBindings "thumbprint" '' } | Should Throw
}
}
Context "When the inputs are valid and the certificates are missing" {
It "Throws Exception when existing cert not found" {
{ Update-CertBindings "99 99 99 99 99" '01 02 03 04 05' } | Should Throw "9999999999"
}
It "Throws Exception when replacement cert not found" {
{ Update-CertBindings "01 02 03 04 05" '99 99 99 99 99' } | Should Throw "9999999999"
}
}
Context "When the inputs are valid and the certificates exist" {
It "Updates Certificate Hash with new certificate hash when the site is valid and matches existing cert" {
Mock -ModuleName $moduleForMock New-Object {
@{
Sites = @{
Name = "Test Site"
Bindings = @{
CertificateHash = "01 02 03 04 05".Split(" ") | ForEach-Object { [CONVERT]::toint16($_,16)}
}
}
}
} -ParameterFilter { $TypeName -and $TypeName -eq "Microsoft.Web.Administration.ServerManager"}
Update-CertBindings "01 02 03 04 05" "10 11 12 13 14"
Assert-MockCalled -ModuleName $moduleForMock Save-IISServerManagerChanges -Times 1 -Exactly -Scope It
}
It "Does not Update Certificate Hash when there are no sites" {
Mock -ModuleName $moduleForMock New-Object { } -ParameterFilter { $TypeName -and $TypeName -eq "Microsoft.Web.Administration.ServerManager"}
Update-CertBindings "01 02 03 04 05" "10 11 12 13 14"
Assert-MockCalled -ModuleName $moduleForMock Save-IISServerManagerChanges -Times 0 -Exactly -Scope It
}
It "Does not Update Certificate Hash when no sites have a certificate binding" {
Mock -ModuleName $moduleForMock New-Object {
@{
Sites = @{
Name = "Test Site"
Bindings = $null
}
}
} -ParameterFilter { $TypeName -and $TypeName -eq "Microsoft.Web.Administration.ServerManager"}
Update-CertBindings "01 02 03 04 05" "10 11 12 13 14"
Assert-MockCalled -ModuleName $moduleForMock Save-IISServerManagerChanges -Times 0 -Exactly -Scope It
}
It "Does not update hash when sites hash matches new cert hash" {
Mock -ModuleName $moduleForMock New-Object {
@{
Sites = @{
Name = "Test Site"
Bindings = @{
CertificateHash = "10 11 12 13 14".Split(" ") | ForEach-Object { [CONVERT]::toint16($_,16)}
}
}
}
} -ParameterFilter { $TypeName -and $TypeName -eq "Microsoft.Web.Administration.ServerManager"}
Update-CertBindings "01 02 03 04 05" "10 11 12 13 14"
Assert-MockCalled -ModuleName $moduleForMock Save-IISServerManagerChanges -Times 0 -Exactly -Scope It
}
It "Does not update hash when sites hash does not match existing certificate" {
Mock -ModuleName $moduleForMock New-Object {
@{
Sites = @{
Name = "Test Site"
Bindings = @{
CertificateHash = "03 04 02 01".Split(" ") | ForEach-Object { [CONVERT]::toint16($_,16)}
}
}
}
} -ParameterFilter { $TypeName -and $TypeName -eq "Microsoft.Web.Administration.ServerManager"}
Update-CertBindings "01 02 03 04 05" "10 11 12 13 14"
Assert-MockCalled -ModuleName $moduleForMock Save-IISServerManagerChanges -Times 0 -Exactly -Scope It
}
}
}

View File

@ -0,0 +1,37 @@
[CmdletBinding()]
Param()
process {
$myCurrentPath = $PSScriptRoot;
Write-Verbose "Installing the Module from $myCurrentPath";
$parentPath = (Split-Path $myCurrentPath);
$systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\";
$myModulePath = (Join-Path $parentPath "module");
$metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata;
$id = $metadata.id;
$version = $metadata.version -replace '-pre.+','';
$targetModulePath = (Join-Path $systemModulePath $id);
$targetModuleVersionPath = (Join-Path $targetModulePath $version);
if (Test-Path $targetModulePath) {
## If the target folder already existed, remove it, because we are re-installing this package, obviously
if (Test-Path $targetModuleVersionPath) {
Write-Warning "Found an already existing module at [$targetModuleVersionPath]!!"
Remove-Item $targetModuleVersionPath -Recurse -Force;
}
## Clear previous children for name conflicts
(Get-ChildItem $targetModulePath) | ForEach-Object {
Write-Information "Removing module located at [$_]";
Remove-Item $_.FullName -Recurse -Force;
}
}
Write-Host "Copying module $id to [$targetModuleVersionPath]";
Copy-Item $myModulePath -Destination $targetModuleVersionPath -Recurse -Force;
}

View File

@ -0,0 +1,25 @@
[CmdletBinding()]
Param()
process {
$myCurrentPath = $PSScriptRoot;
Write-Verbose "Uninstalling the Module from $myCurrentPath";
$parentPath = (Split-Path $myCurrentPath);
$systemModulePath = "C:\Program Files\WindowsPowerShell\Modules\";
$myModulePath = (Join-Path $parentPath "module");
$metadata = ([Xml](Get-Content (Join-Path $parentPath "*.nuspec"))).package.metadata;
$id = $metadata.id;
$version = $metadata.version -replace '-pre.+','';
$targetModulePath = (Join-Path $systemModulePath $id);
$targetModuleVersionPath = (Join-Path $targetModulePath $version);
if (Test-Path $targetModuleVersionPath) {
Write-Information "Removing module at [$targetModuleVersionPath]!!"
Remove-Item $targetModuleVersionPath -Recurse -Force;
}
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-16"?>
<package>
<metadata>
<id>Alkami.DevOps.Common</id>
<version>$version$</version>
<title>Alkami Platform Modules - DevOps - Common</title>
<authors>Alkami Technologies</authors>
<owners>Alkami Technologies</owners>
<projectUrl>https://extranet.alkamitech.com/display/ORB/Alkami.DevOps.Common</projectUrl>
<iconUrl>https://www.alkami.com/files/alkamilogo75x75.png</iconUrl>
<licenseUrl>http://alkami.com/files/orblicense.html</licenseUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Installs the DevOps Common module for use with PowerShell.</description>
<releaseNotes />
<tags>PowerShell</tags>
<copyright>Copyright (c) 2018 Alkami Technologies</copyright>
<dependencies>
<dependency id="Alkami.PowerShell.Common" version="3.2.2" />
<dependency id="Alkami.PowerShell.Configuration" version="3.2.6" />
<dependency id="Alkami.Ops.SecretServer" version="3.0.2" />
</dependencies>
</metadata>
<files>
<file src="AlkamiManifest.xml" target="AlkamiManifest.xml" />
<file src="tools\ChocolateyInstall.ps1" target="tools\ChocolateyInstall.ps1" />
<file src="tools\ChocolateyUninstall.ps1" target="tools\ChocolateyUninstall.ps1" />
<file src="Resources\**" target="Resources\" />
<!-- <file src="public\*.ps1" target="module\public" /> -->
<!-- <file src="private\*.ps1" target="module\private" /> -->
<file src="*.psd1" target="module\" />
<file src="*.psm1" target="module\" />
</files>
</package>

View File

@ -0,0 +1,22 @@
@{
RootModule = 'Alkami.DevOps.Common.psm1'
ModuleVersion = '4.2.1'
GUID = '5151a169-c763-489e-8213-b437b7a294b1'
Author = 'SRE,dsage,cbrand'
CompanyName = 'Alkami Technologies, Inc.'
Copyright = '(c) 2018 Alkami Technologies, Inc.. All rights reserved.'
Description = 'A set of common functions and filters not typically used directly'
FileList = @('Resources\ObjectAsTableTemplate.html')
FormatsToProcess = @('Resources\Formatters\*.ps1xml')
RequiredModules = 'Alkami.PowerShell.Common','Alkami.PowerShell.Configuration','Alkami.Ops.SecretServer','Alkami.DevOps.Certificates'
FunctionsToExport = 'Get-AlkamiAwsProfileList','Get-AlkamiServiceFabricHostnamesByTag','Get-ArmorList','Get-AutomoxAgentPath','Get-AwsCredentialConfiguration','Get-AwsStandardDynamicParameters','Get-CatalogsFromMaster','Get-DesignationTagNameByEnvironment','Get-DynamicAwsProfilesParameter','Get-DynamicAwsRegionParameter','Get-EntrustAdminUrlFromClient','Get-Environment','Get-FileContentHash','Get-HostnamesByEnvironmentName','Get-InstanceHostname','Get-InstancesByTag','Get-IPSTSUrlFromClient','Get-LocalNlbIp','Get-LogDiskUtilization','Get-PackageForInstallation','Get-PodName','Get-SecretsForPod','Get-SecurityGroupPrefix','Get-ServerStatusReport','Get-SlackAction','Get-SlackAttachment','Get-SlackAttachmentFields','Get-SlackMessage','Get-SlackMessageColor','Get-UserCredentialsFromSecretServer','Get-UTF8ContentHash','Invoke-GetAllDesignationsByEnvironmentType','Invoke-GetAllHostsByDesignation','Invoke-GetCurrentStatusByDesignation','Invoke-GetCurrentStatusByHostnames','Invoke-GetDesignationExclusionsByEnvironmentType','Invoke-MaximizeDesignation','Invoke-MinimizeDesignation','Invoke-RemoveShutdownPendingTag','Invoke-StartDesignation','Invoke-StartEnvironment','Invoke-StartServers','Invoke-StopDesignation','Invoke-StopEnvironment','Invoke-StopServers','New-AlkamiServiceFabricClusterByTag','New-WebTierMachineConfigAppSettings','Publish-MessageToSlack','Reset-ASInstanceHealth','Select-AvailableWinRmHosts','Send-ObjectAsHtmlTable','Test-IsTeamCityProcess'
AliasesToExport = 'Create-WebTierMachineConfigAppSettings'
PrivateData = @{
PSData = @{
Tags = @('powershell', 'module', 'common')
ProjectUri = 'Https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Common+Module'
IconUri = 'https://www.alkami.com/files/alkamilogo75x75.png'
}
}
HelpInfoURI = 'https://extranet.alkamitech.com/display/SRE/Alkami.DevOps.Common+Module'
}

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{1bd8fc22-5882-4d5c-8128-81f1d61f8d77}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>MyApplication</RootNamespace>
<AssemblyName>MyApplication</AssemblyName>
<Name>Alkami.DevOps.Common</Name>
<Author>SRE</Author>
<CompanyName>Alkami Technology</CompanyName>
<Copyright>(c) 2017 Alkami Technology. All rights reserved.</Copyright>
<Description>A set of common functions and filters not typically used directly</Description>
<Guid>5151a169-c763-489e-8213-b437b7a294b1</Guid>
<Version>0.0.0.1</Version>
<PostBuildScript>Invoke-Pester;</PostBuildScript>
<PreBuildScript>..\build-project.ps1 (Join-Path $(SolutionDir) "Alkami.DevOps.Common")</PreBuildScript>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Alkami.Ops.Common\Alkami.Ops.Common.csproj">
<Name>Alkami.Ops.Common</Name>
<Project>{fa9745dd-68ac-4194-9c33-acf19411d357}</Project>
<Private>True</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="Public\Compare-StringToLocalMachineIdentifiers.ps1" />
<Compile Include="Public\Compare-StringToLocalMachineIdentifiers.tests.ps1" />
<Compile Include="Public\ConvertTo-SafeTeamCityMessage.ps1" />
<Compile Include="Public\ConvertTo-SafeTeamCityMessage.tests.ps1" />
<Compile Include="Public\Get-AlkamiServiceFabricHostnamesByTag.ps1" />
<Compile Include="Public\Get-ArmorList.ps1" />
<Compile Include="Public\Get-AutomoxAgentPath.ps1" />
<Compile Include="Public\Get-AutomoxAgentPath.tests.ps1" />
<Compile Include="Public\Get-DesignationTagNameByEnvironment.ps1" />
<Compile Include="Public\Get-DesignationTagNameByEnvironment.tests.ps1" />
<Compile Include="Public\Get-Environment.tests.ps1" />
<Compile Include="Public\Get-FileContentHash.ps1" />
<Compile Include="Public\Get-HostnamesByDesignation.ps1" />
<Compile Include="Public\Get-InstancesByTag.tests.ps1" />
<Compile Include="Public\Get-LocalNlbIp.tests.ps1" />
<Compile Include="Public\Get-UserCredentialsFromSecretServer.ps1" />
<Compile Include="Public\Get-UTF8ContentHash.ps1" />
<Compile Include="Public\Invoke-GetAllDesignationsByEnvironmentType.ps1" />
<Compile Include="Public\Invoke-GetAllHostsByDesignation.ps1" />
<Compile Include="Public\Invoke-GetCurrentStatusByDesignation.ps1" />
<Compile Include="Public\Invoke-GetCurrentStatusByHostnames.ps1" />
<Compile Include="Public\Invoke-MaximizeDesignation.ps1" />
<Compile Include="Public\Invoke-MinimizeDesignation.ps1" />
<Compile Include="Public\Invoke-StartDesignation.ps1" />
<Compile Include="Public\Invoke-StartEnvironment.ps1" />
<Compile Include="Public\Invoke-StartServers.ps1" />
<Compile Include="Public\Invoke-StopDesignation.ps1" />
<Compile Include="Public\Invoke-StopEnvironment.ps1" />
<Compile Include="Public\Invoke-StopServers.ps1" />
<Compile Include="Public\New-AlkamiServiceFabricClusterByTag.ps1" />
<Compile Include="Public\New-WebTierMachineConfigAppSettings.ps1" />
<Compile Include="Public\Get-HostnamesByPod.ps1" />
<Compile Include="Public\Get-InstancesByTag.ps1" />
<Compile Include="Public\Get-LocalNlbIp.ps1" />
<Compile Include="Public\Get-PackageForInstallation.ps1" />
<Compile Include="Public\Optimize-FilebeatsConfig.ps1" />
<Compile Include="Public\Optimize-FilebeatsConfig.tests.ps1" />
<Compile Include="Public\Publish-MessageToSlack.ps1" />
<Compile Include="Public\Publish-MessageToSlack.tests.ps1" />
<Compile Include="Public\Reset-ASInstanceHealth.ps1" />
<Compile Include="Public\Select-AvailableWinRmHosts.ps1" />
<Compile Include="Public\Test-IsStringIPAddress.ps1" />
<Compile Include="Public\Test-IsStringIPAddress.tests.ps1" />
<Compile Include="Public\Test-IsTeamCityProcess.ps1" />
<Compile Include="Public\Test-WinRmHostsAvailable.ps1" />
<Compile Include="tools\chocolateyInstall.ps1" />
<Compile Include="tools\chocolateyUninstall.ps1" />
<Compile Include="Private\VariableDeclarations.ps1" />
<Compile Include="Public\Get-CatalogsFromMaster.ps1" />
<Compile Include="Public\Get-EntrustAdminUrlFromClient.ps1" />
<Compile Include="Public\Get-Environment.ps1" />
<Compile Include="Public\Get-IPSTSUrlFromClient.ps1" />
<Compile Include="Public\Get-SecretsForPod.ps1" />
<Compile Include="Alkami.DevOps.Common.psd1" />
<Compile Include="Public\Send-ObjectAsHtmlTable.ps1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Private\" />
<Folder Include="Public\" />
<Folder Include="Resources\" />
<Folder Include="tools\" />
</ItemGroup>
<ItemGroup>
<Content Include="Alkami.DevOps.Common.nuspec" />
<Content Include="Resources\ObjectAsTableTemplate.html" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Target Name="Build" />
<Import Project="$(MSBuildExtensionsPath)\PowerShell Tools for Visual Studio\PowerShellTools.targets" Condition="Exists('$(MSBuildExtensionsPath)\PowerShell Tools for Visual Studio\PowerShellTools.targets')" />
</Project>

View File

@ -0,0 +1,12 @@
<?xml version="1.0"?>
<packageManifest>
<version>1.0</version>
<general>
<creatorCode>Alkami</creatorCode>
<element>Alkami.DevOps.Common</element>
<componentType>SREModule</componentType>
</general>
<moduleManifest>
<status>Production</status>
</moduleManifest>
</packageManifest>

View File

@ -0,0 +1,46 @@
function ConvertTo-AwsCredentialEntry {
<#
.SYNOPSIS
Used to convert AWS credentials parsed from file to a standard / friendly format
.DESCRIPTION
Used to convert AWS credentials parsed from file to a standard / friendly format
.PARAMETER CredentialData
Credential data read from file by Get-AwsCredentialConfiguration
.LINK
Get-AwsCredentialConfiguration
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[System.Collections.ArrayList]$CredentialData
)
begin {
$logLead = Get-LogLeadName
$defaultTypeName = 'AwsCredentialEntry'
$defaultKeys = @('Name')
$defaultDisplaySet = @('Name', 'role_arn', 'mfa_serial', 'region', 'source_profile')
$defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$defaultDisplaySet)
$defaultKeyPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultKeyPropertySet', [string[]]$defaultKeys)
$PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet, $defaultKeyPropertySet)
}
process {
$entryArray = @()
foreach ($entry in $CredentialData) {
try {
$entry.PSObject.TypeNames.Insert(0, $defaultTypeName)
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers -InputObject $entry
$entryArray += $entry
} catch {
Write-Warning "$logLead : Could not convert item to AwsCredentialEntry object. Exception: $($_.Exception.Message)"
}
}
return $entryArray
}
}

View File

@ -0,0 +1,32 @@
# Define Tag Magic Strings
$Global:AlkamiTagKeyEnvironment = "alk:env"
$Global:AlkamiTagKeyService = "alk:service"
$Global:AlkamiTagKeyProject = "alk:project"
$Global:AlkamiTagKeyRole = "alk:role"
$Global:AlkamiTagKeyHostName = "alk:hostname"
$Global:AlkamiTagKeyBootstrap = "alk:bootstrap-phase"
$Global:AlkamiTagKeyInstanceId = "alk:instanceid"
$Global:AlkamiTagValueEnvironmentDev = "dev"
$Global:AlkamiTagValueEnvironmentQa = "qa"
$Global:AlkamiTagValueEnvironmentSandbox = "sandbox"
$Global:AlkamiTagValueEnvironmentStaging = "staging"
$Global:AlkamiTagValueEnvironmentProd = "prod"
$Global:AlkamiTagValueEnvironmentDR = "dr"
$Global:AlkamiTagValueEnvironmentLTM = "ltm"
$Global:AlkamiTagValueEnvironmentLoadtest = "loadtest"
$Global:AlkamiDesignationEnvironmentDev = "designation"
$Global:AlkamiDesignationEnvironmentQA = "designation"
$Global:AlkamiDesignationEnvironmentSandbox = "designation"
$Global:AlkamiDesignationEnvironmentStaging = "lane"
$Global:AlkamiDesignationEnvironmentProd = "pod"
$Global:AlkamiDesignationEnvironmentDR = "pod"
$Global:AlkamiDesignationEnvironmentLTM = "designation"
$Global:AlkamiDesignationEnvironmentLoadtest = "designation"
$Global:AlkamiTagValueRoleApp = "app:app"
$Global:AlkamiTagValueRoleWeb = "web"
$Global:AlkamiTagValueRoleFab = "app:fab"
$Global:AlkamiTagValueRoleMicroservice = "app:microservice"
$Global:AlkamiTagValueRoleEntrust = "app:entrust"

View File

@ -0,0 +1,120 @@
function Get-AlkamiAwsProfileList {
<#
.SYNOPSIS
Retrieves a list of all Alkami AWS profile names.
.DESCRIPTION
Retrieves a list of all Alkami AWS profile names. Will prepend 'temp-' to the profile names if not running
through a TeamCity process; this logic can be bypassed using the '-RawOutput' argument.
.PARAMETER RawOutput
[switch] Flag indicating that the profiles should not have 'temp-' prepended to them.
.PARAMETER IncludeSubsidiaries
[switch] Flag indicating that the profile names for Alkami subsidiaries should also be included.
.EXAMPLE
Get-AlkamiAwsProfileList
temp-corp
temp-dev
temp-loadtest
temp-mgmt
temp-mp
temp-prod
temp-qa
temp-sandbox
temp-security
temp-transit
temp-transitnp
temp-workspaces
.EXAMPLE
Get-AlkamiAwsProfileList -RawOutput
Corp
Dev
Loadtest
Mgmt
Mp
Prod
Qa
Sandbox
Security
Transit
Transitnp
Workspaces
.EXAMPLE
Get-AlkamiAwsProfileList -IncludeSubsidiaries
temp-achdev
temp-corp
temp-dev
temp-loadtest
temp-mgmt
temp-mp
temp-prod
temp-qa
temp-sandbox
temp-security
temp-transit
temp-transitnp
temp-workspaces
#>
[OutputType([string[]])]
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)]
[switch] $RawOutput,
[Parameter(Mandatory = $false)]
[Alias("All")]
[switch] $IncludeSubsidiaries
)
# Define the standard list of AWS profiles for Alkami.
$awsProfileList = @(
"Corp",
"Dev",
"Loadtest",
"Mgmt",
"Mp",
"Prod",
"Qa",
"Sandbox",
"Security",
"Transit",
"Transitnp",
"Workspaces"
)
# Define the standard list of AWS profiles for Alkami subsidiaries.
$awsProfileListSubsidiaries = @(
'AchDev'
'IgniteDev',
'IgniteMerchantsProd',
'IgniteIam',
'IgniteBanksProd',
'IgniteBanksTest',
'IgniteBanksQa'
)
$result = $awsProfileList
if ( $IncludeSubsidiaries ) {
$result += $awsProfileListSubsidiaries
}
if (( $false -eq ( Test-IsTeamCityProcess ) ) -and ( $false -eq $RawOutput.IsPresent ) ) {
# Prepend 'temp-' to the profile name if we aren't on TeamCity and the user didn't specify
# that they wanted raw output.
$result = $result.ForEach( { "temp-" + $_.ToLower() } )
}
return ( $result | Sort-Object )
}

View File

@ -0,0 +1,61 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
Describe "Get-AlkamiAwsProfileList" {
Context "Result Respects TeamCity Process" {
It "Does Not Prepend 'temp-'" {
Mock -CommandName Test-IsTeamCityProcess -ModuleName Alkami.DevOps.Common -MockWith { return $true }
(Get-AlkamiAwsProfileList) | Should -Contain "Prod"
}
It "Calls Test-IsTeamCityProcess" {
Mock -CommandName Test-IsTeamCityProcess -ModuleName Alkami.DevOps.Common -MockWith { return $true }
Get-AlkamiAwsProfileList | Out-Null
Assert-MockCalled -CommandName Test-IsTeamCityProcess -Times 1 -Exactly -Scope It -ModuleName Alkami.DevOps.Common
}
}
Context "Result Respects 'RawOutput' Flag For Non-TeamCity Processes" {
It "Prepends 'temp-' By Default" {
Mock -CommandName Test-IsTeamCityProcess -ModuleName Alkami.DevOps.Common -MockWith { return $false }
( Get-AlkamiAwsProfileList ) | Should -Contain 'temp-prod'
}
It "Does Not Prepend 'temp-' When RawOutput is Specified" {
Mock -CommandName Test-IsTeamCityProcess -ModuleName Alkami.DevOps.Common -MockWith { return $false }
( Get-AlkamiAwsProfileList -RawOutput ) | Should -Contain 'Prod'
}
}
Context "Result Respects 'IncludeSubsidiaries' Flag" {
Mock -CommandName Test-IsTeamCityProcess -ModuleName Alkami.DevOps.Common -MockWith { return $false }
It "Does Not Include AchDev By Default" {
( Get-AlkamiAwsProfileList ) | Should -Not -Contain 'temp-achdev'
}
It "Includes AchDev When IncludeSubsidiaries is Specified" {
( Get-AlkamiAwsProfileList -IncludeSubsidiaries ) | Should -Contain 'temp-achdev'
}
It "Includes AchDev When All is Specified" {
( Get-AlkamiAwsProfileList -All ) | Should -Contain 'temp-achdev'
}
}
}

View File

@ -0,0 +1,105 @@
function Get-AlkamiServiceFabricHostnamesByTag {
<#
.SYNOPSIS
Returns service fabric servers in a pod that are at the minimum bootstrap phase.
Writes an error and returns $null if the requested minimum number of servers cannot be found.
.PARAMETER designation
The pod tag designation for the cluster.
.PARAMETER minservers
The minimum number of servers to wait for in the group, before creating the cluster.
.PARAMETER role
The alk:role tag of the instance to query for, to discover peer servers.
.PARAMETER minBootstrapPhase
The minimum bootstrap phase of servers that are allowed to join the cluster. 95 (at time of writing) is the ServiceFabric bootstrap phase.
.PARAMETER timeout
The amount of time (in minutes) this command will poll for valid servers to join into the cluster.
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true)]
[string]$designation,
[Parameter(Mandatory = $false)]
[int]$minServers = 5,
[Parameter(Mandatory = $false)]
[string]$role = "app:fab",
[Parameter(Mandatory = $false)]
[int]$minBootstrapPhase = 95,
[Parameter(Mandatory = $false)]
[int]$timeout = 15
)
$loglead = Get-LogLeadName;
if (($minServers -le 0) -or ($minServers -eq 2)) {
Write-Error "$loglead : Minimum number of servers in SF cluster must be 1, or 3+"
return;
}
$interval = 15; # Seconds
# $timeout is in minutes.
$numRetries = [Math]::Floor(($timeout * 60) / $interval);
$retries = 0;
$designationTagName = Get-DesignationTagNameByEnvironment
Write-Host "$logLead : Designation tag for current environment is $designationTagName"
# Query for ready-to-join peer FAB servers in this pod.;
Write-Host "$loglead : Querying for FAB servers in the pod.";
Write-Host "$logLead : Query Criteria: `n alk:$designationTagName = $designation`n $Global:AlkamiTagKeyRole = $role`n State = Running"
while ($retries -lt $numRetries) {
# Look for servers from the pod at the service-fabric-joining state of initialization.
$servers = Get-InstancesByTag -tags @{ "alk:$designationTagName" = $designation; $Global:AlkamiTagKeyRole = $role }
# Filter by servers that are ready to be joined into the cluster, or are already in the cluster.
# 95 is the userdata bootscrap script phase that joins nodes into the cluster.
Write-Host "$logLead : Looking for instances with a bootstrap phase greater than $minBootstrapPhase"
$servers = $servers | Where-Object { $null -ne ($_.Tag | Where-Object { ($_.Key -eq $Global:AlkamiTagKeyBootstrap) -and ($_.Value -ge $minBootstrapPhase) }) };
# Sort them by IP so every server that gets to this step will have the same list in the same order.
$servers = $servers | Sort-Object -Property PrivateIpAddress;
# Filter out servers that aren't running.
$servers = $servers | Where-Object { $_.State.Name -eq "Running"; }
# We need at least 3 instances to join into a cluster.
if ($servers.count -ge $minServers) {
# We found the instances we needed.
break;
}
Write-Verbose "$loglead : Found $($servers.count) ready instances. Need at least $minServers servers to join into a Service Fabric cluster. Retrying in 15s";
Start-Sleep -s $interval;
$retries++;
}
if ($retries -eq $numRetries) {
Write-Error "$loglead : Could not discover at least $minServers ready instances to join into a Service Fabric cluster.";
return $null;
}
Write-Host "$loglead : Found $minServers servers at bootstrap phase $minBootstrapPhase to create a ServiceFabric cluster with.";
# Build the fqdn's from the private IP's of the servers in the list.
$serverIps = $servers | select-object -ExpandProperty PrivateIpAddress;
$serverNames = @();
foreach ($ip in $serverIps) {
$hostname = "fab{0}.fh.local" -f $ip.Replace(".", "").Substring(2);
$serverNames += $hostname;
}
Write-Host "$loglead : Discovered Fabric Server Peers:";
$serverNames | ForEach-Object { Write-Host $_; };
return $serverNames;
}

View File

@ -0,0 +1,145 @@
function Get-ArmorList {
<#
.SYNOPSIS
Retrieve hostnames for an environment in a usable format.
.DESCRIPTION
A script that allows the user to retrieve all or some of the hosts in an environment, properly formatted for addition to an armor file, or for use in other applications.
.PARAMETER EnvironmentName
Required Parameter. The moniker associated with the environment (e.g. '12.4', 'Smith')
.PARAMETER EnvironmentType
Optional Parameter. The type of environment (e.g. 'prod', 'dr'). Defaults to the environment of the server the command is run from.
.EXAMPLE
Get-ArmorList -EnvironmentName 12.4 -EnvironmentType 'Prod'
[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod
APP16115197.fh.local,APP167765.fh.local,APP1697110.fh.local,MIC1676159.fh.local,MIC169629.fh.local,WEB16118134.fh.local,WEB1671254.fh.local,WEB1698191.fh.local
.PARAMETER Tier
Optional Parameter. Filter to a specific tier: App, Web, Mic, Fab. Defaults to include all tiers if not provided.
.EXAMPLE
Get-ArmorList -EnvironmentName 12.4 -EnvironmentType 'Prod' -Tier 'Web'
[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod
WEB16118134.fh.local,WEB1671254.fh.local,WEB1698191.fh.local
.PARAMETER Quote
Optional Parameter. Wraps each hostname in doublequotes.
.EXAMPLE
Get-ArmorList -EnvironmentName 12.4 -EnvironmentType 'Prod' -Tier 'App' -Quote
[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod
"APP16115197.fh.local","APP167765.fh.local","APP1697110.fh.local"
.PARAMETER NoDomain
Optional Parameter. Omits the domain name from the hostnames.
.EXAMPLE
Get-ArmorList -EnvironmentName 12.4 -EnvironmentType 'Prod' -Tier 'App' -NoDomain
[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod
APP16115197,APP167765,APP1697110
.PARAMETER List
Optional Parameter. Returns an array of hostnames instead of a comma-delimited string.
.EXAMPLE
Get-ArmorList -EnvironmentName 12.4 -EnvironmentType 'Prod' -Tier 'App' -List
[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod
APP16115197.fh.local
APP167765.fh.local
APP1697110.fh.local
.PARAMETER IncludeOffline
Optional Parameter. Returns both offline and online hosts.
.EXAMPLE
Get-ArmorList -EnvironmentName 12.4 -EnvironmentType 'Prod' -Tier 'App' -IncludeOffline
[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod
APP16111223.fh.local,APP16115197.fh.local,APP16122230.fh.local,APP167765.fh.local,APP1697110.fh.local
.PARAMETER ProfileName
Optional Parameter. Specify the AWS profile to use.
.PARAMETER Region
Optional Parameter. Specify the AWS region to use.
.EXAMPLE
Get-ArmorList -EnvironmentName 17 -EnvironmentType 'Prod' -Tier 'App' -ProfileName 'temp-prod' -Region 'us-west-2'
[Get-DesignationTagNameByEnvironment] : Checking designation value for environment Prod
APP3210599.fh.local,APP3272118.fh.local,APP327852.fh.local
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[Alias("Pod")]
[string]$EnvironmentName,
[Parameter(Mandatory = $false)]
[string]$EnvironmentType,
[Parameter(Mandatory = $false)]
[ValidateSet("web", "app", "mic", "fab")]
[string]$Tier,
[Parameter(Mandatory = $false)]
[Switch]$Quote,
[Parameter(Mandatory = $false)]
[Switch]$NoDomain,
[Parameter(Mandatory = $false)]
[Switch]$List,
[Parameter(Mandatory = $false)]
[Switch]$IncludeOffline,
[Parameter(Mandatory = $false)]
[string]$ProfileName = $null,
[Parameter(Mandatory = $false)]
[string]$Region = $null
)
[string[]] $servers = Get-HostnamesByEnvironmentName -EnvironmentName $EnvironmentName -EnvironmentType $EnvironmentType `
-ProfileName $ProfileName -Region $Region -IncludeOffline:$IncludeOffline
if ( ! $NoDomain ) {
$servers = foreach ( $server in $servers ) {
"$server.fh.local"
}
}
if ( $PSBoundParameters.ContainsKey('Tier') ) {
[string[]] $servers = Get-ServerByType -Server $servers -Type $Tier
}
if ( $Quote ) {
$servers = foreach ( $server in $servers ) {
"`"$server`""
}
}
if ( ! $List ) {
$servers = $servers -join ','
}
return $servers
}

View File

@ -0,0 +1,137 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
Describe "Get-ArmorList" {
Context "Parameter Validation" {
It "Null Environment Name Should Throw" {
{ Get-ArmorList -EnvironmentName $null } | Should Throw
}
It "Empty Environment Name Should Throw" {
{ Get-ArmorList -EnvironmentName '' } | Should Throw
}
It "Invalid Tier Should Throw" {
{ Get-ArmorList -EnvironmentName 'Test' -Tier 'Test' } | Should Throw
}
}
Context "Result Respects NoDomain Parameter" {
Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A', 'B' ) } -ModuleName $moduleForMock
It "Appends Domain By Default" {
( Get-ArmorList -EnvironmentName 'Test' ) | Should -BeExactly 'A.fh.local,B.fh.local'
}
It "Does Not Append Domain When Switch Is Present" {
( Get-ArmorList -EnvironmentName 'Test' -NoDomain ) | Should -BeExactly 'A,B'
}
}
Context "Result Respects List Parameter" {
It "Returns a Comma-Delimited String By Default" {
Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A', 'B' ) } -ModuleName $moduleForMock
( Get-ArmorList -EnvironmentName 'Test' -NoDomain ) | Should -BeExactly 'A,B'
}
It "Returns a String Type By Default" {
Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A', 'B' ) } -ModuleName $moduleForMock
( Get-ArmorList -EnvironmentName 'Test' -NoDomain ) -is [string] | Should -BeTrue
}
It "Returns An Array When Switch Is Present" {
Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A', 'B' ) } -ModuleName $moduleForMock
( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List ) | Should -BeExactly @( 'A', 'B' )
}
It "Returns An Array Type When Switch Is Present" {
Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A', 'B' ) } -ModuleName $moduleForMock
( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List ) -is [array] | Should -BeTrue
}
It "Returns a String Type By Default When There Is A Single Hostname" {
Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A' ) } -ModuleName $moduleForMock
( Get-ArmorList -EnvironmentName 'Test' -NoDomain ) -is [string] | Should -BeTrue
}
It "Returns An Array Type When Switch Is Present When There Is A Single Hostname" {
Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A' ) } -ModuleName $moduleForMock
# This test feels worthless, but Powershell is coercing a single element array to a string on return.
# I verified through debug prints that the variable is of type [string[]] all the way up to the return
# statement. Have fun reading https://superuser.com/a/414666
[string[]] $result = Get-ArmorList -EnvironmentName 'Test' -NoDomain -List
$result -is [array] | Should -BeTrue
}
}
Context "Result Respects Quote Parameter" {
Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'A', 'B' ) } -ModuleName $moduleForMock
It "Returns Unquoted Values By Default" {
( Get-ArmorList -EnvironmentName 'Test' -NoDomain ) | Should -BeExactly 'A,B'
}
It "Returns Quoted Values When Switch Is Present" {
( Get-ArmorList -EnvironmentName 'Test' -NoDomain -Quote ) | Should -BeExactly '"A","B"'
}
}
Context "Result Respects Tier Parameter" {
Mock -CommandName Get-HostnamesByEnvironmentName -MockWith { return @( 'WEB123', 'APP123', 'MIC123', 'FAB123' ) } -ModuleName $moduleForMock
It "Returns All Tiers By Default" {
( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List ) | Should -BeExactly @( 'WEB123', 'APP123', 'MIC123', 'FAB123' )
}
It "Returns Only Web Servers When Web Tier Filtering" {
( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List -Tier 'Web') | Should -BeExactly @( 'WEB123' )
}
It "Returns Only App Servers When App Tier Filtering" {
( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List -Tier 'App') | Should -BeExactly @( 'APP123' )
}
It "Returns Only Mic Servers When Mic Tier Filtering" {
( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List -Tier 'Mic') | Should -BeExactly @( 'MIC123' )
}
It "Returns Only Fab Servers When Fab Tier Filtering" {
( Get-ArmorList -EnvironmentName 'Test' -NoDomain -List -Tier 'Fab') | Should -BeExactly @( 'FAB123' )
}
}
}

View File

@ -0,0 +1,25 @@
function Get-AutomoxAgentPath {
<#
.SYNOPSIS
Returns the full path and filename of the Automox Agent executable
.DESCRIPTION
Searches Program Files and Program Files x86 for amagent.exe. Returns the full path and filename
#>
[CmdletBinding()]
param()
$logLead = (Get-LogLeadName)
$automoxAgentDirectory = Get-ChildItem -File -Recurse -Depth 2 -Path @(${env:ProgramFiles(x86)}, $ENV:ProgramFiles) -Filter amagent.exe | Select-Object -First 1
if ($null -eq $automoxAgentDirectory) {
Write-Warning "$logLead : Unable to Locate the Automox Agent Executable"
return $null
}
return $automoxAgentDirectory.FullName
}

View File

@ -0,0 +1,25 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
Describe "Get-AutomoxAgentPath" {
Context "Validation" {
It "Writes a Warning if No Executable Found" {
Mock -ModuleName $moduleForMock Get-ChildItem { return $null }
( Get-AutomoxAgentPath 3>&1 ) -match "Unable to Locate the Automox Agent Executable" | Should -Be $true
}
It "Returns the FullName Property of the File When Found" {
Mock -ModuleName $moduleForMock Get-ChildItem { return New-Object PSObject -Property @{ Name="Foo"; FullName="C:\Temp\FooBar\amagent.exe"; } }
Get-AutomoxAgentPath | Should -Be "C:\Temp\FooBar\amagent.exe"
}
}
}

View File

@ -0,0 +1,144 @@
function Get-AwsCredentialConfiguration {
<#
.SYNOPSIS
Used to read/parse the AWS credentials file entries to a queryable object array
.DESCRIPTION
Used to read/parse the AWS credentials file entries to a queryable object array. Runs parsed values through ConvertTo-AwsCredentialEntry
.EXAMPLE
Get-AwsCredentialConfiguration
Name : GrandPooBah
role_arn : arn:aws:iam::123430414321:role/CLI-God-Mode-On
mfa_serial : arn:aws:iam::123410044321:mfa/poobah-cli
region : canada-west-99
source_profile : default
Name : Gibberish
role_arn : arn:aws:iam::123495924321:role/CLI-NoGood-DoNothing
mfa_serial : arn:aws:iam::123410044321:mfa/gwhiting-cli
region : mexico-north-1
source_profile : default
.LINK
ConvertTo-AwsCredentialEntry
#>
[CmdletBinding()]
[OutputType([object[]])]
param()
$logLead = Get-LogLeadName
$userProfileDirectory = Get-EnvironmentVariable -StoreName Process -Name USERPROFILE
$configurationSources = @()
$configurationSources += Join-Path -Path $userProfileDirectory -ChildPath ".aws/config"
$configurationSources += Join-Path -Path $userProfileDirectory -ChildPath ".aws/credentials"
# ToDo - Add the AWS SDK Credentials Source
[System.Collections.ArrayList]$objects = @()
$awsProfile = @{}
$lines = @()
$propertiesToIgnore = @("aws_secret_access_key", "aws_session_token")
foreach ($source in $configurationSources) {
if (-NOT (Test-Path -Path $source)) {
Write-Verbose "$logLead : Credential file does not exist at [$source]"
continue
}
$fileContent = Get-Content -Path $source
if (Test-IsCollectionNullOrEmpty -Collection $fileContent) {
Write-Verbose "$logLead : Credential file at [$source] exists but is empty. Skipping."
continue
}
$lines += $fileContent
$lines += "EOF"
}
if (Test-IsCollectionNullOrEmpty -Collection $lines) {
Write-Warning "$logLead : Unable to locate any configured AWS credentials sources. Have you set up your local profiles?"
return
}
foreach ($line in $lines) {
if ($line.StartsWith('#') -or (Test-StringIsNullOrWhitespace -Value $line)) {
# This is a comment or empty line, ignore it
continue
} elseif ($line -eq "EOF" -and (-NOT (Test-StringIsNullOrWhitespace -Value $awsProfile.Name))) {
# Save the final profile or in between file swaps
$objects += $awsProfile
} elseif ($line.StartsWith('[')) {
# This is a new profile, push any old profile into the objects array and start fresh
if ($null -ne $awsProfile.Name -and ($null -eq ($objects | Where-Object { $_.Name -eq $awsProfile.Name }))) {
# $awsProfile object is populated, save it
$objects += $awsProfile
}
$sanitizedProfileName = $line -replace "\[|\]|(profile\s+)", ""
Write-Verbose "$logLead : Looking for existing parsed profiles named [$sanitizedProfileName]"
$existingProfile = $objects | Where-Object { $_.Name -eq $sanitizedProfileName }
if ($null -ne $existingProfile) {
# Load the existing profile to attach properties, where unique
Write-Verbose "$logLead : Duplicate profile name [$sanitizedProfileName] found. First-in properties win in this scenario."
$awsProfile = $existingProfile
$objects.Remove($existingProfile)
} else {
# This is a new profile
$awsProfile = New-Object PSObject -Property @{ Name = $sanitizedProfileName }
}
} else {
if ($null -eq $awsProfile.Name) {
# We skip this step unless we're anchored to a profile
# to avoid picking up trash or duplicate properties
continue
}
# The only things left are valid properties to attach to an object
# Unless we're specifically ignoring them
$splits = $line -split '='
$propertyName = $splits[0].Trim()
if ($propertiesToIgnore -contains $propertyName) {
# This is a sensitive property that will never be recorded
Write-Verbose "$logLead : Skipping excluded property [$propertyName] for profile [$($awsProfile.Name)]"
continue
} elseif ($null -ne $awsProfile.$propertyName) {
# This property has been recorded already. It is a duplicate, and first read wins
Write-Verbose ("$logLead : Skipping duplicate property [$propertyName] for profile [$($awsProfile.Name)] - existing value: [$($awsProfile.$propertyName)]")
continue
}
$propertyValue = $splits[1].Trim()
Add-Member -InputObject $awsProfile -NotePropertyName $propertyName -NotePropertyValue $propertyValue -Force
}
}
return ($objects | ConvertTo-AwsCredentialEntry | Sort-Object -Property Name)
}

View File

@ -0,0 +1,114 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
Describe "Get-AwsCredentialConfiguration" {
# This is a private function, so Pester has a fit
function ConvertTo-AwsCredentialEntry { param([System.Collections.ArrayList]$CredentialData) }
Context "Logic" {
It "Reads from Both Text-Based Credentials Files" {
$expectedFileReads = @("credentials", "config")
Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith {}
Get-AwsCredentialConfiguration | Should -BeNullOrEmpty
foreach ($fileRead in $expectedFileReads) {
Assert-MockCalled -CommandName Get-Content -Scope It -Times 1 -Exactly -ParameterFilter { $Path -match "$fileRead$"}
}
}
It "Writes a Warning and Exits Early if No Credentials Files Found" {
Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith {}
Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { }
Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith { }
Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $false }
Get-AwsCredentialConfiguration -Verbose
Assert-MockCalled -CommandName Write-Warning -Scope It -Times 1 -Exactly -ParameterFilter { $Message -match "Unable to locate any configured AWS credentials sources" }
}
It "Writes a Warning and Exits Early if Credentials Files Found but Empty" {
Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith { return @() }
Mock -CommandName Write-Warning -ModuleName $moduleForMock -MockWith { }
Mock -CommandName Write-Verbose -ModuleName $moduleForMock -MockWith { }
Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $true }
Get-AwsCredentialConfiguration -Verbose
Assert-MockCalled -CommandName Write-Warning -Scope It -Times 1 -Exactly -ParameterFilter { $Message -match "Unable to locate any configured AWS credentials sources" }
}
It "Parses the File Contents Appropriately" {
Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith { return @( "[ErMerGerd]","role_arn=some_value","source_profile=another_value","region=some_region","mfa_serial=12345") } `
-ParameterFilter { $Path -Match "credentials" }
Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith { return @( "[AllInAll]","role_arn=Its","source_profile=Just","region=Another","mfa_serial=BrickInTheWall") } `
-ParameterFilter { $Path -Match "config" }
Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $true }
Mock -CommandName ConvertTo-AwsCredentialEntry -ModuleName $moduleForMock -MockWith { return $_ }
$result = Get-AwsCredentialConfiguration
$result | Should -HaveCount 2
$firstResult = $result | Select -First 1
$lastResult = $result | Select -Last 1
$firstResult.Name | Should -BeExactly "AllInAll"
$firstResult.role_arn | Should -BeExactly "Its"
$firstResult.source_profile | Should -BeExactly "Just"
$firstResult.region | Should -BeExactly "Another"
$firstResult.mfa_serial | Should -BeExactly "BrickInTheWall"
$lastResult.Name | Should -BeExactly "ErMerGerd"
$lastResult.role_arn | Should -BeExactly "some_value"
$lastResult.source_profile | Should -BeExactly "another_value"
$lastResult.region | Should -BeExactly "some_region"
$lastResult.mfa_serial | Should -BeExactly "12345"
}
It "Merges Profile Properties Preferring First In When Overlap Occurs" {
Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith { return @( "[Shmoo]","role_arn=first_value","source_profile=first_value") } `
-ParameterFilter { $Path -Match "config" }
Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith { return @( "[Shmoo]","role_arn=second_value","region=a_value_not_in_object_one" ) } `
-ParameterFilter { $Path -Match "credentials" }
Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $true }
Mock -CommandName ConvertTo-AwsCredentialEntry -ModuleName $moduleForMock -MockWith { return $_ }
$result = Get-AwsCredentialConfiguration
$result | Should -HaveCount 1
$result.role_arn | Should -Be "first_value"
$result.source_profile | Should -BeExactly "first_value"
$result.region | Should -BeExactly "a_value_not_in_object_one"
$result.mfa_serial | Should -BeNullOrEmpty
}
It "Refuses to Return Properties Defined as Sensitive" {
Mock -CommandName Test-Path -ModuleName $moduleForMock -MockWith { return $true }
Mock -CommandName ConvertTo-AwsCredentialEntry -ModuleName $moduleForMock -MockWith { return $_ }
$propertiesToIgnore = @("aws_secret_access_key", "aws_session_token")
foreach ($property in $propertiesToIgnore) {
Mock -CommandName Get-Content -ModuleName $moduleForMock -MockWith { return @( "[OhSay]","role_arn=Can","source_profile=You","$property=See") } `
-ParameterFilter { $Path -Match "config" }
$result = Get-AwsCredentialConfiguration
$result[0].$property | Should -BeNullOrEmpty
}
}
}
}

View File

@ -0,0 +1,91 @@
function Get-AwsStandardDynamicParameters {
<#
.SYNOPSIS
Returns a RuntimeDefinedParameterDictionary with all locally configured AWS Profiles and Alkami-supported Regions
.DESCRIPTION
Returns a RuntimeDefinedParameterDictionary with all locally configured AWS Profiles and Alkami-supported Regions as DynamicParameters in functions.
.PARAMETER RegionParameterName
The parameter name to set on the Region parameter. Defaults to Region
.PARAMETER RegionParameterSetName
The parameter set to associate the dynamic Region parameter with. Defaults to all parameter sets.
.PARAMETER RegionParameterRequired
Whether or not the Region parameter is mandatory. Defaults to false.
.PARAMETER ProfileParameterName
The parameter name to set on the Profile Name parameter. Defaults to ProfileName
.PARAMETER ProfileParameterSetName
The parameter set to associate the dynamic ProfileName parameter with. Defaults to all parameter sets.
.PARAMETER ProfileParameterRequired
Whether or not the ProfileName parameter is mandatory. Defaults to false.
.EXAMPLE
Get-AwsStandardDynamicParameters
Key Value
--- -----
Region System.Management.Automation.RuntimeDefinedParameter
Profile System.Management.Automation.RuntimeDefinedParameter
.LINK
Get-DynamicAwsProfilesParameter
.LINK
Get-DynamicAwsRegionParameter
#>
[Cmdletbinding()]
[OutputType([System.Management.Automation.RuntimeDefinedParameterDictionary])]
param(
[Parameter(Mandatory=$false)]
[string]$RegionParameterName = "Region",
[Parameter(Mandatory=$false)]
[string]$RegionParameterSetName = "__AllParameterSets",
[Parameter(Mandatory=$false)]
[switch]$RegionParameterRequired,
[Parameter(Mandatory=$false)]
[string]$ProfileParameterName = "ProfileName",
[Parameter(Mandatory=$false)]
[string]$ProfileParameterSetName = "__AllParameterSets",
[Parameter(Mandatory=$false)]
[switch]$ProfileParameterRequired
)
$regionDynamicParams = @{
"DynamicParameterName" = $RegionParameterName;
"ParameterSetName" = $RegionParameterSetName;
"IsMandatoryParameter" = $RegionParameterRequired
}
$regionRuntimeParameter = Get-DynamicAwsRegionParameter @regionDynamicParams
$profileDynamicParams = @{
"DynamicParameterName" = $ProfileParameterName;
"ParameterSetName" = $ProfileParameterSetName;
"IsMandatoryParameter" = $ProfileParameterRequired
}
$profileRuntimeParameter = Get-DynamicAwsProfilesParameter @profileDynamicParams
# The Dynamic params functions return dictionaries so they can be used independently if needed
# We will add both returned values to a new Dictionary and return it for use by the calling function
$runtimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$runtimeParameterDictionary.Add($RegionParameterName, $($regionRuntimeParameter.Values[0]))
$runtimeParameterDictionary.Add($ProfileParameterName, $($profileRuntimeParameter.Values[0]))
return $runtimeParameterDictionary
}

View File

@ -0,0 +1,79 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
Describe "Get-AwsStandardDynamicParameters" {
Context "Logic" {
Mock -CommandName Get-AwsCredentialConfiguration -ModuleName $moduleForMock -MockWith { return @(
(New-Object PSObject -Property @{ "Name"="FogoDeChao"; }),
(New-Object PSObject -Property @{ "Name"="12Cuts"; })
)}
Mock -CommandName Get-SupportedAwsRegions -ModuleName $moduleForMock -MockWith { return @(
"mexico-south-55",
"australia-west-99"
)}
It "Profile Uses the Specified Parameter Name" {
$testParamName = "OnlyYouCanMakeAllThisWorldSeemRight"
$params = Get-AwsStandardDynamicParameters -ProfileParameterName $testParamName
$profileParam = $params.Item($testParamName)
$profileParam | Should -Not -BeNullOrEmpty
$profileParam.Attributes.HelpMessage -like "*Profile*" | Should -BeTrue
}
It "Region Uses the Specified Parameter Name" {
$testParamName = "OnlyYouCanMakeTheDarknessBright"
$params = Get-AwsStandardDynamicParameters -RegionParameterName $testParamName
$regionParam = $params.Item($testParamName)
$regionParam | Should -Not -BeNullOrEmpty
$regionParam.Attributes.HelpMessage -like "*Region*" | Should -BeTrue
}
It "Profile Uses the Specified ParameterSet Name" {
$testParamSetName = "OnlyYouAndYouAloneCanThrillMeLikeYouDo"
$params = Get-AwsStandardDynamicParameters -ProfileParameterSetName $testParamSetName
$profileParam = $params.Item("ProfileName")
$profileParam | Should -Not -BeNullOrEmpty
$profileParam.Attributes.ParameterSetName | Should -BeExactly $testParamSetName
}
It "Region Uses the Specified ParameterSet Name" {
$testParamSetName = "AndFillMyHeartWithLoveForOnlyYou"
$params = Get-AwsStandardDynamicParameters -RegionParameterSetName $testParamSetName
$regionParam = $params.Item("Region")
$regionParam | Should -Not -BeNullOrEmpty
$regionParam.Attributes.ParameterSetName | Should -BeExactly $testParamSetName
}
It "Profile Is a Mandatory Parameter if Specified" {
$params = Get-AwsStandardDynamicParameters -ProfileParameterRequired
$profileParam = $params.Item("ProfileName")
$profileParam | Should -Not -BeNullOrEmpty
$profileParam.Attributes.Mandatory | Should -BeTrue
}
It "Region Is a Mandatory Parameter if Specified" {
$params = Get-AwsStandardDynamicParameters -RegionParameterRequired
$regionParam = $params.Item("Region")
$regionParam | Should -Not -BeNullOrEmpty
$regionParam.Attributes.Mandatory | Should -BeTrue
}
}
}

View File

@ -0,0 +1,58 @@
function Get-CatalogsFromMaster {
<#
.SYNOPSIS
Returns a collection of tenants from the master database as a hashtable
#>
[CmdletBinding()]
param(
[Parameter(Position = 0, Mandatory = $false)]
[string]$ConnectionString
)
if ([String]::IsNullOrEmpty($ConnectionString)) {
$masterConnectionString = Get-MasterConnectionString
}
else {
$masterConnectionString = $ConnectionString
}
$conn = New-Object System.Data.SqlClient.SqlConnection
$conStrBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder($masterConnectionString)
$conn.ConnectionString = $conStrBuilder.ToString()
[hashtable[]]$databases = @()
try {
$conn.Open()
$query = New-Object System.Data.SqlClient.SqlCommand("SELECT Name, BankUrlSignatures, BankAdminUrlSignatures, DataSource, Catalog FROM dbo.Tenant", $conn)
$results = $query.ExecuteReader()
if (!$results.HasRows) {
Write-Warning (" No rows were returned from the tenant table.`nServer: {0}`nDatabase: {1}" -f $conStrBuilder.DataSource, $conStrBuilder.InitialCatalog)
return $null
}
while ($results.Read()) {
$tenantConStringBuilder = $conStrBuilder
$tenantConStringBuilder.'Data Source' = $results.Item(3)
$tenantConStringBuilder.'Initial Catalog' = $results.Item(4)
$databases += @{Name = $results.Item(0); Signature = $results.Item(1); AdminSignature = $results.Item(2); DataSource = $results.Item(3); Catalog = $results.Item(4); ConnectionString = $tenantConStringBuilder.ToString()}
}
return $databases
}
catch {
Write-Warning "An exception occurred while trying to pull tenants from the master database"
Write-Warning $_ | Format-List -Force
return $null
}
finally {
if ($conn.State -ne [System.Data.ConnectionState]::Closed) {
$conn.Close()
}
$conn = $null
}
}

View File

@ -0,0 +1,91 @@
function Get-DesignationTagNameByEnvironment {
<#
.SYNOPSIS
Looks up the appropriate designation string for a given environment
.DESCRIPTION
Looks up the appropriate designation string such as Pod, Lane, Box, or Designation for a given environment
Will use the current server's environment for lookup if the machine is an EC2 and no environment parameter has
been provided
.PARAMETER EnvironmentType
[String] An optional parameter to look up the string for a particular environment type. When supplied will override
the current instance value if the instance is in AWS. Is required for execution on non-EC2 machines
.EXAMPLE
Get-DesignationTagNameByEnvironment
[Get-DesignationTagNameByEnvironment] : Checking designation value for environment prod
pod
.EXAMPLE
Get-DesignationTagNameByEnvironment "prodshared"
[Get-DesignationTagNameByEnvironment] : Checking designation value for environment prodshared
pod
#>
[CmdletBinding()]
param(
[ValidateSet("dev", "qa", "staging", "prod", "dr", "sandbox", "devshared", "qashared", "stagingshared", "prodshared", "drshared", "sandboxshared","ltm","ltmshared")]
[Parameter(Mandatory = $false)]
[string]$environmentType
)
$logLead = (Get-LogLeadName)
$checkCurrentInstanceTags = Test-IsAws
$parameterProvided = !([String]::IsNullOrEmpty($environmentType))
if (!$checkCurrentInstanceTags -and !$parameterProvided) {
Write-Warning "$logLead : Current server is not in AWS and no environment type name is provided. Execution cannot continue"
return $null
}
if ($checkCurrentInstanceTags -and !$parameterProvided) {
Write-Verbose "$logLead : Current host is in AWS and no Environment Type provided. Getting the current instance's $Global:AlkamiTagKeyEnvironment tag"
$environmentTagValue = Get-CurrentInstanceTags $Global:AlkamiTagKeyEnvironment -ValueOnly -ErrorAction Continue
if ([String]::IsNullOrEmpty($environmentTagValue)) {
Write-Warning "$logLead : Current server is not configured with the $Global:AlkamiTagKeyEnvironment tag or it could not be retrieved. Execution cannot continue"
return $null
}
Write-Verbose "$logLead : Using tag value $environmentTagValue for lookup"
}
else {
Write-Verbose "$logLead : Using parameter value $environmentType for lookup"
$environmentTagValue = $environmentType
}
Write-Host "$logLead : Checking designation value for environment $environmentTagValue"
$lookupValue = switch ($environmentTagValue) {
"$Global:AlkamiTagValueEnvironmentProd" { $Global:AlkamiDesignationEnvironmentProd }
("$Global:AlkamiTagValueEnvironmentProd" + "shared") { $Global:AlkamiDesignationEnvironmentProd }
"$Global:AlkamiTagValueEnvironmentDR" { $Global:AlkamiDesignationEnvironmentDR }
("$Global:AlkamiTagValueEnvironmentDR" + "shared") { $Global:AlkamiDesignationEnvironmentDR }
"$Global:AlkamiTagValueEnvironmentStaging" { $Global:AlkamiDesignationEnvironmentStaging }
("$Global:AlkamiTagValueEnvironmentStaging" + "shared") { $Global:AlkamiDesignationEnvironmentStaging }
"$Global:AlkamiTagValueEnvironmentSandbox" { $Global:AlkamiDesignationEnvironmentSandbox }
("$Global:AlkamiTagValueEnvironmentSandbox" + "shared") { $Global:AlkamiDesignationEnvironmentSandbox }
"$Global:AlkamiTagValueEnvironmentDev" { $Global:AlkamiDesignationEnvironmentDev }
("$Global:AlkamiTagValueEnvironmentDev" + "shared") { $Global:AlkamiDesignationEnvironmentDev }
"$Global:AlkamiTagValueEnvironmentQa" { $Global:AlkamiDesignationEnvironmentQA }
("$Global:AlkamiTagValueEnvironmentQa" + "shared") { $Global:AlkamiDesignationEnvironmentQA }
"$Global:AlkamiTagValueEnvironmentLTM" { $Global:AlkamiDesignationEnvironmentLTM }
("$Global:AlkamiTagValueEnvironmentLTM" + "shared") { $Global:AlkamiDesignationEnvironmentLTM }
"$Global:AlkamiTagValueEnvironmentLoadtest" { $Global:AlkamiDesignationEnvironmentLoadtest }
("$Global:AlkamiTagValueEnvironmentLoadtest" + "shared") { $Global:AlkamiDesignationEnvironmentLoadtest }
default { "Unknown" }
}
return $lookupValue
}

View File

@ -0,0 +1,126 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
Describe "Get-DesignationTagNameByEnvironment" {
Context "Error and Parameter Handling" {
Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock
It "Does Not Execute if No Parameter Provided and Not in AWS" {
Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $false }
Get-DesignationTagNameByEnvironment | Should -BeNullOrEmpty
Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It `
-ModuleName $moduleForMock -ParameterFilter { $Message -match "Current server is not in AWS" }
}
It "Does Not Lookup Instance Tags if a Parameter is Provided" {
Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true }
Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { }
Get-DesignationTagNameByEnvironment "prod" | Out-Null
Assert-MockCalled -CommandName Get-CurrentInstanceTags -Times 0 -Exactly -Scope It -ModuleName $moduleForMock
}
It "Pulls Current Instance Tags if in AWS And No Parameter is Supplied" {
Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true }
Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { }
Get-DesignationTagNameByEnvironment | Out-Null
Assert-MockCalled -CommandName Get-CurrentInstanceTags -Times 1 -Exactly -Scope It -ModuleName $moduleForMock
}
It "Returns Unknown if the Tags Have an Unexpected Value for Environment" {
Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true }
Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { return @{
Key=$Global:AlkamiTagKeyEnvironment;Value="FoobarDoopityDo";
}}
Get-DesignationTagNameByEnvironment | Should -Be "Unknown"
Assert-MockCalled -CommandName Get-CurrentInstanceTags -Times 1 -Exactly -Scope It -ModuleName $moduleForMock
}
It "Throws if an Unknown Value is Passed as a Parameter" {
{ Get-DesignationTagNameByEnvironment "IfSomeoneAddsThisToTheValidateSetIllFirethem" } | Should -Throw
}
}
Context "Happy Path" {
It "Returns the Expected Values When Parameters are Provided" {
Get-DesignationTagNameByEnvironment "prod" | Should -Be $Global:AlkamiDesignationEnvironmentProd
Get-DesignationTagNameByEnvironment "prodshared" | Should -Be $Global:AlkamiDesignationEnvironmentProd
Get-DesignationTagNameByEnvironment "dr" | Should -Be $Global:AlkamiDesignationEnvironmentProd
Get-DesignationTagNameByEnvironment "drshared" | Should -Be $Global:AlkamiDesignationEnvironmentProd
Get-DesignationTagNameByEnvironment "dev" | Should -Be $Global:AlkamiDesignationEnvironmentDev
Get-DesignationTagNameByEnvironment "devshared" | Should -Be $Global:AlkamiDesignationEnvironmentDev
Get-DesignationTagNameByEnvironment "qa" | Should -Be $Global:AlkamiDesignationEnvironmentQA
Get-DesignationTagNameByEnvironment "qashared" | Should -Be $Global:AlkamiDesignationEnvironmentQA
Get-DesignationTagNameByEnvironment "sandbox" | Should -Be $Global:AlkamiDesignationEnvironmentSandbox
Get-DesignationTagNameByEnvironment "sandboxshared" | Should -Be $Global:AlkamiDesignationEnvironmentSandbox
Get-DesignationTagNameByEnvironment "staging" | Should -Be $Global:AlkamiDesignationEnvironmentStaging
Get-DesignationTagNameByEnvironment "stagingshared" | Should -Be $Global:AlkamiDesignationEnvironmentStaging
}
It "Returns the Expected Values When Tags are Queried" {
Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true }
Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { return $Global:MockedEnvironmentValue }
$Global:MockedEnvironmentValue = "prod"
Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentProd
$Global:MockedEnvironmentValue = "prodshared"
Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentProd
$Global:MockedEnvironmentValue = "dr"
Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentProd
$Global:MockedEnvironmentValue = "drshared"
Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentProd
$Global:MockedEnvironmentValue = "dev"
Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentDev
$Global:MockedEnvironmentValue = "devshared"
Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentDev
$Global:MockedEnvironmentValue = "qa"
Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentQA
$Global:MockedEnvironmentValue = "qashared"
Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentQA
$Global:MockedEnvironmentValue = "sandbox"
Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentSandbox
$Global:MockedEnvironmentValue = "sandboxshared"
Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentSandbox
$Global:MockedEnvironmentValue = "staging"
Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentStaging
$Global:MockedEnvironmentValue = "stagingshared"
Get-DesignationTagNameByEnvironment | Should -Be $Global:AlkamiDesignationEnvironmentStaging
Assert-MockCalled -CommandName Get-CurrentInstanceTags -Times 1 -Scope It -ModuleName $moduleForMock
}
}
}

View File

@ -0,0 +1,80 @@
function Get-DynamicAwsProfilesParameter {
<#
.SYNOPSIS
Returns a RuntimeDefinedParameterDictionary with all locally configured AWS Profiles
.DESCRIPTION
Returns a RuntimeDefinedParameterDictionary with all locally configured AWS Profiles for use as a DynamicParameter in functions. Does
not retrieve profiles for any AWSSDK configurations.
.PARAMETER DynamicParameterName
The parameter name to set on the return value. Defaults to ProfileName
.PARAMETER ParameterSetName
The parameter set to associate the dynamic parameter with. Defaults to all parameter sets.
.PARAMETER IsMandatoryParameter
Wheter or not the parameter is mandatory. Defaults to false.
.EXAMPLE
Get-DynamicAwsProfilesParameter
Key Value
--- -----
ProfileName System.Management.Automation.RuntimeDefinedParameter
.EXAMPLE
Get-DynamicAwsProfilesParameter -DynamicParameterName "HooDoggie"
Key Value
--- -----
HooDoggie System.Management.Automation.RuntimeDefinedParameter
.LINK
Get-AwsCredentialConfiguration
#>
[CmdletBinding()]
[OutputType([System.Management.Automation.RuntimeDefinedParameterDictionary])]
param(
[Parameter(Mandatory = $false)]
[string]$DynamicParameterName = "ProfileName",
[Parameter(Mandatory = $false)]
[string]$ParameterSetName = "__AllParameterSets",
[Parameter(Mandatory = $false)]
[switch]$IsMandatoryParameter
)
# Define the Paramater Attributes
$runtimeParameterDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
$attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$parameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute
$parameterAttribute.Mandatory = $IsMandatoryParameter
$parameterAttribute.ParameterSetName = $ParameterSetName
$parameterAttribute.HelpMessage = "The Local AWS Credential Profile Name to Use for Requests"
$attributeCollection.Add($parameterAttribute)
# Generate and add the ValidateSet
# Do not print warnings because this may be loaded/evaluated on servers without any profiles
$profileNames = Get-AwsCredentialConfiguration -WarningAction SilentlyContinue | Select-Object -ExpandProperty Name
# If no profiles are found, set the only available value to NoLocalProfilesFound
if (Test-IsCollectionNullOrEmpty -Collection $profileNames) {
$profileNames = @( "NoLocalProfilesFound" )
}
$validateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($profileNames)
$attributeCollection.Add($validateSetAttribute)
# Create the dynamic parameter
$runtimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($DynamicParameterName, [string], $attributeCollection)
$runtimeParameterDictionary.Add($DynamicParameterName, $RuntimeParameter)
return $runtimeParameterDictionary
}

View File

@ -0,0 +1,53 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
Describe "Get-DynamicAwsProfilesParameter" {
Context "Logic" {
Mock -CommandName Get-AwsCredentialConfiguration -ModuleName $moduleForMock -MockWith { return @(
(New-Object PSObject -Property @{ "Name"="FogoDeChao"; }),
(New-Object PSObject -Property @{ "Name"="12Cuts"; })
)}
It "Uses the Specified Parameter Name" {
$testParamName = "SupaFly"
$param = Get-DynamicAwsProfilesParameter -DynamicParameterName $testParamName
$param.Keys | Should -BeExactly $testParamName
}
It "Uses the Specified ParameterSet Name" {
$testParamSetName = "MisterP"
$param = Get-DynamicAwsProfilesParameter -ParameterSetName $testParamSetName
$param.Values.Attributes.ParameterSetName | Should -BeExactly $testParamSetName
}
It "Is a Mandatory Parameter if Specified" {
$param = Get-DynamicAwsProfilesParameter -IsMandatoryParameter
$param.Values.Attributes.Mandatory | Should -BeTrue
}
It "Creates a Validate Set with Valid AWS Profile Names" {
$param = Get-DynamicAwsProfilesParameter
$param.Values.Attributes.ValidValues | Should -Be @( "FogoDeChao", "12Cuts")
}
It "Returns a Single Invalid Parameter if None Can be Read from Local Configuration" {
Mock -CommandName Get-AwsCredentialConfiguration -ModuleName $moduleForMock -MockWith { return $null }
$param = Get-DynamicAwsProfilesParameter
$param.Values.Attributes.ValidValues | Should -Be @( "NoLocalProfilesFound" )
}
}
}

View File

@ -0,0 +1,71 @@
function Get-DynamicAwsRegionParameter {
<#
.SYNOPSIS
Returns a RuntimeDefinedParameterDictionary with all Alkami-permitted AWS Regions
.DESCRIPTION
Returns a RuntimeDefinedParameterDictionary with all Alkami-permitted AWS Regions for use as a DynamicParameter in functions.
.PARAMETER DynamicParameterName
The parameter name to set on the return value. Defaults to Region
.PARAMETER ParameterSetName
The parameter set to associate the dynamic parameter with. Defaults to all parameter sets.
.PARAMETER IsMandatoryParameter
Wheter or not the parameter is mandatory. Defaults to false.
.EXAMPLE
Get-DynamicAwsRegionParameter
Key Value
--- -----
Region System.Management.Automation.RuntimeDefinedParameter
.EXAMPLE
Get-DynamicAwsRegionParameter -DynamicParameterName "OhNoHeDidnt"
Key Value
--- -----
OhNoHeDidnt System.Management.Automation.RuntimeDefinedParameter
.LINK
Get-SupportedAwsRegions
#>
[CmdletBinding()]
[OutputType([System.Management.Automation.RuntimeDefinedParameterDictionary])]
param(
[Parameter(Mandatory = $false)]
[string]$DynamicParameterName = "Region",
[Parameter(Mandatory = $false)]
[string]$ParameterSetName = "__AllParameterSets",
[Parameter(Mandatory = $false)]
[switch]$IsMandatoryParameter
)
# Define the Paramater Attributes
$runtimeParameterDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
$attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$parameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute
$parameterAttribute.Mandatory = $IsMandatoryParameter
$parameterAttribute.ParameterSetName = $ParameterSetName
$parameterAttribute.HelpMessage = "The AWS Region to Use for Requests"
$attributeCollection.Add($parameterAttribute)
# Generate and add the ValidateSet
$supportedRegions = Get-SupportedAwsRegions
$validateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($supportedRegions)
$attributeCollection.Add($validateSetAttribute)
# Create the dynamic parameter
$runtimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($DynamicParameterName, [string], $attributeCollection)
$runtimeParameterDictionary.Add($DynamicParameterName, $RuntimeParameter)
return $runtimeParameterDictionary
}

View File

@ -0,0 +1,46 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
Describe "Get-DynamicAwsRegionParameter" {
Context "Logic" {
Mock -CommandName Get-SupportedAwsRegions -ModuleName $moduleForMock -MockWith { return @(
"mexico-south-55",
"australia-west-99"
)}
It "Uses the Specified Parameter Name" {
$testParamName = "SupaFly"
$param = Get-DynamicAwsRegionParameter -DynamicParameterName $testParamName
$param.Keys | Should -BeExactly $testParamName
}
It "Uses the Specified ParameterSet Name" {
$testParamSetName = "wOne"
$param = Get-DynamicAwsRegionParameter -ParameterSetName $testParamSetName
$param.Values.Attributes.ParameterSetName | Should -BeExactly $testParamSetName
}
It "Is a Mandatory Parameter if Specified" {
$param = Get-DynamicAwsRegionParameter -IsMandatoryParameter
$param.Values.Attributes.Mandatory | Should -BeTrue
}
It "Creates a Validate Set with Valid AWS Profile Names" {
$param = Get-DynamicAwsRegionParameter
$param.Values.Attributes.ValidValues | Should -Be @( "mexico-south-55", "australia-west-99")
}
}
}

View File

@ -0,0 +1,17 @@
function Get-EntrustAdminUrlFromClient {
<#
.SYNOPSIS
Returns the Entrust Admin URL based on a Client Database object
#>
[CmdletBinding()]
param(
[Parameter(Position = 0, Mandatory = $true)]
[PSObject]$Client
)
$queryString = "SELECT s.Value FROM core.ItemSetting s JOIN core.Item i on i.ID = s.ItemID JOIN core.Provider p on p.ID = i.ParentId WHERE REPLACE(p.AssemblyInfo, ' ', '') = " +
"'Alkami.Security.Provider.User.Entrust.Provider,Alkami.Security.Provider.User.Entrust' AND s.Name = 'EntrustAdminUrl'"
$url = Invoke-QueryOnClientDatabase $client $queryString
return ($url -ireplace "Service/services/AdminServiceV\d", "/do")
}

View File

@ -0,0 +1,84 @@
function Get-Environment {
<#
.SYNOPSIS
Tries to determine the Environment type (AWS, QA, Prod, Staging, etc.) using environment details
#>
[CmdletBinding()]
[OutputType([System.String])]
Param()
$logLead = (Get-LogLeadName);
$unknown = "UNKNOWN"
# Get Environment From Tags if Possible
if (Test-IsAws) {
Write-Verbose "$logLead : Hosting provider is AWS -- checking tags to determine environment"
$serverEnvironment = Get-CurrentInstanceTags ($Global:AlkamiTagKeyEnvironment) -ValueOnly
if ([String]::IsNullOrEmpty($serverEnvironment)) {
Write-Warning "$logLead : Could not determine environment from the $Global:AlkamiTagKeyEnvironment tag. Value returned: <$serverEnvironment>"
return $unknown
}
$validTagValues = @(
$Global:AlkamiTagValueEnvironmentProd,
$Global:AlkamiTagValueEnvironmentStaging,
$Global:AlkamiTagValueEnvironmentQa,
$Global:AlkamiTagValueEnvironmentDr,
$Global:AlkamiTagValueEnvironmentSandbox,
$Global:AlkamiTagValueEnvironmentDev,
$Global:AlkamiTagValueEnvironmentLTM,
$Global:AlkamiTagValueEnvironmentLoadtest
)
if ($serverEnvironment -notin $validTagValues) {
Write-Warning "$logLead : Unknown tag value '$serverEnvironment' identified. What is going on with your tags?"
return $unknown
}
Write-Host "$logLead : Environment determined to be $serverEnvironment based on the $Global:AlkamiTagKeyEnvironment tag value"
return $serverEnvironment
}
$environmentType = Get-AppSetting "Environment.Type" -ErrorAction SilentlyContinue
# Get Environment from Features / Beacon Values if Tags Not Available. So long as it's an ORB server it should never really get past this stage
if (!([String]::IsNullOrEmpty($environmentType))) {
Write-Host "$logLead : Checking Environment.Type Value $environmentType from Machine Config Against Known List"
$environment = switch ($environmentType)
{
"Production" { $Global:AlkamiTagValueEnvironmentProd }
"Staging" { $Global:AlkamiTagValueEnvironmentStaging }
"QA" { $Global:AlkamiTagValueEnvironmentQa }
"TeamQA" { $Global:AlkamiTagValueEnvironmentQa }
"Integration" { $Global:AlkamiTagValueEnvironmentQa }
"Development" { $Global:AlkamiTagValueEnvironmentDev }
}
Write-Host "$logLead : Environment determined to be $environment based on machine.config"
return $environment
}
# Get Environment from Computer Name if We Absolutely Have To. Some misconfigured CORP dev/qa environments might hit this
$compName = $env:ComputerName
if ($compName -like "ALK-*") {
if ($compName -like "*QA*") {
Write-Host ("$logLead : Environment determined to be {0} based on hostname {1}" -f $Global:AlkamiTagValueEnvironmentQa, $compName)
return $Global:AlkamiTagValueEnvironmentQa
}
else {
Write-Host ("$logLead : Environment determined to be {0} based on hostname {1}" -f $Global:AlkamiTagValueEnvironmentDev, $compName)
return $Global:AlkamiTagValueEnvironmentDev
}
}
# Give up
Write-Warning ("$logLead : Unable to determine environment automatically")
return $unknown
}

View File

@ -0,0 +1,113 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
Describe "Get-Environment" {
Context "Happy Path" {
It "Returns the Expected Environment Based on AWS Tags" {
Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { return $Global:MockedEnvironmentValue; }
Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true }
$Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentDev
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentDev
$Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentQa
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentQa
$Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentSandbox
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentSandbox
$Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentStaging
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentStaging
$Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentProd
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentProd
$Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentDR
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentDR
$Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentLTM
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentLTM
$Global:MockedEnvironmentValue = $Global:AlkamiTagValueEnvironmentLoadtest
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentLoadtest
}
It "Returns the Expected Environment Based on Machine Config Environment.Type Setting" {
Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $false }
Mock -CommandName Get-AppSetting -ModuleName $moduleForMock -MockWith { return $Global:MockAppSettingValue; }
$Global:MockAppSettingValue = "Development"
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentDev
$Global:MockAppSettingValue = "TeamQA"
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentQa
$Global:MockAppSettingValue = "QA"
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentQa
$Global:MockAppSettingValue = "Staging"
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentStaging
$Global:MockAppSettingValue = "Production"
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentProd
}
It "Returns the Expected Environment Based on (Shudder) Naming Convention" {
Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $false }
Mock -CommandName Get-AppSetting -ModuleName $moduleForMock -MockWith { return $null; }
$env:ComputerName = "ALK-PLA1-QA999999999999"
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentQa
$env:ComputerName = "ALK-WIDGETSZ-WEB1"
Get-Environment | Should -Be $Global:AlkamiTagValueEnvironmentDev
}
}
Context "Errors and Unknowns" {
It "Returns Unknown if the AWS Tag Value is Unexpected" {
Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true }
Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { return $Global:MockedEnvironmentValue; }
$Global:MockedEnvironmentValue = "You fool! You thought you could defeat me? With my power levels at OVER 50000?"
Get-Environment | Should -Be "Unknown"
}
It "Returns Unknown if All Efforts At Finding a Value to Key Off of Fail" {
Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $false }
Mock -CommandName Get-AppSetting -ModuleName $moduleForMock -MockWith { return $null; }
$env:COMPUTERNAME = "WEB1234567"
Get-Environment | Should -Be "Unknown"
}
It "Returns Unknown if The $Global:AlkamiTagKeyEnvironment Tag Doesn't Exist" {
Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true }
Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { return $null; }
Get-Environment | Should -Be "Unknown"
}
It "Returns Unknown if The $Global:AlkamiTagKeyEnvironment Tag Is Empty" {
Mock -CommandName Test-IsAws -ModuleName $moduleForMock -MockWith { return $true }
Mock -CommandName Get-CurrentInstanceTags -ModuleName $moduleForMock -MockWith { return ""; }
Get-Environment | Should -Be "Unknown"
}
}
}

View File

@ -0,0 +1,29 @@
function Get-FileContentHash {
<#
.SYNOPSIS
Retrieves a hash generated from a file's contents.
.DESCRIPTION
Use this command to generate a hash for use in determining if a file's text contents have changed.
.PARAMETER FilePath
[string] The full path (including file name) to the file to be hashed. Required.
.EXAMPLE
Get-FileContentHash "C:\Temp\File.txt"
.EXAMPLE
Get-FileContentHash -FilePath "C:\Temp\File.txt"
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[Alias("FilePath")]
[string]$file
)
$fileContent = [System.IO.File]::ReadAllBytes($file)
$convertedContent = [System.Text.Encoding]::GetEncoding(1252).GetString($fileContent);
return Get-UTF8ContentHash $convertedContent
}

View File

@ -0,0 +1,115 @@
function Get-HostnamesByEnvironmentName {
<#
.SYNOPSIS
Retrieves the hostnames for a specified environment.
.PARAMETER EnvironmentName
[string] The moniker associated with the environment (e.g. 'Smith', '16.1')
.PARAMETER EnvironmentType
[string] The type associated with the environment (e.g. 'prod', 'dr').
.PARAMETER IncludeOffline
[switch] Flag indicating whether or not to retrieve hostnames of offline instances.
.PARAMETER ProfileName
[string] AWS profile name to use in the query.
.PARAMETER Region
[string] AWS region to use in the query.
.EXAMPLE
Get-HostnamesByEnvironmentName -EnvironmentName '16' -EnvironmentType 'DR' -Region 'us-west-2' -IncludeOffline -Verbose
VERBOSE: [Get-HostnamesByEnvironmentName] : Using environment value 'DR'
[Get-DesignationTagNameByEnvironment] : Checking designation value for environment DR
VERBOSE: [Get-HostnamesByEnvironmentName] : Designation value resolved to 'pod'
APP32101109
APP326418
MIC3210483
MIC327179
WEB3229114
WEB323468
.EXAMPLE
Get-HostnamesByEnvironmentName -EnvironmentName 16 -Verbose
VERBOSE: [Get-HostnamesByEnvironmentName] : Environment type not specified; attempting to determine value.
[Get-Environment] : Environment determined to be prod based on the alk:env tag value
VERBOSE: [Get-HostnamesByEnvironmentName] : Using environment value 'prod'
[Get-DesignationTagNameByEnvironment] : Checking designation value for environment prod
VERBOSE: [Get-HostnamesByEnvironmentName] : Designation value resolved to 'pod'
APP16107240
APP1611088
APP1612031
APP16121149
APP16122255
MIC16106199
MIC16108100
MIC16117162
MIC1612736
MIC1665169
WEB162253
WEB1623247
WEB163753
WEB1647241
WEB165798
WEB166072
#>
[OutputType([string[]])]
[CmdletBinding()]
param(
[Parameter (Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$EnvironmentName,
[Parameter (Mandatory = $false)]
[string]$EnvironmentType = $null,
[Parameter(Mandatory = $false)]
[switch]$IncludeOffline,
[Parameter(Mandatory = $false)]
[string]$ProfileName = $null,
[Parameter(Mandatory = $false)]
[string]$Region = $null
)
$logLead = (Get-LogLeadName)
if ( [string]::IsNullOrEmpty( $EnvironmentType ) ) {
Write-Verbose "$logLead : Environment type not specified; attempting to determine value."
[string] $EnvironmentType = ( Get-Environment )
}
Write-Verbose "$logLead : Using environment value '$EnvironmentType'"
[string] $designation = ( Get-DesignationTagNameByEnvironment $environmentType )
Write-Verbose "$logLead : Designation value resolved to '$designation'"
$designationTag = ( "alk:{0}" -f $designation )
$searchTags = @{
$designationTag = $EnvironmentName
$Global:AlkamiTagKeyEnvironment = $EnvironmentType.ToLowerInvariant()
}
[Amazon.EC2.Model.Instance[]] $Instances = Get-InstancesByTag -tags $searchTags -IncludeOffline:$IncludeOffline -ProfileName $ProfileName -Region $Region
[string[]] $hostNames = @()
foreach ( $instance in $instances ) {
$hostname = Get-InstanceHostname $instance
if ( ! [string]::IsNullOrEmpty( $hostname ) ) {
$hostNames += $hostname
}
}
[Array]::Sort($hostNames)
return $hostNames
}

View File

@ -0,0 +1,164 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
Describe "Get-HostnamesByEnvironmentName" {
# Load up AWSPowerShell for Mocking if Available
$awsPowerShellLoaded = $false
if ($null -ne (Get-Module -ListAvailable AWSPowerShell)) {
Import-AWSModule # EC2
$awsPowerShellLoaded = $true
} else {
Write-Warning "AWSPowerShell Module *NOT* installed. Some tests will not execute."
}
Context "Parameter Validation" {
It "Null Environment Name Should Throw" {
{ Get-HostnamesByEnvironmentName -EnvironmentName $null } | Should Throw
}
It "Empty Environment Name Should Throw" {
{ Get-HostnamesByEnvironmentName -EnvironmentName '' } | Should Throw
}
}
Context "Get-Environment Is Called When EnvironmentType Parameter Is Null" {
Mock -CommandName Get-Environment -ModuleName $moduleForMock -MockWith { return 'Prod' }
Mock -CommandName Get-DesignationTagNameByEnvironment -ModuleName $moduleForMock -MockWith { return 'Prod' }
Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith { return @() }
$result = ( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType $null )
It "Get-Environment Is Called One Time" {
Assert-MockCalled Get-Environment -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock
}
}
Context "Get-Environment Is Called When EnvironmentType Parameter Is Empty" {
Mock -CommandName Get-Environment -ModuleName $moduleForMock -MockWith { return 'Prod' }
Mock -CommandName Get-DesignationTagNameByEnvironment -ModuleName $moduleForMock -MockWith { return 'Prod' }
Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith { return @() }
$result = ( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType '' )
It "Get-Environment Is Called One Time" {
Assert-MockCalled Get-Environment -Times 1 -Exactly -Scope Context -ModuleName $moduleForMock
}
}
Context "Get-Environment Is Not Called When EnvironmentType Parameter Is Provided" {
Mock -CommandName Get-Environment -ModuleName $moduleForMock -MockWith { return 'Prod' }
Mock -CommandName Get-DesignationTagNameByEnvironment -ModuleName $moduleForMock -MockWith { return 'Prod' }
Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith { return @() }
$result = ( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType 'Prod' )
It "Get-Environment Is Not Called" {
Assert-MockCalled Get-Environment -Times 0 -Exactly -Scope Context -ModuleName $moduleForMock
}
}
Context "Returns Empty Array When No Instances Are Found" {
Mock -CommandName Get-Environment -ModuleName $moduleForMock -MockWith { return 'Prod' }
Mock -CommandName Get-DesignationTagNameByEnvironment -ModuleName $moduleForMock -MockWith { return 'Prod' }
Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith { return @() }
It "Is Empty" {
( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType 'Prod' ) | Should -BeExactly @()
}
}
Context "Sorted Result Does Not Include Null Or Empty Entries" {
Mock -CommandName Get-Environment -ModuleName $moduleForMock -MockWith { return 'Prod' }
Mock -CommandName Get-DesignationTagNameByEnvironment -ModuleName $moduleForMock -MockWith { return 'Prod' }
It "Does Not Include Null" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith {
$testInstance = (New-Object Amazon.EC2.Model.Instance)
$testInstance.InstanceId = 'Test'
$nullInstance = (New-Object Amazon.EC2.Model.Instance)
$nullInstance.InstanceId = $null
return @( $testInstance, $nullInstance )
}
Mock -CommandName Get-InstanceHostname -ModuleName $moduleForMock -MockWith {
return $Instance.InstanceId.ToUpperInvariant()
}
( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType 'Prod' ) | Should -BeExactly @( 'TEST' )
}
It "Does Not Include Empty String" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith {
$testInstance = (New-Object Amazon.EC2.Model.Instance)
$testInstance.InstanceId = 'Test'
$emptyInstance = (New-Object Amazon.EC2.Model.Instance)
$emptyInstance.InstanceId = ''
return @( $testInstance, $emptyInstance )
}
Mock -CommandName Get-InstanceHostname -ModuleName $moduleForMock -MockWith {
return $Instance.InstanceId.ToUpperInvariant()
}
( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType 'Prod' ) | Should -BeExactly @( 'TEST' )
}
It "Is Sorted" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
Mock -CommandName Get-InstancesByTag -ModuleName $moduleForMock -MockWith {
$bInstance = (New-Object Amazon.EC2.Model.Instance)
$bInstance.InstanceId = 'B'
$aInstance = (New-Object Amazon.EC2.Model.Instance)
$aInstance.InstanceId = 'A'
return @( $bInstance, $aInstance )
}
Mock -CommandName Get-InstanceHostname -ModuleName $moduleForMock -MockWith {
return $Instance.InstanceId.ToUpperInvariant()
}
( Get-HostnamesByEnvironmentName -EnvironmentName 'Test' -EnvironmentType 'Prod' ) | Should -BeExactly @( 'A', 'B' )
}
}
}

View File

@ -0,0 +1,17 @@
function Get-IPSTSUrlFromClient {
<#
.SYNOPSIS
Returns the IPSTS URL based on a Client Database object
#>
[CmdletBinding()]
param(
[Parameter(Position = 0, Mandatory = $true)]
[PSObject]$Client
)
$queryString = "SELECT s.Value FROM core.ItemSetting s JOIN core.Item i on i.ID = s.ItemID JOIN core.Provider p on p.ID = i.ParentId WHERE REPLACE(p.AssemblyInfo, ' ', '') = " +
"'Alkami.Security.Provider.TokenTranslator.Entrust,Alkami.Security.Provider.TokenTranslator.Entrust.Provider' AND s.Name = 'Issuer'"
return (Invoke-QueryOnClientDatabase $client $queryString)
}

View File

@ -0,0 +1,39 @@
function Get-InstanceHostname {
<#
.SYNOPSIS
Gets the hostname for an EC2 instance.
.PARAMETER Instance
[Amazon.EC2.Model.Instance] The instance under inspection.
.EXAMPLE
Get-InstanceHostname -Instance (Get-CurrentInstance)
APP1611088
#>
[OutputType([string])]
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[Amazon.EC2.Model.Instance]$Instance
)
$logLead = (Get-LogLeadName)
[string] $hostname = ($instance.Tags | Where-Object { $_.Key -eq $Global:AlkamiTagKeyHostName } | Select-Object -First 1).Value
if ( [string]::IsNullOrEmpty( $hostname ) ) {
Write-Warning ( "{0} : No hostname found for {1}" -f $logLead, $instance.InstanceId )
$hostname = $null
} else {
$hostname = $hostname.ToUpperInvariant()
}
return $hostname
}

View File

@ -0,0 +1,198 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
Describe "Get-InstanceHostname" {
# Load up AWSPowerShell for mocking if available
$awsPowerShellLoaded = $false
if ($null -ne (Get-Module -ListAvailable AWSPowerShell)) {
Import-AWSModule # EC2
$awsPowerShellLoaded = $true
} else {
Write-Warning "AWSPowerShell Module *NOT* installed. Some tests will not execute."
}
Context "Parameter Validation" {
It "Null Instance Should Throw" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
{ Get-InstanceHostname -Instance $null } | Should Throw
}
It "Invalid Parameter Type Should Throw" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
{ Get-InstanceHostname -Instance 'test' } | Should Throw
}
}
Context "Hostname Determined By Instance Tag" {
Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock
It "Writes Warning When Hostname Tag Does Not Exist" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
$testInstance = (New-Object Amazon.EC2.Model.Instance)
$testInstance.InstanceId = 'Test'
Get-InstanceHostname -Instance $testInstance | Out-Null
Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It `
-ModuleName $moduleForMock -ParameterFilter { $Message -match "No hostname found" }
}
It "Returns Null When Hostname Tag Does Not Exist" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
$testInstance = (New-Object Amazon.EC2.Model.Instance)
$testInstance.InstanceId = 'Test'
( Get-InstanceHostname -Instance $testInstance ) | Should -BeNull
}
It "Writes Warning When Hostname Tag Is Null" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
$testTag = (New-Object Amazon.EC2.Model.Tag($Global:AlkamiTagKeyHostName, $null))
$testInstance = (New-Object Amazon.EC2.Model.Instance)
$testInstance.Tags.Add($testTag)
$testInstance.InstanceId = 'Test'
Get-InstanceHostname -Instance $testInstance | Out-Null
Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It `
-ModuleName $moduleForMock -ParameterFilter { $Message -match "No hostname found" }
}
It "Returns Null When Hostname Tag Is Null" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
$testTag = (New-Object Amazon.EC2.Model.Tag($Global:AlkamiTagKeyHostName, $null))
$testInstance = (New-Object Amazon.EC2.Model.Instance)
$testInstance.Tags.Add($testTag)
$testInstance.InstanceId = 'Test'
( Get-InstanceHostname -Instance $testInstance ) | Should -BeNull
}
It "Writes Warning When Hostname Tag Is Null" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
$testTag = (New-Object Amazon.EC2.Model.Tag($Global:AlkamiTagKeyHostName, ''))
$testInstance = (New-Object Amazon.EC2.Model.Instance)
$testInstance.Tags.Add($testTag)
$testInstance.InstanceId = 'Test'
Get-InstanceHostname -Instance $testInstance | Out-Null
Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It `
-ModuleName $moduleForMock -ParameterFilter { $Message -match "No hostname found" }
}
It "Returns Null When Hostname Tag Is Null" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
$testTag = (New-Object Amazon.EC2.Model.Tag($Global:AlkamiTagKeyHostName, ''))
$testInstance = (New-Object Amazon.EC2.Model.Instance)
$testInstance.Tags.Add($testTag)
$testInstance.InstanceId = 'Test'
( Get-InstanceHostname -Instance $testInstance ) | Should -BeNull
}
It "Does Not Write Warning When Hostname Tag Is Valid" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
$testTag = (New-Object Amazon.EC2.Model.Tag($Global:AlkamiTagKeyHostName, 'Test'))
$testInstance = (New-Object Amazon.EC2.Model.Instance)
$testInstance.Tags.Add($testTag)
$testInstance.InstanceId = 'Test'
Get-InstanceHostname -Instance $testInstance | Out-Null
Assert-MockCalled -CommandName Write-Warning -Times 0 -Exactly -Scope It `
-ModuleName $moduleForMock
}
It "Returns Uppercased Hostname Tag Value When Hostname Tag Is Valid" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
$testTag = (New-Object Amazon.EC2.Model.Tag($Global:AlkamiTagKeyHostName, 'Test'))
$testInstance = (New-Object Amazon.EC2.Model.Instance)
$testInstance.Tags.Add($testTag)
$testInstance.InstanceId = 'Test'
( Get-InstanceHostname -Instance $testInstance ) | Should -BeExactly 'TEST'
}
}
}

View File

@ -0,0 +1,68 @@
function Get-InstancesByTag {
<#
.SYNOPSIS
Gets EC2 Instances By Tag Filter
.PARAMETER Tags
[hashtable] Mandatory. Keys are tag names, values are the value.
.PARAMETER IncludeOffline
[switch] Include Instances not in a "running" state
.PARAMETER ProfileName
[string] Specific AWS CLI Profile to use in AWS API calls
.PARAMETER Region
[string] Specific AWS CLI Region to use in AWS API calls
#>
[CmdletBinding()]
param(
[Parameter (Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[hashtable]$Tags,
[Parameter(Mandatory = $false)]
[switch]$IncludeOffline,
[Parameter(Mandatory = $false)]
[string]$ProfileName,
[Parameter(Mandatory = $false)]
[string]$Region
)
$logLead = (Get-LogLeadName)
Import-AWSModule # EC2
$splatParams = @{}
$filters = @()
foreach ($key in $tags.Keys) {
$filter = (New-Object Amazon.EC2.Model.Filter);
$filter.Name = "tag:$key";
$filter.Value = $tags.$key;
$filters += $filter;
}
if (!($includeOffline.IsPresent)) {
$onlineFilter = (New-Object Amazon.EC2.Model.Filter)
$onlineFilter.Name = "instance-state-name"
$onlineFilter.Value = "running"
$filters += $onlineFilter;
}
if (!([string]::IsNullOrEmpty($ProfileName))) {
$splatParams["ProfileName"] = "$ProfileName"
}
if (!([string]::IsNullOrEmpty($Region))) {
$splatParams["Region"] = "$Region"
}
$instances = Get-EC2Instance -Filter $filters @splatParams
Write-Verbose ("$logLead : Found {0} Instances with Filter" -f $instances.Count)
return $instances.RunningInstance
}

View File

@ -0,0 +1,67 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
Describe "Get-InstancesByTag" {
# Load up AWSPowerShell for Mocking if Available
$awsPowerShellLoaded = $false
if ($null -ne (Get-Module -ListAvailable AWSPowerShell)) {
Import-AWSModule # EC2
$awsPowerShellLoaded = $true
} else {
Write-Warning "AWSPowerShell Module *NOT* installed. Some tests will not execute."
}
Context "Parameter and Environment Validation" {
Mock -ModuleName $moduleForMock Get-EC2Instance { return @("I'm not Null!") }
It "Excludes Offline Instances by Default" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
Get-InstancesByTag -tags @{"alk:pod" = "fakepod" }
Assert-MockCalled Get-EC2Instance -Times 1 -Exactly -Scope It `
-ModuleName $moduleForMock -ParameterFilter { ($null -ne ($Filter | Where-Object { $_.Name -eq "instance-state-name" })) }
}
It "Includes Offline Instances When Specified" {
if (!($awsPowerShellLoaded)) {
Set-ItResult -Inconclusive -Because "AWSPowerShell Not Installed"
continue;
}
# This test is less than ideal, since all we can realistically test is that the if statement which controls adding this filter or not
# is hit
Get-InstancesByTag -tags @{"alk:pod" = "fakepod" } -IncludeOffline
Assert-MockCalled Get-EC2Instance -Times 1 -Exactly -Scope It `
-ModuleName $moduleForMock -ParameterFilter { ($null -eq ($Filter | Where-Object { $_.Name -eq "instance-state-name" })) }
}
It "Correctly splats Region" {
Get-InstancesByTag -Tags @{"alk:pod" = "fakepod"} -Region "us-fake-1"
Assert-MockCalled Get-EC2Instance -Times 1 -Exactly -Scope It `
-ModuleName $moduleForMock -ParameterFilter {$Region -eq "us-fake-1"}
}
It "Correctly splats Profile" {
Get-InstancesByTag -Tags @{"alk:pod" = "fakepod"} -ProfileName "fake-profile"
Assert-MockCalled Get-EC2Instance -Times 1 -Exactly -Scope It `
-ModuleName $moduleForMock -ParameterFilter {$ProfileName -eq "fake-profile"}
}
}
}

View File

@ -0,0 +1,91 @@
function Get-LocalNlbIp {
<#
.SYNOPSIS
Gets the Ip for the NLB NIC which is in the same AZ as the server from which it's run.
.DESCRIPTION
Gets the Ip for the NLB NIC which is in the same AZ as the server from which it's run. Uses the current availability zone, ENI description, and interfacetype to determine the appropriate IP
.EXAMPLE
Get-LocalNlbIp -verbose
VERBOSE: [Get-LocalNlbIp] : Current Instance AZ Read as us-east-1b
VERBOSE: [Get-LocalNlbIp] : Environment Read as qa
[Get-DesignationTagNameByEnvironment] : Checking designation value for environment qa
VERBOSE: [Get-LocalNlbIp] : Read designation tag value Smith
VERBOSE: [Get-LocalNlbIp] : Using Expected NLB Name ELB net/Smith-qa-nlb for Filtering
VERBOSE: Invoking Amazon Elastic Compute Cloud operation 'DescribeNetworkInterfaces' in region 'us-east-1'
Returning IP Address for ENI with Description: ELB net/smith-qa-nlb/93947386b64a5aac, Id: eni-0718dc98cdcec5e18
10.26.91.212
#>
[CmdletBinding()]
param()
$logLead = (Get-LogLeadName)
Import-AWSModule # EC2
if (!(Test-IsAws))
{
Write-Warning "$logLead : This function can only be executed on an AWS server"
return
}
# Get the current instance and AZ
$currentInstance = Get-CurrentInstance;
$currentAz = $currentInstance.Placement.AvailabilityZone;
Write-Verbose "$logLead : Current Instance AZ Read as $currentAz"
# Check the current server's role
$serverRole = $currentInstance.Tag | Where-Object {$_.Key -eq $Global:AlkamiTagKeyRole}
if ($serverRole.Value -eq 'app:app')
{
# App servers should use 127.0.0.1
Write-Warning "This is currently running on an app server. The IP returned shouldn't be used in the host file."
}
# Get the expected designation tag name
$environment = $currentInstance.Tag | Where-Object { $_.Key -eq $Global:AlkamiTagKeyEnvironment; };
Write-Verbose "$logLead : Environment Read as $($environment.Value)"
$targetTag = Get-DesignationTagNameByEnvironment $environment.Value
if ($null -ne $targetTag) {
# Pull the Designation Tag Value
$environmentTagValue = $currentInstance.Tag | Where-Object {$_.Key -eq "alk:$targetTag" }
Write-Verbose "$logLead : Read designation tag value $($environmentTagValue.Value)"
} else {
Write-Warning "$logLead : Unable to pull $Global:AlkamiTagKeyEnvironment for the current instance. Execution cannot continue."
return $null;
}
$cleanedName = $environmentTagValue.Value.replace('.','-');
$nlbName = "ELB net/" + $cleanedName + '-' + $environment.Value + '-nlb';
Write-Verbose "$logLead : Using Expected NLB Name $nlbName for Filtering"
$nlbNics = Get-EC2NetworkInterface -Filter @( @{name='availability-zone';values=$currentAz} );
[array]$filteredNics = $nlbNics | Where-Object { $_.InterfaceType -eq 'network_load_balancer' -and $_.Description -match $nlbName}
$matchCount = $filteredNics.Count
Write-Verbose "$logLead : Found $matchCount Matching ENIs with InterfaceType: network_load_balancer, Description: $nlbName, Availability Zone $currentAz"
if ($null -ne $filteredNics -and $filteredNics.Count -eq 1) {
$nic = $filteredNics | Select-Object -First 1
Write-Host ("Returning IP Address for ENI with Description: {0}, Id: {1}" -f $nic.Description, $nic.NetworkInterfaceId)
return (($nic | Select-Object -First 1).PrivateIpAddress);
}
if ($null -eq $filteredNics) {
Write-Warning "$logLead : No ENIs found with Description $nlbName for AZ $currentAz"
return $null
}
Write-Warning ("$logLead : {0} ENIs found with Description $nlbName for AZ $currentAz. Execution cannot continue." -f $filteredNics.Count)
return $null
}

View File

@ -0,0 +1,329 @@
. $PSScriptRoot\..\..\Load-PesterModules.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.tests\.', '.'
$functionPath = Join-Path -Path $here -ChildPath $sut
Write-Host "Overriding SUT: $functionPath"
Import-Module $functionPath -Force
$moduleForMock = ""
#region Get-LocalNlbIp
# Handle the AWS Native Functions When AWSPowerShell Not Available
if ($null -eq (Get-Command "Get-EC2NetworkInterface" -ErrorAction SilentlyContinue))
{
function Get-EC2NetworkInterface {
throw "This Should Never Be Actually Called"
}
}
Describe "Get-LocalNlbIp" {
Mock -ModuleName $moduleForMock Test-IsAws { return $true }
Mock -ModuleName $moduleForMock Get-EC2NetworkInterface { return $null }
Mock -ModuleName $moduleForMock Get-CurrentInstance {
$placementMock = New-Object PSObject -Property @{
"AvailabilityZone" = "us-east-1a";
}
$pod6Instance = New-Object PSObject -Property @{
Placement = $placementMock;
Tag = @(
@{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleWeb},
@{Key = $Global:AlkamiTagKeyEnvironment; Value = $Global:AlkamiTagValueEnvironmentProd},
@{Key = "alk:pod"; Value = "6"}
);
}
return $pod6Instance
}
Context "When Environment Names Overlap" {
Mock -ModuleName $moduleForMock Get-EC2NetworkInterface {
# args[1] is an ec2 filter based on the AvailabilityZone from the current instance
# https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/EC2/TFilter.html
$ec2Filter = $args[1]
Write-Warning "Using EC2 Filter Value: $($ec2Filter.Values)"
$possibleReturns = @(
@{ "Description"="ELB net/12.6-prod-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="8.8.8.8"; InterfaceType="network_load_balancer"},
@{ "Description"="ELB net/6-prod-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="100.100.100.100"; InterfaceType="network_load_balancer"}
)
return $possibleReturns | Where-Object {$_.AvailabilityZone -like $ec2Filter.Values}
}
It "Returns the Correct IP When Searching for POD 6 and 12.6 is Available" {
Get-LocalNlbIp | Should -Be "100.100.100.100"
}
}
Context "When the Environment is an App Server" {
Mock -ModuleName $moduleForMock Get-CurrentInstance {
$placementMock = New-Object PSObject -Property @{
"AvailabilityZone" = "us-east-1a";
}
$pod6Instance = New-Object PSObject -Property @{
Placement = $placementMock;
Tag = @(
@{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleApp},
@{Key = $Global:AlkamiTagKeyEnvironment; Value = $Global:AlkamiTagValueEnvironmentProd},
@{Key = "alk:pod"; Value = "6"}
);
}
return $pod6Instance
}
It "Writes a Warning" {
( Get-LocalNlbIp 3>&1 ) -match "This is currently running on an app server." | Should -Be $true
}
}
Context "Environment Tag Switch" {
$validEnvironments = @("Prod", "DR")
foreach ($Global:podBasedEnvironment in $validEnvironments) {
It "Uses the alk:pod tag when the environment is $podBasedEnvironment" {
Mock -ModuleName $moduleForMock Get-CurrentInstance {
$placementMock = New-Object PSObject -Property @{
"AvailabilityZone" = "us-east-1a";
}
$pod6Instance = New-Object PSObject -Property @{
Placement = $placementMock;
Tag = @(
@{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleWeb},
@{Key = $Global:AlkamiTagKeyEnvironment; Value = "$podBasedEnvironment"},
@{Key = "alk:pod"; Value = "Pass"},
@{Key = "alk:lane"; Value = "Fail"}
);
}
return $pod6Instance
}
Mock -ModuleName $moduleForMock Get-EC2NetworkInterface {
# args[1] is an ec2 filter based on the NLB name constructed from the current instance
# https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/EC2/TFilter.html
$ec2Filter = $args[1]
Write-Warning "Using EC2 Filter Value: $($ec2Filter.Values)"
$possibleReturns = @(
@{ "Description"="ELB net/Pass-$podBasedEnvironment-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="100.100.100.100"; InterfaceType="network_load_balancer"},
@{ "Description"="ELB net/Fail-$podBasedEnvironment-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="9.9.9.9"; InterfaceType="network_load_balancer"},
@{ "Description"="ELB net/Fail-staging-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="8.8.8.8"; InterfaceType="network_load_balancer"}
)
return $possibleReturns | Where-Object {$_.AvailabilityZone -like $ec2Filter.Values}
}
Get-LocalNlbIp | Should -Be 100.100.100.100
}
}
It "Uses the alk:lane tag when the environment is Staging" {
Mock -ModuleName $moduleForMock Get-CurrentInstance {
$placementMock = New-Object PSObject -Property @{
"AvailabilityZone" = "us-east-1a";
}
$pod6Instance = New-Object PSObject -Property @{
Placement = $placementMock;
Tag = @(
@{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleWeb},
@{Key = $Global:AlkamiTagKeyEnvironment; Value = $Global:AlkamiTagValueEnvironmentStaging},
@{Key = "alk:pod"; Value = "Fail"},
@{Key = "alk:lane"; Value = "Pass"}
);
}
return $pod6Instance
}
Mock -ModuleName $moduleForMock Get-EC2NetworkInterface {
# args[1] is an ec2 filter based on the NLB name constructed from the current instance
# https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/EC2/TFilter.html
$ec2Filter = $args[1]
Write-Warning "Using EC2 Filter Value: $($ec2Filter.Values)"
$possibleReturns = @(
@{ "Description"="ELB net/Pass-staging-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="100.100.100.100"; InterfaceType="network_load_balancer"},
@{ "Description"="ELB net/Fail-staging-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="9.9.9.9"; InterfaceType="network_load_balancer"}
@{ "Description"="ELB net/Fail-prod-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="8.8.8.8"; InterfaceType="network_load_balancer"}
)
return $possibleReturns | Where-Object {$_.AvailabilityZone -like $ec2Filter.Values}
}
Get-LocalNlbIp | Should -Be 100.100.100.100
}
$validEnvironments = @("Dev", "QA")
foreach ($Global:designationBasedEnvironment in $validEnvironments) {
It "Uses the alk:designation tag when the environment is $designationBasedEnvironment" {
Mock -ModuleName $moduleForMock Get-CurrentInstance {
$placementMock = New-Object PSObject -Property @{
"AvailabilityZone" = "us-east-1a";
}
$instance = New-Object PSObject -Property @{
Placement = $placementMock;
Tag = @(
@{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleWeb},
@{Key = $Global:AlkamiTagKeyEnvironment; Value = "$designationBasedEnvironment"},
@{Key = "alk:pod"; Value = "Fail"},
@{Key = "alk:lane"; Value = "Fail"},
@{Key = "alk:designation"; Value = "Pass"}
);
}
return $instance
}
Mock -ModuleName $moduleForMock Get-EC2NetworkInterface {
# args[1] is an ec2 filter based on the NLB name constructed from the current instance
# https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/EC2/TFilter.html
$ec2Filter = $args[1]
Write-Warning "Using EC2 Filter Value: $($ec2Filter.Values)"
$possibleReturns = @(
@{ "Description"="ELB net/Pass-$designationBasedEnvironment-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="100.100.100.100"; InterfaceType="network_load_balancer"},
@{ "Description"="ELB net/Fail-$designationBasedEnvironment-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="9.9.9.9"; InterfaceType="network_load_balancer"}
@{ "Description"="ELB net/Fail-prod-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="8.8.8.8"; InterfaceType="network_load_balancer"}
)
return $possibleReturns | Where-Object {$_.AvailabilityZone -like $ec2Filter.Values}
}
Get-LocalNlbIp | Should -Be 100.100.100.100
}
}
}
Context "Error Scenarios" {
Mock -ModuleName $moduleForMock Get-EC2NetworkInterface {
# args[1] is an ec2 filter based on the NLB name constructed from the current instance
# https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/EC2/TFilter.html
$ec2Filter = $args[1]
Write-Warning "Using EC2 Filter Value: $($ec2Filter.Values)"
$possibleReturns = @(
@{ "Description"="ELB net/Pass-$podBasedEnvironment-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="100.100.100.100"; InterfaceType="network_load_balancer"},
@{ "Description"="ELB net/Pass-$podBasedEnvironment-nlb-something"; Id="Test"; "AvailabilityZone"="us-east-1a"; "PrivateIpAddress"="111.111.111.111"; InterfaceType="network_load_balancer"}
)
return $possibleReturns | Where-Object {$_.AvailabilityZone -like $ec2Filter.Values}
}
It "Writes a Warning and Returns Null When No Match Found" {
Mock -ModuleName $moduleForMock Get-CurrentInstance {
$placementMock = New-Object PSObject -Property @{
"AvailabilityZone" = "us-east-999";
}
$pod6Instance = New-Object PSObject -Property @{
Placement = $placementMock;
Tag = @(
@{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleWeb},
@{Key = $Global:AlkamiTagKeyEnvironment; Value = "$podBasedEnvironment"},
@{Key = "alk:pod"; Value = "Pass"},
@{Key = "alk:lane"; Value = "Fail"}
);
}
return $pod6Instance
}
Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock
Get-LocalNlbIp | Should -BeNullOrEmpty
Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It `
-ModuleName $moduleForMock -ParameterFilter { $Message -match "No ENIs found with Description" }
}
It "Writes a Warning and Returns Null When More Than One Match Found" {
Mock -ModuleName $moduleForMock Get-CurrentInstance {
$placementMock = New-Object PSObject -Property @{
"AvailabilityZone" = "us-east-1a";
}
$pod6Instance = New-Object PSObject -Property @{
Placement = $placementMock;
Tag = @(
@{Key = $Global:AlkamiTagKeyRole; Value = $Global:AlkamiTagValueRoleWeb},
@{Key = $Global:AlkamiTagKeyEnvironment; Value = "$podBasedEnvironment"},
@{Key = "alk:pod"; Value = "Pass"},
@{Key = "alk:lane"; Value = "Fail"}
);
}
return $pod6Instance
}
Mock -CommandName Write-Warning -MockWith {} -ModuleName $moduleForMock
Get-LocalNlbIp | Should -BeNullOrEmpty
Assert-MockCalled -CommandName Write-Warning -Times 1 -Exactly -Scope It `
-ModuleName $moduleForMock -ParameterFilter { $Message -match "2 ENIs found with Description" }
}
}
}
#endregion Get-LocalNlbIp

View File

@ -0,0 +1,109 @@
function Get-LogDiskUtilization {
<#
.SYNOPSIS
Gets disk utilization from ORB Logs
.PARAMETER ComputerName
The single remote computer name to execute against. If not supplied, runs on the local system
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$ComputerName = ""
)
$logLead = Get-LogLeadName
$isLocalExecution = $false
if (( [String]::IsNullOrEmpty($ComputerName) ) -or (Compare-StringToLocalMachineIdentifiers -stringToCheck $ComputerName )) {
Write-Verbose "$logLead : Setting local execution to true"
$ComputerName = $ENV:ComputerName
$isLocalExecution = $true
}
$logPath = Get-ORBLogsPath
if ($isLocalExecution) {
Write-Verbose "$logLead : Searching for log files locally"
$logFiles = Get-ChildItem -Path $logPath -Filter *.log* -File
} else {
Write-Verbose "$logLead : Searching for log files on $ComputerName"
$logFiles = Invoke-Command $ComputerName { Get-ChildItem -Path $using:logPath -Filter *.log* -File }
}
if (Test-IsCollectionNullOrEmpty $logFiles) {
Write-Host "$logLead : No log files found under $logPath on computer $ComputerName"
return
}
$logDetails = @()
foreach ($logFile in $logFiles) {
$logDetails += New-Object PSObject -Property @{
Name = $logFile.BaseName -replace ".log$", "";
Size = $logFile.Length
}
}
$groups = $logDetails | Group-Object -Property Name
$totalDiskUtilization = 0
$analysisResults = @()
foreach ($logGroup in ($groups | Sort-Object -Property Count -Descending)) {
[array]$sizeDetails = $logGroup.Group | Select-Object -ExpandProperty Size
if ($sizeDetails.Count -eq 0) {
$cumulativeSize = $sizeDetails;
} else {
[int]$cumulativeSize = 0
$sizeDetails | ForEach-Object {
$cumulativeSize += [int]$_
}
}
$totalDiskUtilization += $cumulativeSize
$analysisResults += New-Object PSObject -Property @{
LogFile = $logGroup.Name;
LogCount = $logGroup.Count;
Size = $cumulativeSize;
}
}
$topTenLogSizes = $analysisResults | Sort-Object -Property Size -Descending | Select-Object -First 10
# Begin unnecessarily complicated table print that I threw together during a meeting because I was bored
$maxLogNameLength = ($topTenLogSizes | Select-Object -ExpandProperty LogFile | Sort-Object -Property Length -Descending | Select-Object -First 1).Length
$maxLogNamePadding = [math]::Max($maxLogNameLength, 8)
$maxLogSizeLength = ($topTenLogSizes | Select-Object -ExpandProperty Size | ForEach-Object { $_.ToString() } | Sort-Object -Property Length -Descending | Select-Object -First 1).Length
$logSizePadding = [math]::Max($maxLogSizeLength , 4)
$headerRow = (" | " + ("Log File".PadRight($maxLogNameLength), "Size".PadRight($maxLogSizeLength), "Log Count" -Join " | ") + " | ")
$headerRowLength = $headerRow.Length - 4
$totalUtilizationLine = " | $ComputerName Log Utilization: $([math]::Round(($totalDiskUtilization/1024/1024),2))mb".PadRight($headerRowLength + 2) + "|"
$border = " %" + ("=" * $headerRowLength) + "%"
Write-Host $border
Write-Host $totalUtilizationLine
Write-Host $border
Write-Host $headerRow
Write-Host $border
foreach ($topLog in $topTenLogSizes) {
$outputLine = " | " + ($topLog.LogFile.PadRight($maxLogNamePadding), $topLog.Size.ToString().PadRight($logSizePadding), $topLog.LogCount.ToString().PadRight(9) -Join " | ") + " | "
Write-Host $outputLine
}
Write-Host $border
}

View File

@ -0,0 +1,39 @@
function Get-PackageForInstallation {
<#
.SYNOPSIS
Downloads a File to the Specified Outputfolder
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true)]
[string]$loglead,
[Parameter(Mandatory = $true)]
[string]$outputFolder,
[Parameter(Mandatory = $true)]
[string]$outputfileName,
[Parameter(Mandatory = $true)]
[string]$downloadUrl
)
if (!(Test-Path $outputFolder)) {
Write-Verbose ("$logLead : Creating Package Download Folder {0}" -f $outputFolder)
New-Item $outputFolder -ItemType Directory | Out-Null
}
$downloadFile = (Join-Path $outputFolder $outputfileName)
if (Test-Path $downloadFile) {
# If a job is rerun, this should prevent downloading again, unless it spans a day
Write-Host ("$logLead : Using Existing Package from {0}" -f $downloadFile)
}
else {
Write-Host ("$logLead : Downloading File from {0} to {1}" -f $downloadUrl, $downloadFile)
Invoke-WebRequest -Uri $downloadUrl -OutFile $downloadFile
}
return $downloadFile
}

View File

@ -0,0 +1,39 @@
function Get-PodName {
<#
.SYNOPSIS
Easily return human-readable name for the pod, lane, designation in which a host belongs.
.DESCRIPTION
Query the host's Machine.config to determine the pod, lane, designation, etc, and return in a format people can use.
.PARAMETER ComputerName
Optional Parameter. If specified, gets the name for that server's pod, lane, designation, etc.
.PARAMETER Full
Optional Parameter. If specified, returns the full name of the environment. Otherwise, returns the short name.
.EXAMPLE
Get-PodName -ComputerName APP169671
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName = "$env:computerName",
[Parameter(Mandatory = $false)]
[switch]$Full
)
# get the FQDN no matter what for simplicity
$ComputerName=[System.Net.Dns]::GetHostByName($ComputerName).HostName
# get the key value
$keyValue = Get-AppSetting -Key Environment.Name -ComputerName $ComputerName
if ( $Full ) {
$keyValue
} else {
$podName = ($keyValue -split " ")[-1]
$podName
}
}

View File

@ -0,0 +1,50 @@
function Get-SecretsForPod {
<#
.SYNOPSIS
Gets Secrets for Pod.
#>
[CmdletBinding()]
Param(
[string]$secretUserName,
[string]$secretPassword,
[string]$secretDomain,
[string]$secretFolderNames
)
$client = New-Object Alkami.Ops.SecretServer.Client
$authResult = $client.AuthenticateAsync($secretUserName, $secretPassword, $secretDomain).GetAwaiter().GetResult()
if ($authResult.Status -ne [Alkami.Ops.SecretServer.Enum.ResultStatus]::Success) {
Write-Warning ("Unable to authenticate with SecretServer: {0}" -f $authResult.Message)
if ($authResult.Errors.Count -gt 0) {
$errors = ("Error(s) from server - " + ($authResult.Errors | Select-Object -ExpandProperty "ErrorMessage") -join ", ")
Write-Warning -Message $errors
}
return
}
[HashTable]$secrets = $null
foreach ($secretFolder in $secretFolderNames.Split(',')) {
$result = $client.GetFolderSecretsAsync($secretFolder.Trim()).GetAwaiter().GetResult()
if ($result.Status -ne [Alkami.Ops.SecretServer.Enum.ResultStatus]::Success) {
Write-Warning ("Error pulling secrets for {0} from SecretServer: {1}" -f $secretFolder, $secretResult.Message)
if ($secretResult.Errors.Count -gt 0) {
$errors = ("Error(s) from server - " + ($secretResult.Errors | Select-Object -ExpandProperty "ErrorMessage") -join ", ")
Write-Warning -Message $errors
}
return
}
$secrets += $result.Secrets
}
return $secrets
}

View File

@ -0,0 +1,28 @@
function Get-SecurityGroupPrefix {
<#
.SYNOPSIS
Easily return human-readable name for the Security Group in which a host belongs.
.DESCRIPTION
Query the host's Machine.config to determine the Security Group for a host and return in a format people can use.
.PARAMETER ComputerName
Optional Parameter. If specified, gets the Security Group for that server's pod, lane, designation, etc. Defaults to local computer.
.EXAMPLE
Get-SecurityGroup -ComputerName APP169671
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName="$env:computerName"
)
# get the FQDN no matter what for simplicity
$ComputerName=[System.Net.Dns]::GetHostByName($ComputerName).HostName
$securityGroup=Get-AppSetting -Key Environment.UserPrefix -ComputerName $ComputerName
return $securityGroup
}

View File

@ -0,0 +1,166 @@
function Get-ServerStatusReport {
<#
.SYNOPSIS
Start the Server Status check, typically run after a Scale-Up event.
.DESCRIPTION
Performs several sanity tests to ensure the servers are fully operational after a Scale-Up event.
Main entrypoint that calls this is in teamcity.sre.code/ScaleEnvironments/Invoke-ReportServerStatus.ps1
.PARAMETER Servers
[string[]]Array of servers to run tests against. Usually this is a list of servers by Designation (LC3, Morph, etc.)
.PARAMETER ProfileName
[string] Specific AWS CLI Profile to use in AWS API calls.
.PARAMETER Region
[string] Specific AWS CLI Region to use in AWS API calls.
.NOTES
Outputs a table with the results of the test by host.
Returns custom object with the following properties:
"Hostname"
"Designation"
"InLoadBalancer"
"IsNagRunning"
"IsSubServiceRunning"
"AllAlkServicesRunning"
"TagsCorrect"
"IsAppServer"
"IsWebServer"
"IsMicServer"
#>
[CmdletBinding()]
[OutputType([Object])]
Param (
[Parameter(Mandatory = $true)]
[string[]] $Servers,
[Parameter(Mandatory = $true)]
[string] $ProfileName,
[Parameter(Mandatory = $true)]
[string] $Region
)
$logLead = (Get-LogLeadName)
Write-Host "$logLead : Running for servers:`n$Servers"
# Script Block to do the tests.
$testsScriptBlock = {
param ($hostname, $arguments)
Write-Host "##teamcity[blockOpened name='Running tests for hostname $hostname']"
$testResults = Invoke-Command -ComputerName $hostname -ScriptBlock {
$computername = (Get-FullyQualifiedServerName)
$isAppServer = Test-IsAppServer -ComputerName $computername
$isWebServer = Test-IsWebServer -ComputerName $computername
$isMicServer = Test-IsMicroServer -ComputerName $computername
# Make an object to hold the results of the tests
$testResult = New-Object PSObject -Property @{
"Hostname" = $computername
"Designation" = "Unknown"
"InLoadBalancer" = $null
"IsNagRunning" = $null
"IsSubServiceRunning" = $null
"AllAlkServicesRunning" = $null
"TagsCorrect" = $null
"IsAppServer" = $isAppServer
"IsWebServer" = $isWebServer
"IsMicServer" = $isMicServer
}
# NAG test
try {
if($IsAppServer) {
Write-Host "Running NAG test"
$nagStatus = Test-IsNagRunning -Server $computername 2>$null
$testResult.IsNagRunning = $nagStatus
} else {
$testResult.IsNagRunning = "N/A"
}
} catch {
Resolve-Error -ErrorRecord $_
$testResult.IsNagRunning = "Unknown"
}
# Subscription Service test
try {
if($isMicServer -or $isWebServer -or $isAppServer) {
Write-Host "Running Subscription Service test"
$chocoServices = Get-ChocolateyServices 2>$null
$subscriptionService = ($chocoServices | Where-Object {$_.Name -eq "Alkami.Services.Subscriptions.Host"})
if($null -ne $subscriptionService) {
$testResult.IsSubServiceRunning = ($subscriptionService.State -eq "Running")
} else {
$testResult.IsSubServiceRunning = $false
}
} else {
$testResult.IsSubServiceRunning = "N/A"
}
} catch {
Resolve-Error -ErrorRecord $_
$testResult.IsSubServiceRunning = "Unknown"
}
# All ALK Services running test?
Write-Host "Running All Alk Services test"
$alkServices = ($chocoServices | Where-Object {$_.Name -like "Alkami.*"})
$stoppedAlkServiceCount = ($alkServices | Where-Object {$_.State -eq "Stopped"}).count
$testResult.AllAlkServicesRunning = !($stoppedAlkServiceCount -gt 0)
# Tags test
try {
Write-Host "Running Tags test"
$tags = Get-CurrentInstanceTags
$autoShutdownTagKey = "alk:autoshutdown"
$autoShutdownTag = ($tags | Where-Object { ($_.Key -eq $autoShutdownTagKey -and $_.Value -eq "true") })
$isAutoShutdown = ($null -ne $autoShutdownTag)
$testResult.TagsCorrect = (!($isAutoShutdown))
} catch {
Resolve-Error -ErrorRecord $_
$testResult.TagsCorrect = "Unknown"
}
return $testResult
}
# LoadBalancer test
# This test is done outside of the above script block as it needs to be run off-host.
# Running this on host will fail due to the IAM creds not having permissions for some
# resources (Only for NGinx lookups).
try {
if($testResults.IsAppServer -or $testResults.IsWebServer) {
Write-Host "Running LoadBalancer test"
$lbState = Get-LoadBalancerState -Server $hostname -AwsProfileName $arguments.ProfileName -AwsRegion $arguments.Region
$testResults.InLoadBalancer = ($lbState -eq "Active")
} else {
$testResults.InLoadBalancer = "N/A"
}
} catch {
Resolve-Error -ErrorRecord $_
$testResults.InLoadBalancer = "Unknown"
}
Write-Host "##teamcity[blockClosed name='Running tests for hostname $hostname']"
# Return the test
return $testResults
}
# Parallel over the servers to test.
try {
$results = (Invoke-Parallel2 -Objects $Servers -Arguments @{ProfileName = $ProfileName; Region = $Region;} -Script $testsScriptBlock).Result
} catch {
Resolve-Error -ErrorRecord $_
}
# Send results to caller.
return $results
}

View File

@ -0,0 +1,34 @@
function Get-SlackAction {
<#
.SYNOPSIS
Used to create interactive elements in messages
.PARAMETER Text
The text to display
.PARAMETER Url
The URL the user will interact with
.PARAMETER Type
Defaults to button
#>
[CmdletBinding()]
[OutputType([object])]
param (
[Parameter(Mandatory = $true)]
[string]$Text,
[Parameter(Mandatory = $true)]
[string]$Url,
[Parameter(Mandatory = $false)]
[ValidateSet('button')] # We only usually offer one option type. Feel free to update when you've got a new use-case
[string]$Type = 'button'
)
return @{
text = $Text
url = $Url
type = $Type
}
}

Some files were not shown because too many files have changed in this diff Show More