Iron Scripter Prelude 3 Solution

Last week the Chairman provided another prelude challenge. This challenge was intended to get you familiar with Just Enough Administration (JEA). This is an admittedly advanced topic, but the Chairman expects nothing less from his Iron Scripters. To assist you in your quest the Chairman has graciously provided another sample solution. JEA is not a cookie-cutter technology as every organization is different and every business case is slightly different.

To get started you would have needed to create a RoleCapability file and a PSSessionConfigurationFile. Your first step might have been to ask PowerShell for help.

Help New-PSRoleCapabilityFile
Help New-PSSessionConfigurationFile

Given the requirements, you might have created a role capability file like this.

#BitsAdministration.psrc
@{
    # ID used to uniquely identify this document
    GUID                    = '2765e350-2627-46cc-8bf5-81493f357cef'
    # Author of this document
    Author                  = 'Art Deco'
    # Description of the functionality provided by these settings
    Description             = 'A sample JEA capability file for BITS administration'
    # Company associated with this document
    CompanyName             = 'Company'
    # Copyright statement for this document
    Copyright               = '2019'
    # Modules to import when applied to a session
    # ModulesToImport = 'MyCustomModule', @{ ModuleName = 'MyCustomModule'; ModuleVersion = '1.0.0.0'; GUID = '4d30d5f0-cb16-4898-812d-f20a6c596bdf' }
    ModulestoImport         = "BitsTransfer"
    # Aliases to make visible when applied to a session
    #VisibleAliases = 'Item1', 'Item2'
    VisibleAliases          = "gsv", "gcim", "dir", "h", "r"
    # Cmdlets to make visible when applied to a session
    # VisibleCmdlets = 'Invoke-Cmdlet1', @{ Name = 'Invoke-Cmdlet2'; Parameters = @{ Name = 'Parameter1'; ValidateSet = 'Item1', 'Item2' }, @{ Name = 'Parameter2'; ValidatePattern = 'L*' } }
    VisibleCmdlets          = "Get-Date",
    "Get-History",
    "Invoke-History",
    @{ Name = 'Start-Service'; Parameters = @{ Name = 'Name'; ValidateSet = 'BITS' }, @{Name = "Passthru"}},
    @{ Name = 'Stop-Service'; Parameters = @{ Name = 'Name'; ValidateSet = 'BITS' }, @{Name = "Passthru"}},
    @{ Name = 'Restart-Service'; Parameters = @{ Name = 'Name'; ValidateSet = 'BITS' }, @{Name = "Passthru"}},
    @{ Name = 'Set-Service'; Parameters = @{ Name = 'Name'; ValidateSet = 'BITS' }, @{Name = 'StartupType'}, @{Name = "Passthru"}},
    "bitstransfer\*"
    # Functions to make visible when applied to a session
    # VisibleFunctions = 'Invoke-Function1',
    # @{ Name = 'Invoke-Function2'; Parameters = @{ Name = 'Parameter1'; ValidateSet = 'Item1', 'Item2' }, @{ Name = 'Parameter2'; ValidatePattern = 'L*' } }
    VisibleFunctions        = "help", "Get-PSSender", "Get-Service", "Get-ChildItem", "Get-CimInstance"
    # External commands (scripts and applications) to make visible when applied to a session
    # VisibleExternalCommands = 'Item1', 'Item2'
    VisibleExternalCommands = "c:\windows\system32\netstat.exe", "c:\windows\system32\bitsadmin.exe"
    # Providers to make visible when applied to a session
    VisibleProviders        = 'FileSystem'
    # Scripts to run when applied to a session
    # ScriptsToProcess = 'C:\ConfigData\InitScript1.ps1', 'C:\ConfigData\InitScript2.ps1'
    # Aliases to be defined when applied to a session
    # AliasDefinitions = @{ Name = 'Alias1'; Value = 'Invoke-Alias1'}, @{ Name = 'Alias2'; Value = 'Invoke-Alias2'}
    # Functions to define when applied to a session
    # FunctionDefinitions = @{ Name = 'MyFunction'; ScriptBlock = { param($MyInput) $MyInput } }
    FunctionDefinitions     = @{ Name = 'Get-PSSender'; ScriptBlock = {
            param()
            [pscustomobject]@{
                ConnectionString = $PSSenderInfo.ConnectionString
                ConnectedUser    = $PSSenderInfo.ConnectedUser
                RunAsUser        = $PSSenderInfo.RunAsUser
                PSVersion        = $PSSenderInfo.ApplicationArguments.PSVersionTable.PSVersion
            }
        }
    },
    @{Name = 'Get-CimInstance'; ScriptBlock = {
            [cmdletbinding()]
            [alias("gcim")]
            Param(
                [ValidateSet("win32_Service")]
                [string]$Classname = "Win32_Service"
            )
            Begin {
                if (-Not $PSBoundParameters.ContainsKey("Classname")) {
                    $PSBoundParameters.Add("Classname", "Win32_Service")
                }
                $PSBoundParameters.add("Filter", "Name='bits'")
                Write-Verbose ($PSBoundParameters | Out-String)
                try {
                    $outBuffer = $null
                    if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                        $PSBoundParameters['OutBuffer'] = 1
                    }
                    $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('CimCmdlets\Get-CimInstance', [System.Management.Automation.CommandTypes]::Cmdlet)
                    $scriptCmd = {& $wrappedCmd @PSBoundParameters }
                    $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
                    $steppablePipeline.Begin($PSCmdlet)
                }
                catch {
                    throw
                }
            } #begin
            Process {
                try {
                    $steppablePipeline.Process($_)
                }
                catch {
                    throw
                }
            } #process
            End {
                try {
                    $steppablePipeline.End()
                }
                catch {
                    throw
                }
            } #end
        }
    },
    @{Name = 'Get-Service'; ScriptBlock = {
            [CmdletBinding()]
            [alias("gsv")]
            Param()
            Begin {
                $PSBoundParameters.Add("Name", "Bits")
                Write-Verbose ($PSBoundParameters | Out-String)
                try {
                    $outBuffer = $null
                    if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                        $PSBoundParameters['OutBuffer'] = 1
                    }
                    $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Get-Service', [System.Management.Automation.CommandTypes]::Cmdlet)
                    $scriptCmd = {& $wrappedCmd @PSBoundParameters }
                    $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
                    $steppablePipeline.Begin($PSCmdlet)
                }
                catch {
                    throw
                }
            } #begin
            Process {
                try {
                    $steppablePipeline.Process($_)
                }
                catch {
                    throw
                }
            } #process
            End {
                try {
                    $steppablePipeline.End()
                }
                catch {
                    throw
                }
            } #end
        }
    },
    @{Name = "Get-ChildItem"; ScriptBlock = {
            [CmdletBinding()]
            [alias("dir")]
            Param(
                [Parameter(Position = 0)]
                [ValidateSet("C:\BitsDownloads")]
                [string]$Path = "C:\BitsDownloads",
                [Parameter(Position = 1)]
                [string]$Filter,
                [string[]]$Include,
                [string[]]$Exclude,
                [Alias('s')]
                [switch]$Recurse,
                [uint32]$Depth,
                [switch]$Force,
                [switch]$Name,
                [switch]$File
            )
            Begin {
                try {
                    $outBuffer = $null
                    if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                        $PSBoundParameters['OutBuffer'] = 1
                    }
                    if (-not ($PSBoundParameters.ContainsKey("Path"))) {
                        $PSBoundParameters.Add("Name", "C:\BitsDownloads")
                    }
                    $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Get-ChildItem', [System.Management.Automation.CommandTypes]::Cmdlet)
                    $scriptCmd = {& $wrappedCmd @PSBoundParameters }
                    $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
                    $steppablePipeline.Begin($PSCmdlet)
                }
                catch {
                    throw
                }
            } #begin
            Process {
                try {
                    $steppablePipeline.Process($_)
                }
                catch {
                    throw
                }
            } #process
            End {
                try {
                    $steppablePipeline.End()
                }
                catch {
                    throw
                }
            } #end
        }
    }
    # Variables to define when applied to a session
    # VariableDefinitions = @{ Name = 'Variable1'; Value = { 'Dynamic' + 'InitialValue' } }, @{ Name = 'Variable2'; Value = 'StaticInitialValue' }
    # Environment variables to define when applied to a session
    # EnvironmentVariables = @{ Variable1 = 'Value1'; Variable2 = 'Value2' }
    # Type files (.ps1xml) to load when applied to a session
    # TypesToProcess = 'C:\ConfigData\MyTypes.ps1xml', 'C:\ConfigData\OtherTypes.ps1xml'
    # Format files (.ps1xml) to load when applied to a session
    # FormatsToProcess = 'C:\ConfigData\MyFormats.ps1xml', 'C:\ConfigData\OtherFormats.ps1xml'
    # Assemblies to load when applied to a session
    # AssembliesToLoad = 'System.Web', 'System.OtherAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
}

