postgres.psm1

$script:SqlExplicitRoleMembersWithInclusions = "
    SELECT
        m.rolname AccountName
        r.rolname RoleName
    FROM pg_auth_members am
        JOIN pg_roles m on am.member = m.oid
        JOIN pg_roles r on r.oid = am.roleid
    WHERE m.rolcanlogin and (r.rolesuper or r.rolinherit or r.rolcreaterole or r.rolreplication or r.rolbypassrls) and r.rolname in ({0})"


$script:SqlExplicitGrantsWithInclusions = "
    SELECT
        u.usesysid AS Id,
        u.usename AS AccountName
    FROM pg_catalog.pg_user u
    where {0}
"

$script:SqlExplicitGrantsWithExclusions = "
    SELECT
    u.usesysid AS Id,
    u.usename AS AccountName
    FROM pg_catalog.pg_user u
    where not {0}"


$script:SqlServerPermissionsMap = @{
    "createdb" = "Create database"
    "super" = "All permissions"
    "repl" = "Manage Replication"
    "bypassrls" = "Bypass row level security"
}
$script:SqlServerPermissionsString = ($script:SqlServerPermissionsMap | Out-String)

function Invoke-ThrowPermissionsException
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]$Message
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    Write-Host -ForegroundColor Yellow $script:SqlServerPermissionsString
    Write-Host -ForegroundColor Yellow "Example Usage:"
    Write-Host -ForegroundColor Yellow " -IncludePermissions @(`"createdb`",`"super`")"
    Write-Host -ForegroundColor Yellow " or"
    Write-Host -ForegroundColor Yellow " -ExcludePermissions @(`"repl`",`"bypassrls`")"
    Write-Host -ForegroundColor Yellow " or (to turn it off)"
    Write-Host -ForegroundColor Yellow " -ExcludePermissions @()"
    throw $Message
}

<#
.SYNOPSIS
Discover privileged accounts in a SQL Server database.
 
.DESCRIPTION
This cmdlet may be used to discover privileged accounts in a Postgres database. When
called without arguments, the default behavior is to find local accounts that have added to any
admin roles or that have been granted global permissions except usecreatedb, usesuper, userepl.
The caller can override this behavior by specifying which directly granted permissions to exclude
or include.
 
When a credential is not supplied to this cmdlet, it will automatically look for an open
access request with a matching asset name or network address and use that access request
to get the password to run the discovery. If no access request is found, the cmdlet
will prompt for an account name and password to use.
 
.PARAMETER NetworkAddress
IP address or hostname of a SQL Server database.
 
.PARAMETER Instance
Name of the postgres instance to connect to
 
.PARAMETER Credential
A PowerShell credential object that can be used to connect to the database server to
execute the discovery job.
 
.PARAMETER Roles
A list of roles to search for to identify privileged accounts, or set to @() to turn off role search.
See https://www.postgresql.org/docs/9.0/database-roles.html
 
.PARAMETER ExcludePermissions
A list of permissions to exclude when searching for privileged accounts, or set to @() to turn off permission search.
See https://www.postgresql.org/docs/9.0/user-manag.html
 
.PARAMETER IncludePermissions
A list of permissions to include when searching for privileged accounts.
See https://www.postgresql.org/docs/9.0/user-manag.html
 
.INPUTS
None.
 
.OUTPUTS
System.Management.Automation.PSObject.
 
.EXAMPLE
Get-SgDiscPostgresAccount Postgres.test.env
 
.EXAMPLE
Get-SgDiscPostgresAccount Postgres.test.env -Credential (Get-Credential)
 
.EXAMPLE
Get-SgDiscPostgresAccount Postgres.test.env -IncludePermissions Create_priv,Drop_priv
#>

