Powershell: Forcing evaluation of a block of code in Add-member's -value option - csv

I'm trying to import users into an active directory using a CSV file and a powershell script. I create a CSV with the headers normally associated with an AD object:
mail,name,givenName,middleName,surname,company,department,title,plaintextPassword,path,description,userPrincipalName
...and filled it up.
Now I want to use Powershell's new-aduser cmmdlet to generate users for each item in this sheet - the problem I'm having is that new-aduser requires a SecureString, not just a normal string for an account's password. Skipping this conversion results in my users being created correctly, but with no passwords and their account disabled.
The command I'm using is as follows:
import-csv .\users.csv | add-member -passthru -memberType NoteProperty -value {$_ | select plaintextPassword | ConvertTo-SecureString -fromplaintext -force}
The result is user records like the following:
mail : tom.fubar#contoso.com
name : tom.fubar
givenName : Tom
middleName :
surname : Fubar
company : Contoso
department : IT
title : Technician
accountPassword : LongPasswordThatFitsADComplexityRequirements123!
path : OU=UserAccounts,OU=IT,OU=employees,DC=contoso,DC=com
description :
userPrincipalName : tom.fubar#contoso.com
encodedPassword : {$_ | select accountPassword | ConvertTo-SecureString -asplaintext -force}
The bit of code that should be evaluated for converting the plaintext password to a SecureString is being passed verbatim, rather than executed inline.
What is the proper way to force the code block to be evaluated, and use its result as the argument to New-Member -value?
Tried:
Enclosing the script block in $(...) - Results in a null NoteProperty added to the object
Replacing the {...} with $(...) - Results in a null NoteProperty added to the object
(as shown by piping the whole command to Get-Member)

Eris has posted a perfectly valid workaround, but to answer why it won't work for you, it's because $_ doesn't apply to any old script block. It's "special" and used only in certain contexts.
Additionally, -NotePropertyValue expects a static value.
Instead, you could add a ScriptProperty like so:
import-csv .\users.csv |
add-member -passthru -memberType ScriptProperty -value {$this.plaintextPassword | ConvertTo-SecureString -fromplaintext -force}
In this context for example, $_ is not even used; you have to use $this to refer to the parent object.
This does result in the script being processed every time the property is accessed though. If you don't want that, and want to do a static value assignment that's calculated per object, then you must enumerate yourself:
import-csv .\users.csv | ForEach-Object {
$val = $_ | select plaintextPassword | ConvertTo-SecureString -fromplaintext -force
$_ | add-member -passthru -memberType NoteProperty -value $val -Force
}

