How to create a Function using Powershell - function

I need help with the code below. I want the script to perform the following: prompt the user for an AD group name, if the group name is found, export the group members to a CSV file. one of the requirements is that I must include a function statement. Thank you in advance.
The code works if I use a variable like the following example: $groupsusers = Get-ADGroup -Identity $nameofgroup, instead of the function statement.
However, I don't want to use a variable, I want to implement a function statement.
$prompt = "Enter A Group Name"
do
{
$nameofgroup = Read-Host $prompt
}
until(!$(dsquery Group-Object $nameofgroup; $prompt = "Group
'$nameofgroup' was not found, try again"))
$nameofgroup = Read-Host $prompt
function GetGroupInfoToCsv (#what parameters go here?){
ForEach-Object{
$settings = #{ Group = $_.DistinguishedName; Member = $null }
$_| Get-ADGroupMember |
ForEach-Object{
$settings.Member = $_.DistinguishedName
New-Object PsObject -Property $settings
}
}
}
GetGroupInfoToCsv | Export-Csv .\GroupMembers.csv -NoTypeInformation

You could combine the asking for user input and returning the info into the same function. Something like this:
function Get-GroupMembers {
$prompt = "Enter A Group Name. Press Q to quit"
# create an endless loop
while ($true) {
Clear-Host
$answer = Read-Host $prompt
# if the user has had enough, exit the function
if ($answer -eq 'Q') { return }
# try and find one or more AD groups using the answer as (part of) the name
$group = Get-ADGroup -Filter "Name -like '*$answer*'"
# if we have found something, exit the while loop and start enumerating the members
if ($group) { break }
$prompt = "Group '$answer' was not found, try again. Press Q to quit"
}
# you only get here if Get-ADGroup found one or more groups
$group | ForEach-Object {
# output a PSObject with the properties you are after
$members = $_ | Get-ADGroupMember
foreach ($member in $members) {
[PsCustomObject]#{
'Group' = $_.DistinguishedName
'Member' = $member.DistinguishedName
}
}
}
}
# call the function
$groupinfo = Get-GroupMembers
# test if the function returned anything.
# the user could have cancelled of the group had no members to output
if ($groupinfo) {
Write-Host "Adding $($groupinfo.Count) items to the CSV file"
# without -Append, you would overwrite your CSV file..
$groupinfo | Export-Csv .\GroupMembers.csv -NoTypeInformation -Append
}
else {
Write-Host 'Nothing found..'
}
As you can see, I have changed the function name so it complies with the Verb-Noun convention in PowerShell.

Related

Get-ADUser in function returns nothing on first run, then returns double values [duplicate]

I have a script block/function that returns PSCustomObject followed by Write-Host.
I want to get the output first then print the write-host but I can't seem to figure it out.
function ReturnArrayList {
param (
[int] $number
)
[System.Collections.ArrayList]$folderList = #()
$folderObject = [PSCustomObject]#{
Name = 'John'
number = $number
}
#Add the object to the array
$folderList.Add($folderObject) | Out-Null
return $folderList
}
$sb = {
param (
[int] $number
)
[System.Collections.ArrayList]$folderList = #()
$folderObject = [PSCustomObject]#{
Name = 'John'
number = $number
}
#Add the object to the array
$folderList.Add($folderObject) | Out-Null
return $folderList
}
ReturnArrayList -number 5
#Invoke-Command -ScriptBlock $sb -ArgumentList 5
Write-Host "This write host should come later"
Result:
This write host should come after
Name number
---- ------
John 5
Desired result:
Name number
---- ------
John 5
This write host should come after
How can I get the return result first and print the write-host message?
Thank you for your help in advance!
You can force PowerShell to write the output from ReturnArrayList to the screen before reaching Write-Host by piping it to either one of the Format-* cmdlets or Out-Default:
$object = ReturnArrayList -number 5
$object |Out-Default
Write-Host "This write host should come later"
Result:
Name number
---- ------
John 5
This write host should come later
Beware that your ReturnArrayList function does not actually return an ArrayList - PowerShell will automatically enumerate the item(s) in $folderlist, and since it only contains one item, the result is just a single PSCustomObject, "unwrapped" from the ArrayList so to speak:
PS ~> $object = ReturnArrayList -number 5
PS ~> $object.GetType().Name
PSCustomObject
To preserve enumerable types as output from functions, you'll need to either use Write-Output -NoEnumerate, or wrap the it in an array using the , operator:
function ReturnArrayList {
param (
[int] $number
)
[System.Collections.ArrayList]$folderList = #()
$folderObject = [PSCustomObject]#{
Name = 'John'
number = $number
}
#Add the object to the array
$folderList.Add($folderObject) | Out-Null
return ,$folderList
# or
Write-Output $folderList -NoEnumerate
}
Data is usually output to the pipeline, while Write-Host bypasses the pipeline and writes to the console directly.
Using Write-Output instead of Write-Host will fix this issue. You can easily find more in-depth information on this topic, and when not to Write-Host.