function Get-SgDiscPostgresAccount
{
    [CmdletBinding(DefaultParameterSetName="IncludePerms")]
    param (
        [Parameter(Mandatory=$true,Position=0)]
        [string]$NetworkAddress,
        [Parameter(Mandatory=$true,Position=1)]
        [string]$Instance,
        [Parameter(Mandatory=$false)]
        [PSCredential]$Credential = $null,
        [Parameter(Mandatory=$false)]
        [string[]]$Roles = @(),
        [Parameter(Mandatory=$false,ParameterSetName="ExcludePerms")]
        [string[]]$ExcludePermissions = @(),
        [Parameter(Mandatory=$false,ParameterSetName="IncludePerms")]
        [string[]]$IncludePermissions = @("createdb", "super", "repl", "bypassrls")
    )

    if (-not $PSBoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
    if (-not $PSBoundParameters.ContainsKey("Verbose")) { $VerbosePreference = $PSCmdlet.GetVariableValue("VerbosePreference") }

    if (-not $Credential)
    {
        # doing this here allows error action and verbose parameters to propagate
        $Credential = (Get-SgDiscConnectionCredential $NetworkAddress)
    }
    
    # make sure InvokeQuery is installed
    if (-not (Get-Module InvokeQuery)) 
    { 
        try 
        {
            Import-Module InvokeQuery
        }
        catch 
        {
            throw "SQL account discovery in safeguard-discovery requires InvokeQuery. Please run: Install-Module InvokeQuery."
        }
    }

    $local:NetworkCredential = $Credential.GetNetworkCredential()
    $ConnectionString = "User ID=$($local:NetworkCredential.UserName);password=$($local:NetworkCredential.Password);host=$NetworkAddress;Database=$Instance"

    # handle explicit permissions
    if (($PSCmdlet.ParameterSetName -eq "IncludePerms" -and $IncludePermissions) -or ($PSCmdlet.ParameterSetName -eq "ExcludePerms" -and $ExcludePermissions))
    {
        if ($PSCmdlet.ParameterSetName -eq "IncludePerms" -and $IncludePermissions)
        {
            $local:PermInclusions = @()
            foreach ($local:Perm in $IncludePermissions)
            {
                if (-not $script:SqlServerPermissionsMap.ContainsKey($local:Perm))
                {
                    Invoke-ThrowPermissionsException "Invalid permission inclusion '$($local:Perm)'"
                }
                $local:PermInclusions += "use$($local:Perm)"
            }
            $local:Sql = ($script:SqlExplicitGrantsWithInclusions -f ($local:PermInclusions -join " or "))
        }
        elseif ($PSCmdlet.ParameterSetName -eq "ExcludePerms" -and $ExcludePermissions)
        {
            $local:PermExclusions = @()
            foreach ($local:Perm in $ExcludePermissions)
            {
                if (-not $script:SqlServerPermissionsMap.ContainsKey($local:Perm))
                {
                    Invoke-ThrowPermissionsException "Invalid permission exclusion '$($local:Perm)'"
                }
                $local:PermExclusions += "use$($local:Perm)"
            }
            $local:Sql = ($script:SqlExplicitGrantsWithExclusions -f ($local:PermExclusions -join " and not "))
        }

        # query to find matching permissions (this is filtered to local accounts)
        $local:PrivilegedAccountsFromPermissions = (Invoke-PostgreSqlQuery -Sql $local:Sql -ConnectionString $ConnectionString)
    }
    else
    {
        $local:PrivilegedAccountsFromPermissions = @()
    }

    # handle sql server roles
    if ($Roles)
    {
        $local:RoleInclusions = @()
        foreach ($local:Role in $Roles)
        {
            $local:RoleInclusions += "'$($local:Role)'"
        }
        $local:Sql = ($script:SqlExplicitRoleMembersWithInclusions -f ($local:RoleInclusions -join ","))

        # query to find matching role memberships (this is filtered to local accounts)
        $local:PrivilegedAccountsFromRoles = (Invoke-PostgreSqlQuery -Sql $local:Sql -ConnectionString $ConnectionString)
    }
    else
    {
        $local:PrivilegedAccountsFromRoles = @()
    }

    # process results
    $local:Results = @{}
    $local:PrivilegedAccountsFromPermissions | ForEach-Object {
            $local:Results[$_.AccountName] = New-Object PSObject -Property ([ordered]@{
                AccountName = $_.AccountName;
                Roles = @();
                Description = "safeguard-discovery"
            })
        }

    $local:PrivilegedAccountsFromRoles | ForEach-Object {
        if ($local:Results[$_.AccountName])
        {
            $local:Results[$_.AccountName].Roles += ($_.RoleName)
        }
        else
        {
            $local:Results[$_.AccountName] = New-Object PSObject -Property ([ordered]@{
                AccountName = $_.AccountName;
                Roles = @($_.RoleName);
                Description = "safeguard-discovery --"
            })
        }
    }

    # convert results to an array and add the description
    $local:Results.Values | ForEach-Object {
        if ($_.Roles)
        {
            $_.Description += " roles:" + ($_.Roles -join ",")
        }
        $_
    }
}