One solution I've found is to not bother with Add-Member, instead use a calculated property like so:
import-csv .\users.csv |
select -Property *, #{
n="encodedPassword";
e={$_.plaintestPassword | ConvertTo-SecureString -fromplaintext -force}}
(Removed broken add-member after comment from #PetSerAl)

Related

Assining cmdlet to variable and printing full, literal value in powershell

I've been trying to assign a cmdlet to a variable and then being able to inspect the variable's value as [it was] in the original cmdlet; as opposed to just executing it or only seeing its usual properties with commands like:
get-variable $seevars | fl *
or:
get-childitem -path variable:\$seevars | fl *
E.g. I create a variable:
$seevars = get-childitem variable: | where-object -property name -match somePrefix*
After a while, I may forget what $seevars contains or may want to modify it and so will want to inspect $seevars, expecting to see the value of :
'get-childitem variable: | where-object -property name -match somePrefix*'
Instead I get, undesirably, its properties in its technical format (I assume); out of which it will be hard for me to reconstruct its original, literal cmdlet.
Is this at all possible?

Can't call piped properties in a function. Powershell

So I'm trying to create a "download" function that uses a piped object property to determine a download method (sftp or http). Then either create an sftp script for putty/winscp or curl the http url. I am defining objects as follows:
#WinSCP
$winscp = new-object psobject
$winscp | add-member noteproperty name "WinSCP"
$winscp | add-member noteproperty dltype "http"
$winscp | add-member noteproperty file "winscp.exe"
$winscp | add-member noteproperty url "https://cdn.winscp.net/files/WinSCP-5.17.8-Setup.exe"
$winscp | add-member noteproperty path "$env:ProgramFiles(x86)\WinSCP"
$winscp | add-member noteproperty install 'msiexec /i "$DataPath\$winscp.file" /quiet /norestart'
#Database
$db = new-object psobject
$db | add-member noteproperty name "Client Database"
$db | add-member noteproperty dltype "sftp"
$db | add-member noteproperty file "database_"
$db | add-member noteproperty ver "check"
$db | add-member noteproperty ext ".csv"
$db | add-member noteproperty dir "db"
#DatabaseVersion
$db_ver = new-object psobject
$db_ver | add-member noteproperty name "Database Version File"
$db_ver | add-member noteproperty dltype "sftp"
$db_ver | add-member noteproperty file "current_version.txt"
$db_ver | add-member noteproperty dir "db"
Currently I'm having issues with the $Input variable within the function. It can only be used once and does not translate into an if statement. Since it contains an object with multiple properties, it needs converted to a new object within the function first I think. I'm new to powershell and haven't found a way of doing this yet. Here is the function I made and am trying to use:
function Download () {
#HTTP Download Method
if ($input.dltype -eq "http") {
curl $input.url -O $DataPath\$input.file
#HTTP Success or Error
$curlResult = $LastExitCode
if ($curlResult -eq 0)
{
Write-Host "Successfully downloaded $input.name"
}
else
{
Write-Host "Error downloading $input.name"
}
pause
}
#SFTP Download Method
if ($input.dltype -eq "sftp") {
sftpPassCheck
#Detect if version required
if ($input.ver = "check") {
#Download the objects version file
"$+$Input+_ver" | Download
#Update the object's ver property
$input.ver = [IO.File]::ReadAllText("$DataPath\current_version.txt")
#Build the new filename
$input.file = "$input.file"+"$input.ver"+"$input.ext"
#Delete the version file
Remove-Item "$DataPath\current_version.txt"
}
& "C:\Program Files (x86)\WinSCP\WinSCP.com" `
/log="$DataPath\SFTP.log" /ini=nul `
/command `
"open sftp://ftpconnector:$script:sftp_pass#$input.ip/ -hostkey=`"`"ssh-ed25519 255 SETvoRlAT0/eJJpRhRRpBO5vLfrhm5L1mRrMkOiPS70=`"`" -rawsettings ProxyPort=0" `
"cd /$input.dir" `
"lcd $DataPath" `
"get $input.file" `
"exit"
#SFTP Success or Error
$winscpResult = $LastExitCode
if ($winscpResult -eq 0)
{
Write-Host "Successfully downloaded $input.name"
}
else
{
Write-Host "Error downloading $input.name"
}
}
}
I'm probably missing something simple but I'm clueless at this point. Oh usage should be:
WinSCP | download
The proper way to bind input from the pipeline to a function's parameters is to declare an advanced function - see about_Functions_Advanced_Parameters and the implementation in the bottom section of this answer.
However, in simple cases a filter will do, which is a simplified form of a function that implicitly binds pipeline input to the automatic $_ variable and is called for each input object:
filter Download {
if ($_.dltype -eq "http") {
# ...
}
}
$input is another automatic variable, which in simple (non-advanced) functions is an enumerator for all pipeline input being received and must therefore be looped over.
That is, the following simple function is the equivalent of the above filter:
function Download {
# Explicit looping over $input is required.
foreach ($obj in $input) {
if ($obj.dltype -eq "http") {
# ...
}
}
}
If you do want to turn this into an advanced function (note that I've changed the name to conform to PowerShell's verb-noun naming convention):
function Invoke-Download {
param(
# Declare a parameter explicitly and mark it as
# as pipeline-binding.
[Parameter(ValueFromPipeline, Mandatory)]
$InputObject # Not type-constraining the parameter implies [object]
)
# The `process` block is called for each pipeline input object
# with $InputObject referencing the object at hand.
process {
if ($InputObject.dltype -eq "http") {
# ...
}
}
}
mklement0 is spot on - $input is not really meant to used directly, and you're probably much better off explicitly declaring your input parameters!
In addition to the $InputObject pattern shown in that answer, you can also bind input object property values to parameters by name:
function Download
{
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[Alias('dltype')]
[string]$Protocol = 'http'
)
process {
Write-Host "Choice of protocol: $Protocol"
}
}
Notice that although the name of this parameter is $Protocol, the [Alias('dltype')] attribute will ensure that the value of the dltype property on the input object is bound.
The effect of this is:
PS ~> $WinSCP,$db |Download
Choice of protocol: http
Choice of protocol: sftp
Keep repeating this pattern for any required input parameter - declare a named parameter mapped to property names (if necessary), and you might end up with something like:
function Download
{
[CmdletBinding()]
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[ValidateSet('sftp', 'http')]
[Alias('dltype')]
[string]$Protocol,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[Alias('dir')]
[string]$Path = $PWD,
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
[Alias('url','file')]
[string]$Uri
)
process {
Write-Host "Downloading $Uri to $Path over $Protocol"
}
}
Now you can do:
PS ~> $WinSCP,$db |Download
Downloading https://cdn.winscp.net/files/WinSCP-5.17.8-Setup.exe to C:\Program Files(x86)\WinSCP over http
Downloading database_ to db over sftp
We're no longer dependent on direct access to $input, $InputObject or $_, nice and clean.
Please see the about_Functions_Advanced_Parameters help file for more information about parameter declaration.