Script to Enforce MFA failing to grab dataset from servers

function Enforce-MFA($exclude){
Connect-MsolService
$excludedUsers = 'admin','admin2','admin3','admin4' + $exclude
$excluded = ($excludedUsers | ForEach-Object { [regex]::Escape($_) }) -join '|'
$st = New-Object -TypeName Microsoft.Online.Administration.StrongAuthenticationRequirement
$st.RelyingParty = "*"
$st.State = "Enforced"
$sta = #($st)
$array = (Get-MsolUser | Where-Object { $_.DisplayName -notmatch $excluded }).UserPrincipalName
ForEach ($user in $array)
{
Set-MsolUser -UserPrincipalName $user -StrongAuthenticationRequirements $sta
Write-Host "Complete"
}
}
The general function is to grab a list of objects, exclude certain objects, and Enforce MFA for the remaining objects. This script seemed to work without any issue last week, but this week, I'm getting no data from the Array variable. I was working on a lot of different changes and I'm thinking I may have messed something up in the process, but I'm just not seeing it. What did I mess up or what am I not seeing?
You forgot to add -Credential $cred to the Connect-MsolService.
You should create that connection first and take it out of the function.
function Enforce-MFA {
$excludeTheseUsers = 'admin', 'user1', 'user2' # etc.
# for using the regex `-notmatch` operator later, you need to combine the entries with the regex OR sign ('|'),
# but you need to make sure to escape special characters some names may contain
$excludes = ($excludeTheseUsers | ForEach-Object { [regex]::Escape($_) }) -join '|'
# create the StrongAuthenticationRequirement object just once, to use on all users
$st = New-Object -TypeName Microsoft.Online.Administration.StrongAuthenticationRequirement
$st.RelyingParty = "*"
$st.State = "Enabled"
$sta = #($st)
# get an array of UserPrincipalNames
$array = (Get-MsolUser | Where-Object { $_.DisplayName -notmatch $excludes }).UserPrincipalName
foreach ($user in $array) {
Set-MsolUser -UserPrincipalName $user -StrongAuthenticationRequirements $sta
}
Write-Host "Enforcing MFA Complete"
}
# ask for credentials to make the connection
$cred = Get-Credential -Message 'Please enter your credentials to connect to Azure Active Directory'
Connect-MsolService -Credential $cred
As for your loop, try something like this:
# enter an endless loop
while($true) {
$var = Read-Host -Prompt "Enter the corresponding number: 1: Enforce 2: Enable 3: Disable 4: Exit"
switch($var){
1 { Enforce-MFA }
2 { Enable-MFA }
3 { Disable-MFA }
4 { exit }
default{ "Please choose either 1, 2 ,3 or 4" }
}
}

Can't get it to provide proper output