This file would most likely be placed in a RoleCapabilities folder in a module that you will deploy to the target server.

a JEA module layout

The psm1 file can be empty with no functions exported. The proxy and helper functions are defined in the psrc file but they could also have been defined in the module. Otherwise, the manifest is pretty simple.

#
# Module manifest for module 'BitsAdmin'
#

@{
# Script module or binary module file associated with this manifest.
RootModule = 'bitsadmin.psm1'
# Version number of this module.
ModuleVersion = '1.2.0'
# Supported PSEditions
CompatiblePSEditions = @("Desktop")
# ID used to uniquely identify this module
GUID = 'd3c7dad3-6ade-40c5-9f30-e41bdd23d1e0'
# Author of this module
Author = 'The Chairman'
# Company or vendor of this module
CompanyName = ''
# Copyright statement for this module
Copyright = ''
# Description of the functionality provided by this module
# Description = ''
# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '5.1'
# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''
# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''
# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# DotNetFrameworkVersion = ''
# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# CLRVersion = ''
# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @('bitstransfer')
# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()
# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()
# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = ''
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = '*'
# Variables to export from this module
VariablesToExport = '*'
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = '*'
# DSC resources to export from this module
# DscResourcesToExport = @()
# List of all modules packaged with this module
# ModuleList = @()
# List of all files packaged with this module
# FileList = @()
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{
    PSData = @{
        # Tags applied to this module. These help with module discovery in online galleries.
        # Tags = @()
        # A URL to the license for this module.
        # LicenseUri = ''
        # A URL to the main website for this project.
        # ProjectUri = ''
        # A URL to an icon representing this module.
        # IconUri = ''
        # ReleaseNotes of this module
        # ReleaseNotes = ''
    } # End of PSData hashtable
} # End of PrivateData hashtable
# HelpInfo URI of this module
# HelpInfoURI = ''
# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''
}