PowerShell function not accepting array of objects

In PowerShell, I have an array of objects that I need to pass to a function. The function is to then loop through all of the objects in the array, but it seems that it is not accepting the parameter value correctly.
Take the following example, where I pass an array containing two objects. I would expect the count of the array to be 2 both before the function and within the function, but as soon as it hits the function the count is 1, and my input is not as expected; only the last object is discovered.
Am I missing something here, or is this a bug in PowerShell?
Example code
### I've also tries '[object]', '[array]' and '[array[]]' as the type for '$testArr'.
function Test-PassArrayOfObjects
{
param(
[parameter(Mandatory,ValueFromPipeline)]
[object[]]$testArr
)
Write-Host "In function count: $($testArr.Count)"
$testArr | ForEach-Object { $_ }
}
$test1 = New-Object –TypeName PSObject
$test1 | Add-Member -MemberType NoteProperty -Name Test1 -Value Value1
$test2 = New-Object –TypeName PSObject
$test2 | Add-Member -MemberType NoteProperty -Name Test2 -Value Value2
$testArr = #($test1, $test2)
$testArr.GetType() | Format-Table
Write-Host "Before function count: $($testArr.Count)"
$testArr | Test-PassArrayOfObjects
Output from example code
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Before function count: 2
In function count: 1
Test2
-----
Value2
Working fix
Based on the answer below, I have this working example, which I've been able to apply to my real life scenario.
function Test-PassArrayOfObjects
{
param(
[parameter(Mandatory,ValueFromPipeline)]
[object]$testArr
)
Process {
Write-Host "In function count: $($testArr.Count)"
$testArr
}
}
$test1 = New-Object –TypeName PSObject
$test1 | Add-Member -MemberType NoteProperty -Name Test1 -Value Value1
$test1 | Add-Member -MemberType NoteProperty -Name Test2 -Value Value2
$test2 = New-Object –TypeName PSObject
$test2 | Add-Member -MemberType NoteProperty -Name Test1 -Value Value1
$test2 | Add-Member -MemberType NoteProperty -Name Test2 -Value Value2
$testArr = #($test1, $test2)
$testArr.GetType() | Format-Table
Write-Host "Before function count: $($testArr.Count)"
$testArr | ForEach-Object { $_ | Test-PassArrayOfObjects }
When sending input to a function via the pipeline, your function should include a Process block:
function Test-PassArrayOfObjects
{
param(
[parameter(Mandatory,ValueFromPipeline)]
[object[]]$testArr
)
Process {
Write-Host "In function count: $($testArr.Count)"
$testArr | ForEach-Object { $_ }
}
}
This is necessary because of the way the pipeline handles collections I believe. It automatically unrolls them and handles them one item at a time, so your ForEach-Object isn't getting the whole collection in the $testArr variable.
You often see functions like this still incorporate a ForEach though, in case the input is sent via a parameter in which case it is received all at once. For example: Test-PassArrayOfObjects -TestArr #(1,2,3).
Your issue is further conflated by the fact that your array has two objects with different properties. This is creating confusion in the output because PowerShell decides how to format the output based on the first object and uses the same formatting when it outputs the second object, but then you don't see it because it doesn't share any of the same properties (I think this is what is occurring anyway..).
You can see that both objects get processed by putting | Format-List on the $_ which forces both outputs to be formatted as list output independently. Note that this isn't good practice in a real function scenario of course. An alternative is to make the property name on both objects Test1. Then you will see the output you probably expected without using Format-List.

Creating a Csv using Powershell