I have this function and I can not get it to work, the $DecimalConversion output is not coming out.. I think I am having some syntax errors.
function Get-DecimalNumber(){
$FileCheck = Test-Path "C:\Conversions\conversions.csv"
if($FileCheck){
Do
{
[int]$GetDecimal = Read-host "Write a number between 1-255" | Out-Null
}
while ($GetDecimal -notmatch '\d{1,3}' -or (1..255) -notcontains $GetDecimal)
$DecimalConversion= "{0:X}" -f $GetDecimal
$DecimalConversion
}
else{Write-Warning "Can not find conversions.csv, creating now under C:\Conversions\"; New-Item "C:\Conversions\conversions.csv" -Force | Out-Null}
}
$getfunction=Get-DecimalNumber
You could probably use a better while condition. However, ur issue is caused because of the out-null cmdlet on read-host.
If you use that, $GetDecimal will not get the value you pass in since the out-null is processed before the assignment happens. Just remove it. And it should work.
Final code, I think this looks better, let me know what you think!
function Get-DecimalNumber {
<#
.Description
The Get-DecimalNumber function gets user input for a decimal number and
converts it into hexadecimal and binary numbers. Then this data is added to an
excel file (.csv) and the date of conversion is displayed in short form m/d/yyyy.
#>
$ErrorActionPreference = 'silentlycontinue' #Silences errors
$Test = Test-Path "C:\temp\test\conversions.csv" #Variable to test path
if (! $Test) { #Checking if path does not exist
Write-Warning "conversions.csv File Not Present, creating under C:\temp\test\"
New-Item 'C:\temp\test\conversions.csv' -Force | Out-Null; break; exit #Creating path with file & suppressing output
}
else {
[int]$Num = Read-Host "Enter number from 1-255"
if ($Num -gt 255 -or $Num -le 1) {
Write-Warning "You did not enter a number in the specified range"; break; exit
}
else {
$Hex = [Convert]::ToString($Num, 16) #Converting from decimal to hexadecimal
$Bin = [Convert]::ToString($Num, 2) #Converting from decimal to binary
Write-Host "Decimal to Hex and Binary:"
$NewHashTable1 = #{ } #Creating hashtable
$NewHashTable1.Add('Decimal', $Num) #Adding values from variables to hash table
$NewHashTable1.Add('Hexadecimal', $hex)
$NewHashTable1.Add('Binary', $bin)
$NewHashTable1 #Output to screen the previously created hashtable
$NewHashTable1 >> "C:\temp\test\conversions.csv" #Appending hashtable to .csv file
Write-Output "'n"
Get-Date -Format d #Output date in short format
$Now = Get-Date -Format d
$Now >> "C:\temp\test\conversions.csv" #Output date to .csv file
}
}
}

Powershell scripts work when run directly but not when called by another

I have a long list of simple jobs I would like to somewhat automate. It's simple stuff, grab or post info via API and build some reports, nothing fancy.
I decided to build a master script which directs out to a variety of other scripts, each handling its own job. Each one of those little scripts, reference functions from a Utility script which I built that has functions which are common to all the other simple job scripts.
Each of the scripts work perfectly when I run them directly, however, when I try to run them via the master script, which routes to them, they all fail.
One example is that in many cases I need to fetch data from an API but get capped at 1000 object returns when I need 10k+. To solve this, I built a function which recursively calls itself until there is no more data left to collect. Again, this works when called by itself but not from the master script, for some reason, it bails out after the first run (should run 10+ times in this case). Then, it returns nothing.
I am thinking maybe this has something to do with how I am scoping the functions/variables?? Not sure. I have tried scoping to Global, Local & Script but none seem to work. Here's some of the code...
*Master Director Script runs script based on user input*
...
&$choice_hash[$action].script_path
$ScriptDirectory = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
. "$ScriptDirectory\Utilities.psm1"
$user_data = $null
$env_choice = $null
$csv_output_path = $null
$collated_user_data = [System.Collections.ArrayList]#()
function selectEnv {
$global:env_choice = Read-Host #"
> Select an Environment: [Prod] or [Dev]
Your Choice
"#
if ($env_choice -ne 'Prod' -and $env_choice -ne 'Dev') {
consoleCmt $env_choice
consoleCmt 'Invalid Choice. Try again...'
selectEnv
} else {
if ($env_choice -eq 'Prod') {
$global:csv_output_path = '\\etoprod\******\Exports\Report_Users_Prod.csv'
} else {
$global:csv_output_path = '\\etoprod\******\Exports\Report_Users_Dev.csv'
}
$global:user_data = process_data $env_choice 'api/xm/1/people?embed=roles&limit=1000'
}
}
function processUsersData {
foreach($user in $user_data) {
$user_roles = ''
$role_divider = ','
for($i = 0; $i -lt $user.roles.data.length; $i++) {
# Only append a comma if there are more, otherwise leave blank for CSV deliniation
if ($i -eq $user.roles.data.length - 1) {
$role_divider = ''
}
$user_roles += $user.roles.data[$i].name + $role_divider
}
# Build ordered hash table with above data
$sanatized_user = [pscustomobject][ordered]#{id = $user.targetName; firstName = $user.firstName; lastName = $user.lastName; siteName = $user.site.name; roles = $user_roles }
# Shovel into storage array used for building the CSV
$global:collated_user_data += $sanatized_user
}
}
notice 'Initiating Groups Report Script'
selectEnv
processUsersData
exportCsv $collated_user_data $csv_output_path
Utility Script (relevant functions being called)
$res = $null
$content = #()
...
function process_data($env, $url) {
fetch_data $env $url
foreach($i in $res.data) {
$global:content += $i
}
if($res.links.next) {
fetch_more $env $res.links.next
}
return $content **Should return full collection of data, but fails after one pass**
}
function fetch_data($env, $url) {
$base = generateEnvBase $env
$path = "$base/$url"
$req = Invoke-WebRequest -Credential $cred -Uri $path -Method GET
$global:res = ConvertFrom-Json $req
}
function fetch_more($env, $url) {
$base = generateEnvBase $env
$path = "$base$url"
$req = Invoke-WebRequest -Credential $cred -Uri $path -Method GET
$res = ConvertFrom-Json $req
foreach($i in $res.data) {
$global:content += $i
}
if($res.links.next) {
fetch_more $env $res.links.next
}
}
Sorry in advance if I have not followed procedure or etiquette, I'm new here.
This should work if you declare all variables in Main.ps1 that are needed by functions. You could also use the "Script" scope when creating a new variable inside a function that you want to use outside the function. Example $Script:Var = "Stuff" created inside a function will be available to whole script.
Directory Structure
C:\Script\Root
| Main.ps1
\---Utilities
fetch_data.ps1
fetch_more.ps1
processUsersData.ps1
process_data.ps1
selectEnv.ps1
Main.ps1
#---[ Initization ]---#
# Strings
[String]$RootPath = $PSScriptRoot
[String]$UtilPath = "$($RootPath)\Utilities"
[String]$env_choice = $null
[String]$csv_output_path = $null
# Arrays
[Array]$user_data = #()
[Array]$content = #()
[Array]$collated_user_data = #()
[Array]$res = #()
#---[ Source in Utilities ]---#
# Get the scripts
$Utilities = Get-ChildItem -Path "$UtilPath" -File | Where-Object {$_.Extension -eq ".ps1"}
# Source in each one
foreach ($Item in $Utilities) {
.$Item.FullName
}
#---[ Select an Environment ]---#
# Get the User's choice
$env_choice = selectEnv
# Process the choice
switch ($env_choice) {
Prod {
$csv_output_path = '\\etoprod\******\Exports\Report_Users_Prod.csv'
$user_data = process_data 'Prod' 'api/xm/1/people?embed=roles&limit=1000'
}
Dev {
$csv_output_path = '\\etoprod\******\Exports\Report_Users_Dev.csv'
$user_data = process_data 'Dev' 'api/xm/1/people?embed=roles&limit=1000'
}
Test {
Write-Output "Test is not an option. Choose wisely."
exit 1
}
Default {
Write-Output "Unknown Environment Choice."
exit 1
}
}
#---[ Process Users and Export ]---#
processUsersData
exportCsv $collated_user_data $csv_output_path
selectEnv.ps1
function selectEnv {
$Title = "Environment:"
$Info = "Please choose an environment"
# Options
$Prod = New-Object System.Management.Automation.Host.ChoiceDescription '&Prod', 'Production environment'
$Dev = New-Object System.Management.Automation.Host.ChoiceDescription '&Dev', 'Development environment'
$Test = New-Object System.Management.Automation.Host.ChoiceDescription '&Test', 'Testing environment'
$Options = [System.Management.Automation.Host.ChoiceDescription[]]($Prod, $Dev, $Test)
$Default = 0
# Promp the User
$Choice = $host.UI.PromptForChoice($Title , $Info , $Options, $Default)
$Result = $Options[$Choice].Label -Replace '&',''
return $Result
}

Get AD distinguished name

I'm trying to take input from a CSV file, which has a list of group names (canonical names) and get the Distinguished Name from it, then output to another CSV file. The code:
#get input file if passed
Param($InputFile)
#Set global variable to null
$WasError = $null
#Prompt for file name if not already provided
If ($InputFile -eq $NULL) {
$InputFile = Read-Host "Enter the name of the input CSV file (file must have header of 'Group')"
}
#Import Active Directory module
Import-Module -Name ActiveDirectory -ErrorAction SilentlyContinue
$DistinguishedNames = Import-Csv -Path $InputFile -Header Group | foreach-Object {
$GN = $_.Group
$DN = Get-ADGroup -Identity $GN | Select DistinguishedName
}
$FileName = "RESULT_Get-DistinguishedNames" + ".csv"
#Export list to CSV
$DNarray | Export-Csv -Path $FileName -NoTypeInformation
I've tried multiple solutions, and none have seemed to work. Currently, it throws an error because
Cannot validate argument on parameter 'Identity'. The argument is null. Supply a non-null argument and try the command again.
I tried using -Filter also, and in a previous attempt I used this code:
Param($InputFile)
#Set global variable to null
$WasError = $null
#Prompt for file name if not already provided
If ($InputFile -eq $NULL) {
$InputFile = Read-Host "Enter the name of the input CSV file(file must have header of 'GroupName')"
}
#Import Active Directory module
Import-Module -Name ActiveDirectory -ErrorAction SilentlyContinue
$DistinguishedNames = Import-Csv -Path $InputFile | foreach {
$strFilter = "*"
$Root = [ADSI]"GC://$($objDomain.Name)"
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher($root)
$objSearcher.Filter = $strFilter
$objSearcher.PageSize = 1000
$objsearcher.PropertiesToLoad.Add("distinguishedname") | Out-Null
$objcolresults = $objsearcher.FindAll()
$objitem = $objcolresults.Properties
[string]$objDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
[string]$DN = $objitem.distinguishedname
[string]$GN = $objitem.groupname
#Get group info and add mgr ID and Display Name
$props = #{'Group Name'= $GN;'Domain' = $objDomain;'Distinguished Name' = $DN;}
$DNS = New-Object psobject -Property $props
}
$FileName = "RESULT_Get-DistinguishedNames" + ".csv"
#Export list to CSV
$DistinguishedNames | Sort Name | Export-Csv $FileName -NoTypeInformation
The filter isn't the same one I was using here, I can't find the one I was using, the I currently have is a broken attempt.
Anyway, the main issue I was having is that it will get the group name, but search for it in the wrong domain (it wouldn't include Organizational Units) which caused none of them to be found. When I search for a group in PowerShell though (using Get-ADGroup ADMIN) they show up with the correct DN and everything. Any hints or code samples are appreciated.
You seemingly miss the point of $variable = cmdlet|foreach {script-block} assignment. The objects to assign to $variable should be returned (passed through the script block) in order to end up in $variable. Both your main loops contain the structure of the line $somevar=expectedOutput where expectedOutput is either a New-Object psobject or Get-ADGroup call. The assignment to $someVar suppresses the output, so that the script block does not have anything to return, and $variable remains null. To fix, do not prepend the call that should return an object into outside variable with an assignment.
$DistinguishedNames = Import-Csv -Path $InputFile -Header Group | foreach-Object {
$GN = $_.Group
Get-ADGroup -Identity $GN | Select DistinguishedName # drop '$DN=`
}
$DistinguishedNames | Export-CSV -Path $FileName -NoTypeInformation
The same issue with the second script.