To use this will require a PSSessionConfiguration file.

$params = @{
    Path                = ".\myBits.pssc"
    SessionType         = "RestrictedRemoteServer"
    TranscriptDirectory = "c:\JEA-Transcripts"
    RunAsVirtualAccount = $True
    Description         = "Company BITS Admin endpoint"
    RoleDefinitions     = @{'Company\BitsAdmins' = @{ RoleCapabilities = 'BITSAdministration' }}
}
New-PSSessionConfigurationFile @params

Here is the completed file.

#myBits.pssc
@{
# Version number of the schema used for this document
SchemaVersion = '2.0.0.1'
# ID used to uniquely identify this document
GUID = 'c09ce4c6-3759-472c-b48c-7a2b5e4c6419'
# Author of this document
Author = 'Art Deco'
# Description of the functionality provided by these settings
Description = 'Company BITS Admin endpoint'
# Session type defaults to apply for this session configuration. Can be 'RestrictedRemoteServer' (recommended), 'Empty', or 'Default'
SessionType = 'RestrictedRemoteServer'
# Directory to place session transcripts for this session configuration
TranscriptDirectory = 'C:\JEA-Transcripts'
# Whether to run this session configuration as the machine's (virtual) administrator account
RunAsVirtualAccount = $true
# Scripts to run when applied to a session
#ScriptsToProcess = 'C:\ConfigData\InitScript2.ps1'
# User roles (security groups), and the role capabilities that should be applied to them when applied to a session
RoleDefinitions = @{
    'BitsAdmins' = @{
        'RoleCapabilities' = 'BITSAdministration' } }
}