I am trying to take a filename such as: John_Doe_E_DOB_1/1/46_M(This is the gender)_ID_0000000_IMG_FileName_Date-of-File_1/1/15_Doc-page-1 And create a CSV file to open in Excel with column headers for: Last Name, First Name, MI, ID No, File Name, Date of File along with doc type. Here's my code so far:
Get-ChildItem -Path C:\Users\name\desktop\test -Recurse | ForEach-Object {$_ | add-member -name "Owner" -membertype noteproperty -value (get-acl $_.fullname).owner -passthru} | Sort-Object fullname | Select BaseName,Name,Owner | Export-Csv -Force -NoTypeInformation C:\Users\name\desktop\test\thing.csv
All this is doing is dropping that really long file name in at the top, and then adding the ext at the end in another column. Example:
John_Doe_E_DOB_1/1/46_M(This is the gender)_ID_0000000_IMG_FileName_Date-of-File_1/1/15_Doc-page-1 Would be in column 1 and
John_Doe_E_DOB_1/1/46_M(This is the gender)_ID_0000000_IMG_FileName_Date-of-File_1/1/15_Doc-page-1.txt <----- Would be the only difference in column 2
How can I split this up for over a million files, all different lengths, and sizes, and get it to break up into the categories listed above? All help would be greatly appreciated.
I would replace the Select stage of your pipeline with a call to a filter function like this:
filter GenObj {
$parts = $_.FullName.Split('_')
new-object pscustomobject -property #{
Owner = (get-acl $_.fullname).owner
FirstName = $parts[0]
LastName = $parts[1]
MiddleInitial = $parts[2]
# Fill in the rest
}
}
Get-ChildItem -Path C:\Users\name\desktop\test -Recurse |
Sort-Object fullname |
GenObj |
Export-Csv -Force -NoTypeInformation C:\Users\name\desktop\test\thing.csv
This will create a new custom object with all the properties on it that correspond to the parts of the filename you want to extract.
This string splitting approach may not work depending on how you handle names with no middle initial.
Also be aware that if you are processing a million files, the use of Sort-Object will cause every single FileInfo object (one for every file) to get buffered in memory so the sort can be performed. You may likely run out of memory and the command will fail. I would consider removing Sort-Object in this scenario.

Powershell, JSON and NoteProperty

I'm trying to use Zabbix API with powershell to automate some monitoring stuff.
I'd like to retrieve "items" based on different parameters passed to my function to do something like this : if -itemDescription parameter is passed, look for this description and/or if parameter -host is passed limit scope to that host etc...
You can find the method description here : https://www.zabbix.com/documentation/1.8/api/item/get
This is a correct request :
{
"jsonrpc":"2.0",
"method":"item.get",
"params":{
"output":"shorten",
"search": {"description": "apache"},
"limit": 10
},
"auth":"6f38cddc44cfbb6c1bd186f9a220b5a0",
"id":2
}
So, I know how to add several "params", I did it for the host.create method, with something like this :
$proxy = #{"proxyid" = "$proxyID"}
$templates = #{"templateid" = "$templateID"}
$groups = #{"groupid" = "$hostGroupID"}
...
Add-Member -PassThru NoteProperty params # {host=“$hostName”;dns="$hostFQDN";groups=$groups;templates=$templates;proxy_hostid=$proxyID} |
...
What I don't know however is how to make it conditional. I can't find the right syntax to add a "if" statement in the middle of that line. Something like :
Add-Member -PassThru NoteProperty params #{output="extend";if(itemDescription) {search=$desctiption} } )
Thanks a lot guys!
Also, pleaser pardon my English, it's not my 1st language
Like Kayasax, i created my "params" before passing it to add-member.
FYI, this is my woring code :
#construct the params
$params=#{}
$search=#{}
#construct the "search" param
if ($itemDescription -ne $null) {
$search.add("description", $itemDescription)
$params.add("search",$search)
}
#contruct the "host" param
if ($hostName -ne $null) {$params.add("host", $hostname) }
#finish the params
$params.add("output", "extend")
#construct the JSON object
$objitem = (New-Object PSObject | Add-Member -PassThru NoteProperty jsonrpc '2.0' |
Add-Member -PassThru NoteProperty method 'item.get' |
Add-Member -PassThru NoteProperty params $params |
Add-Member -PassThru NoteProperty auth $session.result |
Add-Member -PassThru NoteProperty id '2') | ConvertTo-Json