It is a good idea to test it.

Test-PSSessionConfigurationFile .\myBits.pssc

At this point you will want to set up the Active Directory domain with the necessary groups and user accounts.

#domain admin credential
$cred = Get-Credential Company\artd
$dc = New-PSSession -VMName DOM1 -Credential $cred
#create a global group
Invoke-Command { New-ADGroup -Name BitsAdmins -GroupScope Global } -Session $dc
#create a test user account
Invoke-Command {
    $p = @{
        Name              = "BillBits"
        SamAccountName    = "billb"
        UserPrincipalName = "[email protected]"
        AccountPassword   = (ConvertTo-SecureString "[email protected]" -AsPlainText -force)
        Enabled           = $True
        passthru          = $True
    }
    New-ADUser @p
} -session $dc
#add the user to the BitsAdmin domain global group
Invoke-Command {
    Add-ADGroupMember -Identity "BitsAdmins" -Members (Get-ADUser billb)
} -session $dc
Remove-PSSession $dc

Next, the node needs to be setup.

$s = New-PSSession -VMName SRV1 -Credential $cred
#add the Bits feature
Invoke-Command { Add-WindowsFeature Bits } -session $s
#copy the module assuming in the parent location
$copyparams = @{
    Path        = ".\BitsAdmin"
    Container   = $True
    Recurse     = $True
    Destination = "$env:ProgramFiles\WindowsPowerShell\Modules"
    ToSession   = $s
    force       = $True
}
Copy-item @copyparams
#verify the module
Invoke-Command { Get-Module BitsAdmin -list } -session $s
#create Transcript folder
Invoke-Command {
    If (-Not (Test-Path c:\JEA-Transcripts)) {
        New-Item C:\JEA-Transcripts -ItemType Directory
    }
} -session $s
#create BitsTransfer folder
Invoke-Command {
    If (-Not (Test-Path C:\BitsDownloads)) {
        New-item C:\BitsDownloads -ItemType Directory
    }
    Set-Content -Path C:\BitsDownloads\readme.txt -Value "This is a sample file."
} -session $s
#copy the pssc
Copy-Item -Path .\myBits.pssc -Destination C:\ -ToSession $s -force
#setup the new one
Invoke-Command { Register-PSSessionConfiguration -Path C:\myBits.pssc -Name BitsAdmin} -session $s
#Get session config to verify
Invoke-Command {Get-PSSessionConfiguration bitsadmin | Select-Object *} -session $s
#need an execution policy so modules will load
Invoke-Command { Set-ExecutionPolicy remotesigned -force } -session $s

Once setup you might want to verify what the user can and cannot do.

Invoke-Command {
    Get-PSSessionCapability -ConfigurationName BitsAdmin -Username company\billb
} -session $s
#test as user
$bill = Get-Credential Company\billb
$test = New-PSSession -VMName SRV1 -Credential $bill -ConfigurationName bitsadmin
Enter-PSSession $Test
#run commands and verify what user can and cannot do

The JEA endpoint in action

Creating and deploying a JEA configuration takes some planning, testing and refinement.

The Chairman will be back with another prelude challenge.


One Reply to “Iron Scripter Prelude 3 Solution”

Comments are closed.