├── .cspell.json ├── .editorconfig ├── .gitattributes ├── .github ├── .vscode │ ├── extensions.json │ ├── settings.json │ └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── feature_request.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── copilot-instructions.md ├── instructions │ └── codacy.instructions.md ├── workflow-scripts │ └── Get-RepositoryInfo.ps1 └── workflows │ ├── GitGuardian.yml │ ├── MegaLinter.yml │ └── PSScriptAnalyzer.yml ├── .gitignore ├── Active Directory ├── AD DS Deployment.ps1 ├── AD Groups │ ├── Archive-ObsoleteGroups.ps1 │ ├── Copy-GroupMembership.ps1 │ ├── Get Foreign Security Principals in Groups.ps1 │ ├── Get-EmptyADGroups.ps1 │ ├── Get-GroupFspMember.ps1 │ ├── Get-GroupReport.ps1 │ ├── Get-SpecialGroups.ps1 │ ├── Get-UnusedGroup.ps1 │ ├── New-ADGroupsFromCsv.ps1 │ ├── Remove Disabled Computer from All Groups Except Domain Computers.ps1 │ └── Remove-AllGroupMembers.ps1 ├── AD Users │ ├── Convert Mixed-Case Usernames to Lowercase.ps1 │ ├── Export-Managers.ps1 │ ├── Find 'Do Not Require Kerberos Preauthentication' .ps1 │ ├── Get Users with Logon Scripts.ps1 │ ├── Get Users with Whitespace in Mail Attribute.ps1 │ ├── Get-ADDirectReport.ps1 │ ├── Get-ADUserEncryptionTypes.ps1 │ ├── Get-AccountsWithPasswordNeverExpires.ps1 │ ├── Get-InactiveUsers.ps1 │ ├── Get-LockedOutLocation.ps1 │ ├── Remove-ADGroupMembership.ps1 │ ├── Remove-DisabledUsersFromAllGroups.ps1 │ ├── Search-KerbDelegatedAccounts.ps1 │ ├── Set User EmailAddress from PrimaryAddress.ps1 │ ├── Set-ADUserUPNSuffix.ps1 │ ├── Test-IsMemberOfProtectedUsers.ps1 │ ├── Test-UsernameConventionMatch.ps1 │ └── Working with Custom Extension Attributes.ps1 ├── Add-ComputerToGpo.ps1 ├── Convert-SID.ps1 ├── Domain Services │ ├── AD ACL Scanner 2.0.3.ps1 │ ├── AD Permissions Class Types.ps1 │ ├── Add-ADSubnets.ps1 │ ├── Check AD Domain Permissions.ps1 │ ├── Check all DCs.ps1 │ ├── Checking and Setting AD Schema Attributes.ps1 │ ├── Collect-MissingADSubnets.ps1 │ ├── Create Missing AD Sites.ps1 │ ├── DCDIAG Complete Testing.ps1 │ ├── DNSZonesRemote.ps1 │ ├── Enable-ADRecycleBin.ps1 │ ├── Export-ADSchema.ps1 │ ├── Get-ADForestBackups.ps1 │ ├── Get-ADSitesInDefaultLink.ps1 │ ├── Get-ADSitesWithoutLink.ps1 │ ├── Get-DCTimeConfig.ps1 │ ├── Get-DomainControllerLastBackup.ps1 │ ├── Get-FSMORoleDetails.ps1 │ ├── Get-MissingADSubnet v2.ps1 │ ├── Get-TrustedDomainNetBIOSNames.ps1 │ ├── Invoke-DcDiag.ps1 │ ├── Protect All OUs from Accidental Deletion.ps1 │ ├── Query AD Object GUID Names.md │ ├── Set-ADFunctionalLevels.ps1 │ └── Test-DNSName.ps1 ├── Export-AllADUserTransitiveGroupMemberships.ps1 ├── Export-AllGroupMemberships.ps1 ├── Find-DeepestOU.ps1 ├── Get-ADAttributeUniqueValues.ps1 ├── Get-ADGroupForeignMembers.ps1 ├── Get-ADObjectFromPipeline.ps1 ├── Get-ADUserTransitiveGroupMembership.ps1 ├── Get-GPMissingPermissionGPO.ps1 ├── Get-GPOsMissingPermission.ps1 ├── Get-GpoGroupsToRename.ps1 ├── Get-LAPSPasswords.ps1 ├── Get-MsDsParentDistinguishedName.ps1 ├── Get-OUDetail.ps1 ├── Get-OrganizationalUnitDepth.ps1 ├── Get-OverlappingOUName.ps1 ├── MapGuidsToGpoNames.ps1 ├── No-RSAT │ └── README.md ├── README.md ├── Rename-GPOsByCSV.ps1 ├── Rename-GroupsByCsv.ps1 ├── Resolve SID in Event Logs.ps1 ├── SIDHistory │ ├── Get-ADObjectWithSIDHistory.ps1 │ ├── Get-AllADSIDHistoryDetail.ps1 │ ├── Get-AllADSIDHistorySourceDomain.ps1 │ ├── Get-SIDHistorySourceDomainList.ps1 │ └── Get-TrustedDomainSIDMapping.ps1 ├── Set DNS Server Zone Settings via Registry.ps1 └── Test-IsWellKnownSID.ps1 ├── Citrix └── Reset-CitrixWorkspace.ps1 ├── DDI ├── DNS Reverse Lookup from CSV and Add Column with Hostname.ps1 ├── Get Hostnames from CSV IP Addresses.ps1 ├── Get-DNSHostEntryAsync.ps1 ├── Get-DhcpOptionUsed.ps1 ├── Get-DhcpScopeOptions.ps1 ├── Get-DomainSubdomains.ps1 ├── Get-ServiceRecords.ps1 ├── Invoke-ReverseDnsLookup.ps1 ├── Measure-DnsServerResponse.ps1 ├── README.md ├── Remove-DhcpAllLeases.ps1 ├── Resolve-IPs.ps1 ├── Reverse DNS Record Validation.ps1 ├── Update-DnsServerList.ps1 └── Validate IP Address.ps1 ├── Defender └── MDI │ ├── Disable-NetAdapterLso.ps1 │ ├── Install-MDI.ps1 │ ├── README.md │ └── Remove-AtaSensor.ps1 ├── Entra ├── Get Entra Applications.ps1 ├── Get License Utilization Report.ps1 ├── Get-DSReg.ps1 ├── Get-EntraTenantInfo.ps1 ├── Get-MicrosoftEndpoints.ps1 ├── Install Graph Modules for Identity.ps1 ├── M365 Service URLs.ps1 ├── README.md ├── Remove Entra ID Duplicate Devices - Windows.ps1 ├── Test-EntraModule.ps1 └── Test-Microsoft.Entra-Module.ps1 ├── Exchange ├── Add-EmailAddressDomain.ps1 ├── Basic Exchange Health Checks.ps1 ├── Exchange Org Config Starter.ps1 ├── Get Email Addresses with a Digit Before the At Sign.ps1 ├── Get Receive Connectors with External Relay Permissions.ps1 ├── Get-AliasConflicts.ps1 ├── Get-ExchangeVirtualDirectories.ps1 ├── Get-MailboxDatabaseSize.ps1 ├── MailboxPermissionsAllFolders.ps1 ├── Parse-TransportLogs.ps1 ├── README.md ├── Remove Disabled Users from Distribution Groups.ps1 ├── Remove Old Exchange Server Log Files.ps1 ├── Rename-RecipientByCsv.ps1 ├── Reset-HealthMailboxes.md ├── Set-ExchangeURLs.ps1 ├── Set-JunkMailSettings.ps1 ├── StartMaintenance.ps1 ├── StopMaintenance.ps1 └── Update-DefaultSmtpAddress.ps1 ├── General ├── Copy-ToVMwareGuest.ps1 ├── Expand-ShortUrl.ps1 ├── ExperimentalFeatures.ps1 ├── Get Latest PowerShell Download URLs.ps1 ├── Get-EnvironmentVariable.ps1 ├── Get-ModuleImportCandidate.ps1 ├── Get-ModulesWithUpdate.ps1 ├── Get-NtpSourceType.ps1 ├── Get-PatchTuesday.ps1 ├── Get-PathDepth.ps1 ├── Get-PingResultsHashTable.ps1 ├── Install-PowerShellAsDotNetTool.ps1 ├── Measure-CommandInventory.ps1 ├── New-Function.ps1 ├── Purge-InstalledModules.ps1 ├── Save-MaesterOffline.ps1 ├── Set-EnvironmentVariable.ps1 ├── Show-BuildMermaid.ps1 ├── Test-IsLocalAccountSession.ps1 ├── Test-IsPalindrome.ps1 ├── Test-ModuleInstall.ps1 ├── Test-NonInteractive.ps1 └── Update-ModuleVersion.ps1 ├── Microsoft 365 ├── .codacy │ ├── cli.sh │ └── codacy.yaml ├── Check Teams Client cloudEnvironment.ps1 ├── Check for new CSV of Microsoft product names and service plan identifiers.ps1 ├── Connect-ExchangeOnlineWorkaround.ps1 ├── Get Viva Insights Users with no Manager.ps1 ├── Get-UserTeamsMembership.ps1 ├── GetAllTenantUsers.ps1 ├── Intune │ └── Uninstall-AppsViaIntune.ps1 ├── New-TeamsFromManagers.ps1 ├── Power Apps │ └── Expression to Build a Planner Task URL.txt ├── README.md ├── Set-EXOExternalEmailWarnings.ps1 ├── Setup-SensitivityLabels.ps1 ├── Test-MaesterSuite.ps1 └── Update-Office-Apps.ps1 ├── Opsgenie └── Get-OnCallReport.ps1 ├── Profile and Prompt ├── Check for VSCode in Script or Profile.ps1 ├── Comply │ ├── README.md │ ├── comply-example-admin-user.png │ ├── comply-example-standard-user.png │ ├── comply.omp.json │ └── comply.png ├── PSProfileBase.ps1 ├── Simple prompt with history.ps1 └── task - Edit PowerShell Profiles.json ├── README.md ├── Script Logging ├── Transcripting.ps1 ├── Transcription.ps1 └── Write-This.ps1 ├── Snippets ├── Average Time to Run a Command.ps1 ├── Check if a key exists in a hash table.ps1 ├── Compare Two Files with Code.ps1 ├── Custom Object Examples.ps1 ├── Error Information.ps1 ├── Examples of Using .NET Types.ps1 ├── ForEach Differences.ps1 ├── Get Patch Tuesday Variations.ps1 ├── Get Script Path, Name.ps1 ├── Get-Command Examples.ps1 ├── Get-InputProperties.ps1 ├── Get-PSUptime.ps1 ├── How to Catch Multiple Exception Types.ps1 ├── How to Split String into Two Variables.ps1 ├── Install Multiple Modules at Once.ps1 ├── Level-Up Notes │ ├── Strings - Split.ps1 │ └── Strings.ps1 ├── New Variable Notes.ps1 ├── Podcast Examples.ipynb ├── PowerShell Scratchpad.ipynb ├── Predictive PSReadLine.md ├── Relaunch Script as Admin.ps1 ├── RemoteServerSessionLoop2.ps1 ├── Resolve SID in Event Logs.ps1 ├── Restart a script as administrator.ps1 ├── Return Multiple Objects from Function.ps1 ├── Run a Job in the Background and Stream Output to Host.ps1 ├── Show-ExampleStatusWithStopwatch.ps1 ├── Storing Functions in Hash Tables.ps1 ├── StringBuilder Examples.ps1 ├── Test-Administrator.ps1 ├── Test-IsLocalAdmin.ps1 ├── Test-Obsolete.ps1 ├── Test-RedText.ps1 ├── Test-Type.ps1 ├── Update PSReadLine.md ├── Use TLS 1.2 Instead of Older Versions.ps1 ├── Use TLS in Scripts.ps1 ├── Using NET Types in PS.ipynb ├── Using New-Variable.ipynb └── Ways to Suppress Output.ps1 └── Windows ├── Activate and Get License.ps1 ├── Clear-QuickAccessList.ps1 ├── Configure-TLS.ps1 ├── ConvertTo-StringFromCertificate.ps1 ├── Copy-ToPSSession.ps1 ├── Disable NetBIOS per NIC.ps1 ├── Disable-Poshv2.ps1 ├── Disable-QuickAccessList.ps1 ├── Get-BITSDownload.ps1 ├── Get-FileHashes.ps1 ├── Get-NtpClientConfig-Alt (WIP).ps1 ├── Get-NtpClientConfig.ps1 ├── Get-SharesAndPermissions.ps1 ├── IISLogsCleanup.ps1 ├── New-gMSA.ps1 ├── Push-DNSClientServerAddresses.ps1 ├── Set-DefaultPrinter.ps1 ├── Set-DefaultPrinter.tests.ps1 ├── Test-PendingReboot.ps1 ├── Test-WinREEnabled.ps1 └── Update-SysInternals.ps1 /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": [ 3 | "@cspell/dict-powershell/cspell-ext.json", 4 | "@cspell/dict-csharp/cspell-ext.json" 5 | ], 6 | "version": "0.2", 7 | "language": "en", 8 | "words": [ 9 | "RSAT" 10 | ], 11 | "ignoreWords": [ 12 | "stefanzweifel", 13 | "diffy" 14 | ], 15 | "patterns": [ 16 | { 17 | "name": "ALL-CAPS-WORDS", 18 | "pattern": "/\b[A-Z0-9]+\b/g", 19 | "description": "Any word in ALL CAPS." 20 | } 21 | ], 22 | "ignoreRegExpList": [ 23 | "ALL-CAPS-WORDS", 24 | "Email", 25 | "github.com/", 26 | "@" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | # Powershell files 14 | [*.{ps1,psd1,psm1}] 15 | indent_size = 4 16 | 17 | [*.md] 18 | indent_size = 2 19 | 20 | # Data serialization 21 | [*.{json,yaml,yml}] 22 | indent_size = 2 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Fix syntax highlighting on GitHub to allow comments 2 | .devcontainer.json linguist-language=JSON-with-Comments 3 | devcontainer.json linguist-language=JSON-with-Comments 4 | 5 | # Apply override to all files in the directory 6 | *.md linguist-detectable 7 | 8 | # Basic .gitattributes for a PowerShell repo. 9 | # Source files 10 | # ============ 11 | *.ps1 text eol=crlf 12 | *.ps1x text eol=crlf 13 | *.psm1 text eol=crlf 14 | *.psd1 text eol=crlf 15 | *.ps1xml text eol=crlf 16 | *.pssc text eol=crlf 17 | *.psrc text eol=crlf 18 | *.cdxml text eol=crlf 19 | 20 | # Fix syntax highlighting on GitHub to allow comments 21 | .vscode/*.json linguist-language=JSON-with-Comments 22 | -------------------------------------------------------------------------------- /.github/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "ms-vscode.PowerShell", 6 | "EditorConfig.EditorConfig", 7 | "streetsidesoftware.code-spell-checker", 8 | "DavidAnson.vscode-markdownlint", 9 | "SamErde.pspreworkout-powershell-extensions-pack" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.github/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // When enabled, will trim trailing whitespace when you save a file. 3 | "files.trimTrailingWhitespace": true, 4 | // specifies the location of the explicitly ScriptAnalyzer settings file 5 | "powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1", 6 | // specifies the PowerShell coding style used in this project (https://github.com/PoshCode/PowerShellPracticeAndStyle/issues/81) 7 | "powershell.codeFormatting.preset": "OTBS", 8 | "cSpell.words": [ 9 | "Preworkout" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in contributing to this repository! 4 | 5 | Whether it's a bug report, new feature, correction, or additional documentation, your feedback and contributions are appreciated. 6 | 7 | Please read through this document before submitting any issues or pull requests to ensure all the necessary information is provided to effectively respond to your bug report or contribution. 8 | 9 | Please note there is a code of conduct, please follow it in all your interactions with the project. 10 | 11 | ## Contributing via Pull Requests 12 | 13 | If you have questions about how to submit a PR, I would be more than happy to walk you through the steps over in the [discussions](https://github.com/SamErde/PowerShell/discussions)! 14 | 15 | ## Code of Conduct 16 | 17 | This project has a [Code of Conduct](CODE_OF_CONDUCT.md). 18 | 19 | ## Licensing 20 | 21 | See the [LICENSE](LICENSE) file for our project's licensing. 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: SamErde # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: SamErde # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: SamErde # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Submit a new bug 4 | title: '🪲 Bug report' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ### Expected Behavior 13 | 14 | 15 | ### Current Behavior 16 | 17 | 18 | ### Possible Solution 19 | 20 | 21 | ### Steps to Reproduce 22 | 23 | 24 | 25 | 1. 26 | 2. 27 | 3. 28 | 4. 29 | 30 | ### Context (Environment) 31 | 32 | 33 | * Operating System and version as reported by `$PSVersionTable.OS`: 34 | * PowerShell versions as reported by `$PSVersionTable.PSEdition`: 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Questions & Discussions ✍️ 4 | url: https://github.com/SamErde/PowerShell/discussions 5 | about: Please use GitHub discussions for any questions. 6 | # - name: Basic FAQs ❓ 7 | # url: https://github.com/SamErde/PowerShell/wiki/FAQs 8 | # about: Answers to the Basic Frequently Asked Questions 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '🙏 Feature request' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Description 11 | 12 | 13 | ### Describe the solution you'd like 14 | 15 | 16 | ### Describe any alternatives you've considered 17 | 18 | 19 | ### Additional context 20 | 21 | -------------------------------------------------------------------------------- /.github/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sam Erde 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request 2 | 3 | ## Type of Change 4 | 5 | - [ ] 📖 Documentation 6 | - [ ] 🪲 Fix 7 | - [ ] 🩹 Patch 8 | - [ ] ⚠️ Security Fix 9 | - [ ] 🚀 Feature 10 | - [ ] 💥 Breaking Change 11 | 12 | ## Issue 13 | 14 | 15 | ## Description 16 | 17 | 18 | ## Checklist 19 | 20 | - [ ] 🕵️ I have reviewed my code for errors and tested it. 21 | - [ ] 🚩 My pull request does not contain multiple types of changes. 22 | - [ ] 📄 By submitting this pull request, I confirm that my contribution is made under the terms of the projects associated license. 23 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | 8 | 9 | If you discover a vulnerability in PSPreworkout, please follow the _following process_: 10 | 11 | 1. Open a generic bug issue advising you have discovered a vulnerability. 12 | - Avoid sharing specifics or details of the vulnerability in an open GitHub issue. 13 | 2. A repo owner will reach out to you to establish a private form of communication. 14 | 3. We will evaluate the vulnerability and, if necessary, release a fix or mitigating steps to address it. We will contact you to let you know the outcome, and will credit you in the report. 15 | 16 | Please **do not disclose the vulnerability publicly** until a fix is released! 17 | 18 | 4. Once we have either a) published a fix, or b) declined to address the vulnerability for whatever reason, you are free to publicly disclose it. 19 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # PowerShell Coding Standards 2 | - Always use approved PowerShell verbs for function names (get, set, new, start, remove, update, etc.) 3 | - Use Pascal case for all function names, variables, and parameters 4 | - Follow OTBS (One True Brace Style) formatting 5 | - Include one blank line at the end of every script 6 | - Remove all trailing spaces 7 | - Use proper cmdlet binding and parameter validation 8 | - Always include comment-based help for functions 9 | 10 | # General Coding Guidelines 11 | - Always add meaningful comments for complex logic 12 | - Prefer explicit error handling over silent failures 13 | - Include unit tests for all new functions 14 | 15 | # Response Preferences 16 | - Include brief explanations of why a particular approach is recommended 17 | - When suggesting refactoring, explain the benefits 18 | - Provide both the solution and alternative approaches when applicable 19 | 20 | # Security Guidelines 21 | - Never hardcode credentials or API keys 22 | - Always validate input parameters 23 | - Implement proper authentication and authorization checks 24 | 25 | # PowerShell Commit Message Template 26 | 27 | Generate commit messages for PowerShell projects using this format: 28 | 29 | `[optional scope]: ` 30 | 31 | Follow the GitMoji specifications at for 32 | commit messages. Tailor commit messages for PowerShell development, using the provided types and scopes. 33 | 34 | ## PowerShell-Specific Types: 35 | - feat: ✨ New cmdlet, function, or module feature 36 | - fix: 🐛 Bug fix in PowerShell code 37 | - docs: 📚 Help documentation, comment-based help 38 | - style: 🎨 Code formatting, OTBS compliance, Pascal case fixes 39 | - refactor: ♻️ Code restructuring, approved verb compliance 40 | - test: ✅ Pester tests, unit tests 41 | - build: 🛠️ Module manifest, build scripts 42 | - ci: 🤖 Azure DevOps, GitHub Actions for PowerShell 43 | - chore: 🧹 Module organization, file cleanup 44 | - perf: ⚡ Performance improvements in cmdlets or functions 45 | - revert: ⏪ Reverting changes in PowerShell scripts or modules 46 | - packaging: 📦 Packaging changes, module version updates 47 | - security: 🔒 Security-related changes, input validation, authentication 48 | 49 | ## PowerShell Scopes: 50 | - module: Module-level changes 51 | - cmdlet: Specific cmdlet modifications 52 | - function: Function updates 53 | - help: Documentation changes 54 | - manifest: Module manifest updates 55 | - tests: Test-related changes 56 | 57 | ## Examples: 58 | ✨feat(cmdlet): add Get-UserProfile with parameter validation 59 | 🐛fix(function): resolve Invoke-ApiCall error handling 60 | 📚docs(help): update comment-based help for Set-Configuration 61 | 🎨style(module): apply OTBS formatting and Pascal case 62 | ✅test(cmdlet): add Pester tests for Get-SystemInfo 63 | -------------------------------------------------------------------------------- /.github/workflow-scripts/Get-RepositoryInfo.ps1: -------------------------------------------------------------------------------- 1 | function Get-RepositoryInfo { 2 | [CmdletBinding()] 3 | [OutputType([hashtable])] 4 | param ( 5 | ) 6 | 7 | $RepositoryRoot = [System.IO.Path]::Combine($PSScriptRoot, '..', '..') 8 | 9 | [PSCustomObject]$Info = @{ 10 | NumberOfScripts = (Get-ChildItem $RepositoryRoot -Filter *.ps1 -File -Recurse).Count 11 | NumberOfFOlders = (Get-ChildItem $RepositoryRoot -Directory -Recurse).Count 12 | Categories = (Get-ChildItem -Path $RepositoryRoot -Directory -Exclude '.github').Name 13 | CategoryList = (Get-ChildItem -Path $RepositoryRoot -Directory -Exclude '.github').Name -replace 'DDI', 'DDI (DNS, DHCP, IPAM)' -join ', ' 14 | } 15 | 16 | $Info 17 | } 18 | 19 | Get-RepositoryInfo 20 | -------------------------------------------------------------------------------- /.github/workflows/GitGuardian.yml: -------------------------------------------------------------------------------- 1 | # GitGuardian 2 | --- 3 | name: GitGuardian 4 | 5 | on: [push] 6 | 7 | jobs: 8 | scanning: 9 | name: GitGuardian Scan 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 # fetch all history so multiple commits can be scanned 16 | - name: GitGuardian Scan 17 | uses: GitGuardian/ggshield/actions/secret@v1.43.0 18 | env: 19 | GITHUB_PUSH_BEFORE_SHA: ${{ github.event.before }} 20 | GITHUB_PUSH_BASE_SHA: ${{ github.event.base }} 21 | GITHUB_PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }} 22 | GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} 23 | GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }} 24 | -------------------------------------------------------------------------------- /.github/workflows/PSScriptAnalyzer.yml: -------------------------------------------------------------------------------- 1 | # PSScriptAnalyzer 2 | # https://github.com/microsoft/psscriptanalyzer-action 3 | # https://github.com/PowerShell/PSScriptAnalyzer 4 | --- 5 | name: PSScriptAnalyzer 6 | 7 | on: 8 | push: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | permissions: 16 | contents: read # for actions/checkout to fetch code 17 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 18 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 19 | name: 🕵️‍♂️ PSScriptAnalyzer 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: 🕵️‍♂️ Run PSScriptAnalyzer 25 | uses: microsoft/psscriptanalyzer-action@6b2948b1944407914a58661c49941824d149734f 26 | with: 27 | # The below set up runs PSScriptAnalyzer to your entire repository and runs some basic security rules. 28 | path: .\ 29 | recurse: true 30 | # Include your own basic security rules. Removing this option will run all the rules 31 | includeRule: '"PSAvoidGlobalAliases", "PSAvoidUsingConvertToSecureStringWithPlainText"' 32 | excludeRule: '"PSAvoidLongLines"' 33 | output: results.sarif 34 | 35 | # Upload the SARIF file generated in the previous step 36 | - name: ⬆️ Upload SARIF results file 37 | uses: github/codeql-action/upload-sarif@v3 38 | with: 39 | sarif_file: results.sarif 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/linux,macos,windows,powershell,visualstudiocode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,windows,powershell,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | # General 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### macOS Patch ### 49 | # iCloud generated files 50 | *.icloud 51 | 52 | ### VisualStudioCode ### 53 | .vscode/* 54 | !.vscode/settings.json 55 | !.vscode/tasks.json 56 | !.vscode/launch.json 57 | !.vscode/extensions.json 58 | !.vscode/*.code-snippets 59 | 60 | # Local History for Visual Studio Code 61 | .history/ 62 | 63 | ### VisualStudioCode Patch ### 64 | # Ignore all local history of files 65 | .history 66 | .ionide 67 | 68 | ### Windows ### 69 | # Windows thumbnail cache files 70 | Thumbs.db 71 | Thumbs.db:encryptable 72 | ehthumbs.db 73 | ehthumbs_vista.db 74 | 75 | # Dump file 76 | *.stackdump 77 | 78 | # Folder config file 79 | [Dd]esktop.ini 80 | 81 | # Recycle Bin used on file shares 82 | $RECYCLE.BIN/ 83 | 84 | # Windows Installer files 85 | *.msi 86 | *.msix 87 | *.msm 88 | *.msp 89 | 90 | # Windows shortcuts 91 | *.lnk 92 | 93 | # Code Analysis Tools 94 | .codacy/* 95 | 96 | # End of https://www.toptal.com/developers/gitignore/api/linux,macos,windows,powershell,visualstudiocode 97 | 98 | 99 | #Ignore vscode AI rules 100 | .github\instructions\codacy.instructions.md 101 | -------------------------------------------------------------------------------- /Active Directory/AD Groups/Archive-ObsoleteGroups.ps1: -------------------------------------------------------------------------------- 1 | # ============================================================ 2 | # Script Information 3 | # 4 | # Title: Obsolete AD Group Archiver 5 | # Author: Sam Erde 6 | # 7 | # Created: 11/04/2014 8 | # Description: Read a list of obsolete groups from a text file, export the members to a separate text file for each group, 9 | # and then empty the obsolete groups. They can be deleted after a week or two to prove they are no longer used. 10 | # The empty groups are also moved to the "Obsolete Groups" OU. 11 | # 12 | # DO NOT RUN TWICE ON THE SAME FILE OR THE ARCHIVE WILL BE OVERWRITTEN! 13 | # 14 | # To Do: 15 | # Add error handling 16 | # Prompt for a job name at each run so their is a separate archive folder for each job to help prevent an archive from being overwritten. 17 | # Add handling of group names to it can discover DNs if needed. This may require a specific format within the input file, such as group DNs there. 18 | # 19 | # ============================================================ 20 | 21 | #Import the Active Directory module so we can work with AD groups. 22 | Import-Module ActiveDirectory 23 | 24 | #Set the Active Directory server name that will be used. Using a serverless domain name here may also work. 25 | $Domain = '' 26 | 27 | #Read in the CSV or text file of group names. 28 | $File = Get-Content -Path C:\Scripts\ObsoleteGroups\ObsoleteGroups.csv 29 | 30 | #Loop through each line of the text file and run the following commands for each line: 31 | Foreach ($Group in $File) { 32 | #Get the members of each group (recursively in case groups are nested) in the specified domain or domain controller. 33 | #Select the name of each member within the group and then write each name to a CSV file. Each CSV file is named with the name of each security group. 34 | Get-ADGroupMember -Server $Domain -Identity $Group -Recursive | Export-Csv -Path "C:\Scripts\ObsoleteGroups\Archive\$group.csv" -NoTypeInformation 35 | 36 | <# * * * * * * * * * * 37 | This section will require special customization until we further develop the script to pull the full group DN. 38 | In the interest of time today, I have hard coded some of the information. 39 | * * * * * * * * * * 40 | /#> 41 | .\Remove-AllGroupMembers.ps1 -group "CN=$Group" -ou 'OU=' -domain 'DC=' 42 | Move-ADObject -Server $Domain -Identity 'CN=ps,DC=' -TargetPath '' 43 | } 44 | 45 | #Copy and rename the CSV file with a timestamp to keep as a record of run history. 46 | $timeStamp = Get-Date -Format 'yyyy-MM-dd hh-m-ss' 47 | Copy-Item -Path C:\Scripts\ObsoleteGroups\ObsoleteGroups.csv -Destination "C:\Scripts\ObsoleteGroups\Run History\ObsoleteGroups $timeStamp.csv" 48 | -------------------------------------------------------------------------------- /Active Directory/AD Groups/Copy-GroupMembership.ps1: -------------------------------------------------------------------------------- 1 | # This is ancient. I need to rewrite this, wherever it came from! 2 | Import-Module ActiveDirectory 3 | Get-ADGroupMember 'SourceGroupA-sAMAccountName' | ForEach-Object { 4 | Add-ADGroupMember -Identity 'DestinationGroupB-sAMAccountName' -Members $_ 5 | } 6 | -------------------------------------------------------------------------------- /Active Directory/AD Groups/Get Foreign Security Principals in Groups.ps1: -------------------------------------------------------------------------------- 1 | # List all foreign security principals in Active Directory that are a member of any group 2 | $FSPContainer = $Domain.ForeignSecurityPrincipalsContainer 3 | Get-ADObject -Filter 'ObjectClass -eq "foreignSecurityPrincipal"' -Properties 'msds-principalname', 'memberof' -SearchBase $FSPContainer -Server $GlobalCatalog | 4 | Where-Object { $_.memberof -ne $null } | ForEach-Object { 5 | $AllForeignSecurityPrincipalMembers.Add($_) 6 | } 7 | -------------------------------------------------------------------------------- /Active Directory/AD Groups/Get-EmptyADGroups.ps1: -------------------------------------------------------------------------------- 1 | Import-Module ActiveDirectory 2 | Get-ADGroup -Filter {GroupCategory -eq 'Security'} | Where-Object {@(Get-ADGroupMember $_).Length -eq 0} 3 | -------------------------------------------------------------------------------- /Active Directory/AD Groups/Get-GroupFspMember.ps1: -------------------------------------------------------------------------------- 1 | function Get-GroupFspMember { 2 | <# 3 | .SYNOPSIS 4 | Check Active Directory groups for members that are foreign security principals from other domains or forests. 5 | #> 6 | #Requires -Modules 'ActiveDirectory' 7 | 8 | Import-Module ActiveDirectory 9 | $Domain = Get-ADDomain -Current LocalComputer 10 | $DomainSID = $Domain.DomainSID.Value 11 | # Using a global catalog may be required for some queries to be comprehensive, but need to update to 12 | # handle child domains that do not have a global catalog. 13 | # [string]$DomainController = (Get-ADDomainController -DomainName $Domain.DnsRoot -Discover).HostName 14 | 15 | # Get all groups that are capable of containing foreign security principals. Ignore empty groups and global groups, which cannot contain members from other domains or forests. 16 | $Groups = Get-ADGroup -Properties members, Description -Filter 'GroupCategory -eq "Security" -and (GroupScope -eq "Universal" -or GroupScope -eq "DomainLocal") -and Members -like "*"' 17 | 18 | $GroupsWithForeignMembers = New-Object System.Collections.Generic.List[System.Object] 19 | 20 | foreach ($group in $Groups) { 21 | $FspMembers = $group.members | Where-Object { $_ -like 'CN=S-1-*' -and $_ -notlike "$DomainSID*" } 22 | if ($FspMembers.count -ne 0) { 23 | $tempgroup = New-Object -TypeName PSObject 24 | $tempgroup | Add-Member -MemberType NoteProperty -Name 'GroupDN' -Value $group.distinguishedName 25 | $tempgroup | Add-Member -MemberType NoteProperty -Name 'Description' -Value $group.Description 26 | $tempgroup | Add-Member -MemberType NoteProperty -Name 'FspMembers' -Value ($FspMembers -join (', ')) 27 | $GroupsWithForeignMembers.Add($tempgroup) 28 | } 29 | } 30 | $GroupsWithForeignMembers 31 | } 32 | -------------------------------------------------------------------------------- /Active Directory/AD Groups/Get-SpecialGroups.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Here's a cool idea for a function: 3 | 4 | Get the members of well known security groups by their non-localized SIDs. 5 | 6 | Get count of members in tier-1 groups 7 | Check if user [x] is a member of any tier-1 groups 8 | Check if user [x] is in a specific tier-1 group or well-known group 9 | 10 | #> 11 | $PrincipalContext = [System.DirectoryServices.AccountManagement.PrincipalContext]::new('domain',$domain) 12 | $DomainSID = [System.Security.Principal.SecurityIdentifier]::new($domain.GetDirectoryEntry().objectSid.Value,0) 13 | $GroupSID = [System.Security.Principal.SecurityIdentifier]::new("$($DomainSID.Value)-516") 14 | $GroupPrincipal = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($PrincipalContext,$GroupSID) 15 | $DomainControllerCount.Group += $GroupPrincipal.GetMembers($true).count 16 | -------------------------------------------------------------------------------- /Active Directory/AD Groups/Get-UnusedGroup.ps1: -------------------------------------------------------------------------------- 1 | Import-Module ActiveDirectory 2 | 3 | function Get-UnusedGroup { 4 | [CmdletBinding()] 5 | Param( 6 | [Parameter(Mandatory = $True)] 7 | [string]$SearchBase 8 | ) 9 | 10 | Get-ADGroup -Filter * -Properties members, isCriticalSystemObject -SearchBase $SearchBase | Where-Object { 11 | ($_.members.count -eq 0 ` 12 | -and !($_.IsCriticalSystemObject) -eq 1 ` 13 | -and $_.DistinguishedName -notmatch 'Exchange Security' ` 14 | -and $_.DistinguishedName -notmatch 'Exchange Install' ` 15 | -and $_.DistinguishedName -notmatch 'Builtin') 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Active Directory/AD Groups/Remove Disabled Computer from All Groups Except Domain Computers.ps1: -------------------------------------------------------------------------------- 1 | $Token = (Get-ADGroup 'Domain Computers' -Properties PrimaryGroupToken).PrimaryGroupToken 2 | 3 | Get-ADComputer -Filter 'Enabled -eq "False"' -SearchBase 'OU=Disabled Computers,...' -Properties PrimaryGroup, MemberOf | ForEach-Object { 4 | 5 | #If Computer Primary Group is not Domain Computers, then Set Domain Computers as Primary Group. 6 | If ($_.PrimaryGroup -notmatch 'Domain Computers') { 7 | Set-ADComputer -Identity $_ -Replace @{PrimaryGroupID = $Token } -Verbose 8 | } #If 9 | 10 | #If Computer is a member of more than 1 Group. Remove All Group except Domain Computers. 11 | If ($_.memberof) { 12 | $Group = Get-ADPrincipalGroupMembership -Identity $_ | Where-Object { $_.Name -ne 'Domain Computers' } 13 | Remove-ADPrincipalGroupMembership -Identity $_ -MemberOf $Group -Confirm:$false -Verbose 14 | } #If 15 | 16 | #Move Computer to Disabled OU. 17 | #Move-ADObject -Identity $_ -TargetPath "OU=Disabled Computers,OU=Disabled Accounts,DC=DOMAINNAME,DC=org" -Verbose 18 | 19 | } #Foreach 20 | -------------------------------------------------------------------------------- /Active Directory/AD Groups/Remove-AllGroupMembers.ps1: -------------------------------------------------------------------------------- 1 | #Created by the Scripting Guy 2 | #http://blogs.technet.com/b/heyscriptingguy/archive/2009/07/28/hey-scripting-guy-how-do-i-remove-all-group-members-in-active-directory.aspx 3 | 4 | Param( 5 | [string]$group, 6 | [string]$ou, 7 | [string]$domain, 8 | [switch]$whatif, 9 | [switch]$help 10 | ) #end param 11 | 12 | Function Get-ScriptHelp { 13 | 'Remove-AllGroupMembers.ps1 removes all members of a group' 14 | "Remove-AllGroupMembers.ps1 -group cn=mygroup -ou ou=myou -domain 'dc=nwtraders,dc=com'" 15 | "Remove-AllGroupMembers.ps1 -group cn=mygroup -ou ou=myou -domain 'dc=nwtraders,dc=com' -whatif" 16 | } # end function Get-ScriptHelp 17 | 18 | Function Remove-AllGroupMembers { 19 | Param( 20 | [string]$group, 21 | [string]$ou, 22 | [string]$domain 23 | ) #end param 24 | $ads_Property_Clear = 1 25 | $de = [adsi]"LDAP://$group,$ou,$domain" 26 | $de.putex($ads_Property_Clear, 'member', $null) 27 | $de.SetInfo() 28 | } # end function Remove-AllGroupMembers 29 | 30 | Function Get-Whatif { 31 | Param( 32 | [string]$group, 33 | [string]$ou, 34 | [string]$domain 35 | ) #end param 36 | "WHATIF: Remove all members from $group,$ou,$domain" 37 | } #end function Get-Whatif 38 | 39 | # *** Entry Point to script *** 40 | 41 | if (-not($group -and $ou -and $domain)) 42 | { throw ('group ou and domain required') } 43 | if ($whatif) { Get-Whatif -group $group -ou $ou -domain $domain ; exit } 44 | if ($help) { Get-Scripthelp ; exit } 45 | "Removing all members from $group,$ou,$domain" 46 | Remove-AllGroupMembers -group $group -ou $ou -domain $domain 47 | -------------------------------------------------------------------------------- /Active Directory/AD Users/Convert Mixed-Case Usernames to Lowercase.ps1: -------------------------------------------------------------------------------- 1 | # Convert all mixed-case usernames to lowercase usernames: 2 | 3 | $MixedCaseUserNames = Get-ADUser -Filter * | Where-Object {$_.samAccountName -cmatch "^(?=.*[a-z])(?=.*[A-Z]).*$" } 4 | 5 | foreach ($user in $MixedCaseUserNames) { 6 | $AccountName = $user.SamAccountName.ToLower() 7 | $UPN = $user.UserPrincipalName.ToLower() 8 | 9 | Set-ADUser -Identity $user -SamAccountName $AccountName -UserPrincipalName $UPN -WhatIf 10 | } 11 | -------------------------------------------------------------------------------- /Active Directory/AD Users/Find 'Do Not Require Kerberos Preauthentication' .ps1: -------------------------------------------------------------------------------- 1 | $Users = Get-ADUser -Filter * -Properties DoesNotRequirePreauth | Where-Object { $_.DoesNotRequirePreauth -eq $True } 2 | $Users | Select-Object Name, SamAccountName, UserPrincipalName, DoesNotRequirePreauth | Format-Table 3 | $Users.Count 4 | 5 | #Prevent accidental running before ready 6 | break 7 | 8 | # Turn off "DoesNotRequirePreauth" for all users. 9 | Get-ADUser -Filter * | Set-ADAccountControl -DoesNotRequirePreAuth $False 10 | -------------------------------------------------------------------------------- /Active Directory/AD Users/Get Users with Logon Scripts.ps1: -------------------------------------------------------------------------------- 1 | # Get all Active Directory users that have a logon script defined 2 | Get-ADUser -Filter 'ScriptPath -eq "*"' 3 | -------------------------------------------------------------------------------- /Active Directory/AD Users/Get Users with Whitespace in Mail Attribute.ps1: -------------------------------------------------------------------------------- 1 | function Get-UsersWithWhitespaceInMail { 2 | <# 3 | .SYNOPSIS 4 | Get AD users that have whitespace in their mail attribute. 5 | 6 | .DESCRIPTION 7 | This function retrieves Active Directory users whose mail attribute contains whitespace characters. This can cause problems 8 | with some systems and applications, so it's useful to identify these users and remove the whitespace. 9 | 10 | .PARAMETER ExportCsv 11 | Use this switch to export the results to a CSV file. 12 | 13 | .EXAMPLE 14 | Get-UsersWithWhitespaceInMail 15 | 16 | This command retrieves all AD users with whitespace in their mail attribute and displays the results in the console. 17 | 18 | .EXAMPLE 19 | Get-UsersWithWhitespaceInMail -ExportCsv 20 | 21 | This command retrieves all AD users with whitespace in their mail attribute and exports the results to a CSV file named 'Accounts with Whitespace in Mail.csv'. 22 | 23 | #> 24 | [CmdletBinding()] 25 | param ( 26 | [Parameter()] 27 | [switch]$ExportCsv 28 | ) 29 | 30 | $Pattern = '\s+' 31 | $Users = Get-ADUser -Filter * -Properties mail | Where-Object { $_.mail -Match $Pattern } 32 | 33 | if ($Users.Count -gt 0) { 34 | 35 | if ($ExportCsv) { 36 | $Users | Select-Object Name, sAMAccountName, mail, DistinguishedName | Export-Csv 'Accounts with Whitespace in Mail.csv' -NoTypeInformation 37 | } 38 | 39 | $Users 40 | } else { 41 | Write-Output 'No users found with whitespace in mail attribute.' 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Active Directory/AD Users/Get-AccountsWithPasswordNeverExpires.ps1: -------------------------------------------------------------------------------- 1 | function Get-AccountsWithPasswordNeverExpires { 2 | <# 3 | .SYNOPSIS 4 | Find accounts in Active Directory with passwords set to never expire. 5 | 6 | .DESCRIPTION 7 | Find accounts in Active Directory with passwords set to never expire. 8 | 9 | .EXAMPLE 10 | Get-AccountsWithPasswordNeverExpires 11 | #> 12 | [CmdletBinding()] 13 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] 14 | param () 15 | 16 | Search-ADAccount -PasswordNeverExpires 17 | } 18 | -------------------------------------------------------------------------------- /Active Directory/AD Users/Remove-ADGroupMembership.ps1: -------------------------------------------------------------------------------- 1 | function Remove-AllADGroupMembership { 2 | [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] 3 | param ( 4 | [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] 5 | $User 6 | ) 7 | begin { 8 | 9 | } 10 | process { 11 | $UserObject = Get-ADUser $User -Properties memberof 12 | $SamAccountName = $UserObject.$SamAccountName 13 | $UserObject.memberof | ForEach-Object { 14 | # Implement ShouldProcess to support -WhatIf and -Confirm 15 | if ($PSCmdlet.ShouldProcess($UserObject)) { 16 | Get-ADGroup $_ | Remove-ADGroupMember -Member $SamAccountName 17 | } 18 | } 19 | } 20 | end { 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Active Directory/AD Users/Remove-DisabledUsersFromAllGroups.ps1: -------------------------------------------------------------------------------- 1 | $Token = (Get-ADGroup 'Domain Users' -Properties PrimaryGroupToken).PrimaryGroupToken 2 | 3 | Get-ADUser -Filter 'Enabled -eq "False"' -Properties PrimaryGroup, MemberOf | ForEach-Object { 4 | 5 | # If the user's Primary Group is not Domain Users, then set Domain Users as their Primary Group. 6 | If ($_.PrimaryGroup -notmatch 'Domain Users') { 7 | Set-ADUsers -Identity $_ -Replace @{PrimaryGroupID = $Token } -Verbose 8 | } 9 | 10 | # If User is a member of more than 1 Group, remove all group memberships except Domain Users. 11 | If ($_.memberof) { 12 | $Group = Get-ADPrincipalGroupMembership -Identity $_ | Where-Object { $_.Name -ne 'Domain Users' } 13 | Remove-ADPrincipalGroupMembership -Identity $_ -MemberOf $Group -Confirm:$false -Verbose 14 | } 15 | 16 | # Move User to Disabled OU. 17 | #Move-ADObject -Identity $_ -TargetPath "OU=Disabled Users,OU=Disabled Accounts," -Verbose 18 | 19 | } #Foreach 20 | -------------------------------------------------------------------------------- /Active Directory/AD Users/Set User EmailAddress from PrimaryAddress.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Copy the user's primary email address from the proxyAddresses attribute to the email address attribute on their AD Account. 4 | 5 | .NOTES 6 | 7 | This is an old script that I found. There is a better, easier way to do this now! 8 | 9 | #> 10 | 11 | <# Simple Version 12 | $users = Get-ADUser -Filter * -Properties mail,emailaddress,proxyAddresses 13 | foreach ($user in $users) { 14 | $addresses = $user | Select -ExpandProperty ProxyAddresses 15 | $emailaddress = ($addresses | ? {$_ -clike "SMTP:*"}).TrimStart("SMTP:") #Find the primary address with an all-caps SMTP prefix 16 | Set-ADUser $user -EmailAddress $emailaddress -Whatif #Put the email address back in! 17 | } 18 | # The fun version follows! 19 | #> 20 | 21 | 22 | $MainScriptActivity = "Setting users' emailaddress from their primary STMP in proxyAddresses" 23 | $UserLoopActivity = "Looping through $userCount user accounts" 24 | 25 | Write-Progress -Id 1 -Activity $MainScriptActivity -Status "Getting all Active Directory user objects that have a value in their proxyAddresses attribute" -PercentComplete 1 26 | $users = Get-ADUser -Filter * -Properties mail,emailaddress,proxyAddresses | Sort-Object Name 27 | $userCount = $users.Count 28 | $MainScriptStatus = "Processing $userCount users" 29 | $i = 0 30 | 31 | Write-Progress -Id 1 -Activity $MainScriptActivity -Status $MainScriptStatus -PercentComplete 5 32 | foreach ($user in $users) 33 | { 34 | $i++ 35 | Write-Progress -Id 2 -Activity $UserLoopActivity -Status "$i of $userCount - $user.Name" -PercentComplete ($i / $userCount * 100) -ParentId 1 36 | 37 | $addresses = $user | Select-Object -ExpandProperty ProxyAddresses 38 | If ($addresses) 39 | { 40 | $emailaddress = ($addresses | Where-Object {$_ -clike "SMTP:*"}).TrimStart("SMTP:") 41 | Set-ADUser $user -EmailAddress $emailaddress -Confirm:$false 42 | Write-Progress -Id 1 -Activity $MainScriptActivity -Status $MainScriptStatus -PercentComplete ((($i / $userCount * 100) * 0.95) + 5) 43 | } 44 | Else 45 | { 46 | Write-Output "No proxyAddresses found for $user." 47 | Write-Progress -Id 1 -Activity $MainScriptActivity -Status $MainScriptStatus -PercentComplete ((($i / $userCount * 100) * 0.95) + 5) 48 | } 49 | } 50 | Write-Progress -Id 2 -Activity $UserLoopActivity -Status "Finished processing the list of users" -ParentId 1 51 | Write-Progress -Id 1 -Activity $MainScriptActivity -Status "Task Complete" 52 | -------------------------------------------------------------------------------- /Active Directory/AD Users/Working with Custom Extension Attributes.ps1: -------------------------------------------------------------------------------- 1 | # To Do: Wrap in a function. Create a module to report on, set, or clear these. 2 | 3 | # Check the usage of CustomAttribute[1-15] in Exchange Server: 4 | $Recipients = Get-Recipient -Resultsize Unlimited 5 | [System.Collections.ArrayList]$Report = @() 6 | (1..15) | ForEach-Object { 7 | $CustomAttribute = "CustomAttribute$_" 8 | $Report.Add([PSCustomObject]@{ 9 | Name = $CustomAttribute 10 | Available = (($Recipients.Where({ [string]::IsNullOrEmpty($_.$CustomAttribute) }) ).Count) 11 | Used = (($Recipients.Where({ -not [string]::IsNullOrEmpty($_.$CustomAttribute) }) ).Count) 12 | UsedBy = $null # Optionally include the $Recipients that use this (not NullOrEmpty). 13 | }) | Out-Null 14 | } 15 | $Report 16 | 17 | # Check the usage of ExtensionAttribute[1-15] in Active Directory. Optionally filter disable accounts. 18 | $Users = Get-ADUser -Filter * -ResultsetSize 100000 19 | [System.Collections.ArrayList]$Report = @() 20 | (1..15) | ForEach-Object { 21 | $ExtensionAttribute = "ExtensionAttribute$_" 22 | $Report.Add([PSCustomObject]@{ 23 | Name = $ExtensionAttribute 24 | Available = (($Users.Where({ [string]::IsNullOrEmpty($_.$ExtensionAttribute) }) ).Count) 25 | Used = (($Users.Where({ -not [string]::IsNullOrEmpty($_.$ExtensionAttribute) }) ).Count) 26 | UsedBy = $null # Optionally include the $Users that use this (not NullOrEmpty). 27 | }) | Out-Null 28 | } 29 | $Report 30 | -------------------------------------------------------------------------------- /Active Directory/Convert-SID.ps1: -------------------------------------------------------------------------------- 1 | Function Resolve-SID { 2 | <# 3 | .NOTES 4 | By Jeff Hicks 5 | https://jdhitsolutions.com/blog/powershell/8947/i-sid-you-not/ 6 | #> 7 | [cmdletbinding()] 8 | [OutputType("ResolvedSID", "String")] 9 | Param( 10 | [Parameter( 11 | Position = 0, 12 | Mandatory, 13 | ValueFromPipeline, 14 | ValueFromPipelineByPropertyName, 15 | HelpMessage = "Enter a SID string." 16 | )] 17 | [ValidateScript({ 18 | If ($_ -match 'S-1-[1235]-\d{1,2}(-\d+)*') { 19 | $True 20 | } 21 | else { 22 | Throw "The parameter value does not match the pattern for a valid SID." 23 | $False 24 | } 25 | })] 26 | [string]$SID, 27 | [Parameter(HelpMessage = "Display the resolved account name as a string.")] 28 | [switch]$ToString 29 | ) 30 | Begin { 31 | Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)" 32 | } #begin 33 | 34 | Process { 35 | Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Converting $SID " 36 | Try { 37 | if ($SID -eq 'S-1-5-32') { 38 | #apparently you can't resolve the builtin account 39 | $resolved = "$env:COMPUTERNAME\BUILTIN" 40 | } 41 | else { 42 | $resolved = [System.Security.Principal.SecurityIdentifier]::new($sid).Translate([system.security.principal.NTAccount]).value 43 | } 44 | 45 | if ($ToString) { 46 | $resolved 47 | } 48 | else { 49 | if ($resolved -match "\\") { 50 | $domain = $resolved.Split("\")[0] 51 | $username = $resolved.Split("\")[1] 52 | } 53 | else { 54 | $domain = $Null 55 | $username = $resolved 56 | } 57 | [pscustomObject]@{ 58 | PSTypename = "ResolvedSID" 59 | NTAccount = $resolved 60 | Domain = $domain 61 | Username = $username 62 | SID = $SID 63 | } 64 | } 65 | } 66 | Catch { 67 | Write-Warning "Failed to resolve $SID. $($_.Exception.InnerException.Message)" 68 | } 69 | } #process 70 | 71 | End { 72 | Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)" 73 | } #end 74 | 75 | } #close Resolve-SID 76 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Check AD Domain Permissions.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | The idea for this script was based a sample script provided by Trimarc at 3 | https://www.hub.trimarcsecurity.com/post/mitigating-exchange-permission-paths-to-domain-admins-in-active-directory. 4 | 5 | See also: https://www.hub.trimarcsecurity.com/post/securing-active-directory-performing-an-active-directory-security-review 6 | #> 7 | 8 | # Customizable Variables: 9 | $DomainRootPermissionsReportDir = '.\Reports' 10 | $DomainRootPermissionsReportName = 'DomainRootPermissionsReport.csv' 11 | 12 | # Script Variables 13 | $ADDomain = (Get-ADDomain).DnsRoot 14 | $ConfigurationContext = $((Get-ADRootDSE).ConfigurationNamingContext) 15 | $DomainTopLevelObjectDN = (Get-ADDomain $ADDomain).DistinguishedName 16 | $GUID = '' 17 | $DomainRootPermissionsReportPath = $DomainRootPermissionsReportDir + '\' + $ADDomain + '-' + $DomainRootPermissionsReportName 18 | 19 | function Get-PermissionName { 20 | param ( 21 | [string]$GUID 22 | ) 23 | # Look for the GUID in the Extended-Rights of the ControlAccessRight class 24 | $ExtendedRightsName = ( (Get-ADObject -SearchBase "CN=Extended-Rights,$ConfigurationContext" -LDAPFilter "(&(ObjectClass=ControlAccessRight)(RightsGUID=$GUID))") ).Name 25 | if ( $ExtendedRightsName ) { 26 | Return $ExtendedRightsName 27 | } 28 | # If the GUID is not found in Extended-Rights, look for it in the SchemaNamingContext: 29 | else { 30 | $SchemaRightsName = ( (Get-ADObject -SearchBase (Get-ADRootDSE).SchemaNamingContext -LDAPFilter "(SchemaIDGUID=$GUID)" -Properties Name, SchemaIDGUID) ).Name 31 | Return $SchemaRightsName 32 | } 33 | } 34 | 35 | # Create the directory if it does not already exist 36 | if ( !(Test-Path $DomainRootPermissionsReportDir) ) { 37 | New-Item -type Directory -Path $DomainRootPermissionsReportDir 38 | } 39 | 40 | # Get the details of the domain root permissions. 41 | $DomainRootPermissions = Get-ADObject -Identity $DomainTopLevelObjectDN -Properties * | Select-Object -ExpandProperty nTSecurityDescriptor | Select-Object -ExpandProperty Access 42 | 43 | # Export the permissions and details to the CSV file. 44 | $DomainRootPermissions | Select-Object IdentityReference, ActiveDirectoryRights, AccessControlType, IsInherited, InheritanceType, ` 45 | InheritedObjectType, ObjectFlags, InheritanceFlags, PropagationFlags, @{N = 'Type'; E = { Get-PermissionName $_.ObjectType } } ` 46 | | Export-Csv $DomainRootPermissionsReportPath -NoTypeInfo 47 | 48 | # Display the essential details. 49 | $DomainRootPermissions | Select-Object IdentityReference, ActiveDirectoryRights, AccessControlType, IsInherited | Sort-Object ActiveDirectoryRights, IdentityReference 50 | 51 | Write-Output `n"$ADDomain Domain Permission Report saved to $DomainRootPermissionsReportPath" 52 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Check all DCs.ps1: -------------------------------------------------------------------------------- 1 | #WIP 2 | 3 | # Get all DC records and test them for connectivity 4 | Get-ADDomainController -Filter * | Sort-Object Name | Format-Table Name, Enabled, IsGlobalCatalog, ComputerObjectDN 5 | foreach ($dc in $dcs) { 6 | Test-NetConnection $dc.DnsHostname | Select-Object ComputerName, RemoteAddress, PingSucceeded | Format-Table 7 | } 8 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Checking and Setting AD Schema Attributes.ps1: -------------------------------------------------------------------------------- 1 | Import-Module ActiveDirectory 2 | Set-ADUser 'Bilbo Baggins' -Add @{drink = 'coffee' } 3 | Get-ADUser 'Bilbo Baggins' -Properties drink | Select-Object * 4 | 5 | $Users = Get-ADUser -Filter 'Enabled -eq $true' -Properties drink 6 | $Drinks = $Users | Group-Object drink 7 | $Drinks 8 | 9 | $SchemaPath = (Get-ADRootDSE).schemaNamingContext 10 | $DrinkSchema = Get-ADObject -Filter 'Name -like "drink"' -SearchBase $SchemaPath -Properties * 11 | [void]$DrinkSchema 12 | $UserSchema = Get-ADObject -Filter 'Name -like "user"' -SearchBase $SchemaPath -Properties * 13 | $PersonSchema = Get-ADObject -Filter 'Name -like "person"' -SearchBase $SchemaPath -Properties * 14 | [void]$PersonSchema 15 | $Attributes = @() 16 | $Attributes += $UserSchema.maycontain 17 | $Attributes += $userSchema.systemMayContain 18 | $Attributes += $UserSchema.auxiliaryClass 19 | $Attributes += $UserSchema.systemAuxiliaryClass 20 | $Attributes = $Attributes | Sort-Object 21 | 22 | $Attributes 23 | $UserSchema.AddedProperties 24 | 25 | # Import the Active Directory module 26 | Import-Module ActiveDirectory 27 | 28 | # Get the schema object for the drink attribute 29 | $schemaObject = Get-ADObject -Filter { name -eq 'drink' } -SearchBase (Get-ADRootDSE).schemaNamingContext -Properties isDefunct 30 | 31 | # Check if the drink attribute is enabled in the schema 32 | if ($schemaObject.isDefunct) { 33 | Write-Output 'The drink attribute is not enabled.' 34 | } else { 35 | Write-Output 'The drink attribute is enabled.' 36 | } 37 | 38 | # Check if the drink attribute is added to the user class 39 | $userClass = Get-ADObject -Filter { name -eq 'user' } -SearchBase (Get-ADRootDSE).schemaNamingContext -Properties mayContain 40 | if ($userClass.mayContain -contains 'drink') { 41 | Write-Output 'The drink attribute is added to the user class.' 42 | } else { 43 | Write-Output 'The drink attribute is not added to the user class.' 44 | } 45 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Create Missing AD Sites.ps1: -------------------------------------------------------------------------------- 1 | $NetlogonLogPath = 'C:\Windows\Debug\netlogon.log' 2 | 3 | 4 | Import-Module ActiveDirectory 5 | 6 | # Get all subnets from Active Directory Sites and Services 7 | $ADSubnets = Get-ADSubnet -Filter * | Select-Object -ExpandProperty Name 8 | 9 | # Read the netlogon.log file 10 | $LogEntries = Get-Content -Path $NetlogonLogPath 11 | 12 | # Define a regex pattern to match log entries with IP addresses 13 | $IpPattern = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' 14 | 15 | # Initialize an array to store unmatched subnets 16 | $UnmatchedSubnets = New-Object System.Collections.Generic.List[ipaddress] 17 | 18 | # Parse the log entries 19 | foreach ($entry in $LogEntries) { 20 | if ($entry -match $IpPattern) { 21 | $IpAddress = $matches[0] 22 | $SubnetMatch = $false 23 | 24 | # Check if the IP address belongs to any AD subnet 25 | foreach ($subnet in $ADSubnets) { 26 | if ($IpAddress -like "$subnet*") { 27 | $SubnetMatch = $true 28 | break 29 | } 30 | } 31 | 32 | # If no match found, add to unmatched subnets 33 | if (-not $SubnetMatch) { 34 | $UnmatchedSubnets.Add($IpAddress) 35 | } 36 | } 37 | } 38 | 39 | # Output the unmatched subnets 40 | if ($UnmatchedSubnets.Count -gt 0) { 41 | Write-Output 'The following IP addresses are from subnets not listed in Active Directory Sites and Services:' 42 | $UnmatchedSubnets | Sort-Object | Get-Unique | ForEach-Object { Write-Output $_ } 43 | } else { 44 | Write-Output 'No unmatched subnets found.' 45 | } 46 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/DCDIAG Complete Testing.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Run a complete set of DCDIAG tests on all DCs in the forest. 4 | 5 | .DESCRIPTION 6 | This script runs a standard set of DCDIAG tests against all DCs in the forest and saves the details to a time-stamped log file. 7 | It then lists all of the omitted tests that are noted in the log file and runs each one of those, saving a log file for each. 8 | 9 | .NOTES 10 | Requires ADDS management tools (dcdiag.exe, netdom.exe) which are installed with an ADDS role automatically or when adding the 11 | ADDS management tools feature to any computer. Minimizes dependencies by not requiring the ActiveDirectory PowerShell module. 12 | 13 | DCDIAG Reference: https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/cc731968(v=ws.11) 14 | 15 | AUTHOR : Sam Erde 16 | DATE : 2021/06/30 17 | VERSION : 1.0 18 | CONTACT : 19 | https://github.com/SamErde 20 | https://twitter.com/SamErde 21 | #> 22 | 23 | $TimeStamp = Get-Date -Format 'yyymmdd_hhmmss' 24 | $LogFile = "dcdiag_$TimeStamp.log" 25 | 26 | # Find the PDC FSMO role holder. 27 | try { 28 | $PDCe = ( ( ( (& netdom query PDC) | 29 | Select-String -Pattern 'The command completed successfully.' -Context 1 -SimpleMatch) -Split '\r?\n' ) | 30 | Select-Object -Index 0).Trim() 31 | } catch { 32 | Write-Output 'Failed to find a PDCe.' 33 | $PDCe = $null 34 | } 35 | 36 | # Run DCDIAG agains all DCs in the enterprise [forest] (/e), with verbose output (/v), ignoring superfluous error messages (/i), writing to a log file (/f). 37 | & dcdiag /s:$PDCe /e /v /i /f:$LogFile 38 | 39 | # Find lines that note omitted tests and add each unique test name to an array 40 | $OmittedTests = (Get-Content -Path $LogFile | Select-String -Pattern 'Test omitted' -SimpleMatch | Select-Object -Unique) -Replace (' Test omitted by user request: ', '') 41 | 42 | foreach ($item in $OmittedTests) { 43 | $TestLogFile = 'dcdiag_' + $TimeStamp + '_' + $item + '.log' 44 | & dcdiag.exe /Test:$item /e /f:$TestLogFile 45 | } 46 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Enable-ADRecycleBin.ps1: -------------------------------------------------------------------------------- 1 | function Enable-ADRecycleBin { 2 | <# 3 | .SYNOPSIS 4 | Enable the Active Directory Recycle Bin 5 | .DESCRIPTION 6 | Enable the Active Directory recycle bin for the specified domain after confirming the required functional level. 7 | #> 8 | [CmdletBinding()] 9 | param ( 10 | # The domain in which to enable the AD Recycle Bin 11 | [Parameter(Mandatory, Position = 0)] 12 | [string] 13 | $Domain 14 | ) 15 | 16 | begin { 17 | if ( ((Get-ADForest).ForestMode) -ge (Get-ADOptionalFeature -Identity 'Recycle Bin Feature').RequiredForestMode ) { 18 | Write-Verbose "[OK] $Domain meets the minimum required forest functional level." 19 | } else { 20 | Write-Information -InformationAction Continue -MessageData "The Active Directory recycle bin feature requires the forest functional level to be at least 'WindowsServer2008R2'. Please raise the FFL before continuing." 21 | return 22 | } 23 | } 24 | 25 | process { 26 | if (Get-ADOptionalFeature -Identity 'Recycle Bin Feature') { 27 | Write-Output "The Recycle Bin Feature is already enabled in $Domain." 28 | } else { 29 | try { 30 | Enable-ADOptionalFeature 'Recycle Bin Feature' -Scope ForestOrConfigurationSet -Target $Domain 31 | Write-Output "The Recycle Bin Feature has been enabled for $Domain." 32 | } catch { 33 | $_ 34 | } 35 | } # end if 36 | } # end process 37 | 38 | end { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Export-ADSchema.ps1: -------------------------------------------------------------------------------- 1 | # Export the Active Directory schema to an LDIF file 2 | # with two flavors: 3 | 4 | $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain() 5 | $Schema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema() 6 | 7 | $RootDSE = New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE") 8 | $RootDSE = Get-ADRootDSE 9 | 10 | $SchemaPath = "LDAP://CN=Schema,$($Domain.GetDirectoryEntry().distinguishedName)" 11 | $SchemaPath = $RootDSE.schemaNamingContext 12 | 13 | # Example using LDAP 14 | $Schema.GetDirectoryEntry().psbase.Invoke("Dump", "$SchemaPath", "schema.ldf") 15 | 16 | # Example using LDIFDE 17 | ldifde.exe -f schema.ldf -s localhost -t 3268 -d "CN=Schema,CN=Configuration,DC=contoso,DC=com" -p subtree -r "(objectClass=*)" -l "objectClass,cn,attributeID,attributeSyntax,omSyntax" 18 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Get-ADForestBackups.ps1: -------------------------------------------------------------------------------- 1 | Get-ADForestDomainControllerBackups { 2 | <# 3 | .SYNOPSIS 4 | Get the last backup date of Active Directory domain controllers in the forest. 5 | 6 | .DESCRIPTION 7 | Get-ADForestDomainControllerBackups retrieves the last successful backup date of each domain controller in the forest. 8 | 9 | .EXAMPLE 10 | Get-ADForestDomainControllerBackups 11 | Get the last backup date of all domain controllers in the forest. 12 | 13 | .NOTES 14 | Author: Sam Erde, Sentinel Technologies, Inc. 15 | Version: 0.0.1 16 | Modified: 2024-11-18 17 | #> 18 | [CmdletBinding()] 19 | param () 20 | 21 | begin { 22 | $ForestDomainControllers = foreach ($domain in (Get-ADForest).domains) { 23 | Get-ADDomainController -Server $domain -Filter * 24 | } 25 | } 26 | process { 27 | foreach ($DC in $ForestDomainControllers) { 28 | $DCName = Get-ADObject -Identity $DC.NTDSSettingsObjectDN -Properties * 29 | $BackupTime = Get-ADObject -Identity $DCName -Properties BackupLastSuccessful 30 | if ($backupTime.BackupLastSuccessful) { 31 | "Last backup time for $($DC.Name): $($backupTime.BackupLastSuccessful)" 32 | } else { 33 | 'No backup information available.' 34 | } 35 | #[PSCustomObject]@{ 36 | # Hostname = $DC.HostName 37 | # LastBackupDate = $BackupDate 38 | #} 39 | } 40 | } 41 | end {} 42 | } 43 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Get-ADSitesInDefaultLink.ps1: -------------------------------------------------------------------------------- 1 | Import-Module ActiveDirectory 2 | $sites = Get-ADReplicationSite -Filter * 3 | foreach ($site in $sites) { 4 | $siteLinks = Get-ADReplicationSiteLink -Filter { SitesIncluded -match $($site.Name) } 5 | [PSCustomObject]@{ 6 | SiteName = $site.Name 7 | SiteLinks = $siteLinks.Name -join ', ' 8 | } 9 | } 10 | Get-ADReplicationSiteLink -Filter 'SitesIncluded -contains "Default-First-Site-Name"' | Select-Object -ExpandProperty Name 11 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Get-ADSitesWithoutLink.ps1: -------------------------------------------------------------------------------- 1 | function Get-ADSitesWithoutLink { 2 | <# 3 | .SYNOPSIS 4 | Get all Active Directory sites that are not included in any site link. 5 | 6 | .DESCRIPTION 7 | Get-ADSitesWithoutLinks gets all Active Directory replication sites and site links, and then determines which sites are not included in any site link. 8 | 9 | .EXAMPLE 10 | Get-ADSitesWithoutLinks 11 | 12 | .EXAMPLE 13 | Get-AdSitesWithoutLinks | Format-Table @{Name = 'Site'; Expression = { $_.Key } }, @{Name = 'SiteLink[s]'; Expression = { $_.Value -join ', '}} 14 | 15 | Format the output as a table with custom headers. 16 | 17 | .NOTES 18 | Author: Sam Erde, Sentinel Technologies, Inc. 19 | Version: 0.0.1 20 | Modified: 2024-11-18 21 | #> 22 | [CmdletBinding()] 23 | param ( ) 24 | 25 | begin { } 26 | 27 | process { 28 | # Create an ordered dictionary hash table from $ADSites that stores the site name as the key and the site object as the value. 29 | $ADSites = [ordered]@{} 30 | foreach ($site in (Get-ADReplicationSite -Filter * -Properties * -ErrorAction SilentlyContinue | Sort-Object -Property Name)) { 31 | $ADSites[$site.Name] = $site 32 | } 33 | 34 | $ADSiteLinks = Get-ADReplicationSiteLink -Filter * | Sort-Object -Property Name 35 | 36 | $SiteLinkMap = [ordered]@{} 37 | foreach ( $siteName in ($ADSites.GetEnumerator()).Name ) { 38 | 39 | foreach ($link in $ADSiteLinks) { 40 | foreach ($siteIncluded in $link.SitesIncluded) { 41 | if ( (Get-ADReplicationSite $siteIncluded).Name -eq $siteName ) { 42 | $SiteLinkMap[$SiteName] += "$($link).Name, " # need to remove the last comma and space from the last item 43 | } 44 | } 45 | } 46 | } 47 | 48 | foreach ($siteLink in $SiteLinkMap.GetEnumerator()) { 49 | if ($null -eq $siteLink.Value) { 50 | Write-Warning -Message "The site $($siteLink.Key) is not found in any site link." 51 | } 52 | } 53 | #endregion Sites without SiteLinks 54 | } 55 | 56 | end { 57 | $SiteLinkMap 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Get-DCTimeConfig.ps1: -------------------------------------------------------------------------------- 1 | # WIP 2 | 3 | # Check domain controllers' time sources. 4 | $DomainControllers = Get-ADDomainController -Filter * 5 | foreach ($DC in $DomainControllers) { 6 | $TimeSource = w32tm /query /status /computer:$DC.HostName | Select-String 'Source' 7 | [PSCustomObject]@{ 8 | Hostname = $DC.HostName 9 | TimeSource = $TimeSource -replace 'Source: ', '' 10 | } 11 | } 12 | 13 | # Check the time settings on each domain controller using Get-CimInstance 14 | foreach ($DC in $DomainControllers) { 15 | $TimeSettings = Get-CimInstance -ClassName Win32_TimeZone -ComputerName $DC.HostName 16 | [PSCustomObject]@{ 17 | Hostname = $DC.HostName 18 | Caption = $TimeSettings.Caption 19 | Description = $TimeSettings.Description 20 | StandardName = $TimeSettings.StandardName 21 | DaylightName = $TimeSettings.DaylightName 22 | } 23 | } 24 | 25 | # Check the NTP server and Win32Time service status on each domain controller using Get-CimInstance 26 | foreach ($DC in $DomainControllers) { 27 | $NTPServer = Get-CimInstance -ClassName Win32_ComputerSystem -ComputerName $DC.HostName | Select-Object -ExpandProperty DomainRole 28 | $Win32TimeService = Get-CimInstance -ClassName Win32_Service -Filter "Name='w32time'" -ComputerName $DC.HostName 29 | 30 | [PSCustomObject]@{ 31 | Hostname = $DC.HostName 32 | NTPServer = $NTPServer 33 | Win32TimeServiceState = $Win32TimeService.State 34 | Win32TimeServiceStartMode = $Win32TimeService.StartMode 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Get-DomainControllerLastBackup.ps1: -------------------------------------------------------------------------------- 1 | # WIP 2 | 3 | # Check the last backup date of Active Directory domain controllers 4 | foreach ($DC in $ForestDomainControllers) { 5 | $BackupDate = Get-ADObject -Filter { ObjectClass -eq 'msDS-LastSuccessfulBackup' } -SearchBase "CN=NTDS Settings,CN=$($DC.Name),CN=Servers,CN=$($DC.Site),CN=Sites,CN=Configuration,$((Get-ADRootDSE).ConfigurationNamingContext)" -Property msDS-LastSuccessfulBackup | Select-Object -ExpandProperty msDS-LastSuccessfulBackup 6 | 7 | [PSCustomObject]@{ 8 | Hostname = $DC.HostName 9 | LastBackupDate = $BackupDate 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Get-FSMORoleDetails.ps1: -------------------------------------------------------------------------------- 1 | #WIP 2 | 3 | # Get the hostname and AD site location of domain controllers that hold the AD FSMO roles. 4 | $FSMORoles = Get-ADForest | Select-Object -ExpandProperty SchemaMaster, DomainNamingMaster 5 | $FSMORoles += Get-ADDomain | Select-Object -ExpandProperty PDCEmulator, RIDMaster, InfrastructureMaster 6 | 7 | # Get the details of each FSMO role holder 8 | foreach ($role in $FSMORoles) { 9 | $DC = Get-ADDomainController -Identity $role 10 | [PSCustomObject]@{ 11 | Hostname = $DC.HostName 12 | IPAddress = $DC.IPAddress 13 | ADSite = $DC.Site 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Get-TrustedDomainNetBIOSNames.ps1: -------------------------------------------------------------------------------- 1 | Import-Module ActiveDirectory 2 | [array]$ADDomainTrusts = (Get-ADObject -Filter { ObjectClass -eq 'trustedDomain' }).Name 3 | [array]$NetBIOSDomainNames = @() 4 | 5 | foreach ($trust in $ADDomainTrusts) { 6 | $trustedDNSDomainName = $trust 7 | $NetBIOSDomainNames += ((Get-ADDomain $trustedDNSDomainName | Select-Object NetBIOSName) | Out-String).Trim() 8 | } 9 | 10 | $NetBIOSDomainNames 11 | 12 | <# Or using this: 13 | $TrustedDomains = @{} 14 | $TrustedDomains += Get-ADObject -Filter {ObjectClass -eq "trustedDomain"} -Properties * | 15 | Select-Object @{ Name = 'NetBIOSName'; Expr = { $_.FlatName } },@{ Name = 'DNSName'; Expr = { $_.Name } },$TrustedDomains 16 | 17 | #foreach ($Domain in $TrustedDomains) { 18 | # @{ Name = 'Server'; Expr = { (Get-ADDomainController -Discover -ForceDiscover -Writable -Service ADWS -DomainName $_.Name).Hostname[0] } } 19 | #} 20 | #> 21 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Invoke-DcDiag.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-DcDiag { 2 | param( 3 | [Parameter(Mandatory)] 4 | [ValidateNotNullOrEmpty()] 5 | [string]$DomainController 6 | ) 7 | 8 | $result = dcdiag /s:$DomainController 9 | $result | Select-String -Pattern '\. (.*) \b(passed|failed)\b test (.*)' | ForEach-Object { 10 | $obj = @{ 11 | TestName = $_.Matches.Groups[3].Value 12 | TestResult = $_.Matches.Groups[2].Value 13 | Entity = $_.Matches.Groups[1].Value 14 | } 15 | [pscustomobject]$obj 16 | } 17 | } 18 | $result 19 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Protect All OUs from Accidental Deletion.ps1: -------------------------------------------------------------------------------- 1 | Get-ADOrganizationalUnit -Filter * -Properties ProtectedFromAccidentalDeletion | Where-Object {$_.ProtectedFromAccidentalDeletion -eq $false} | Set-ADOrganizationalUnit -ProtectedFromAccidentalDeletion $true 2 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Query AD Object GUID Names.md: -------------------------------------------------------------------------------- 1 | # Query Active Directory Object GUID Type Names 2 | 3 | [https://www.sciencedirect.com/topics/computer-science/active-directory-object](https://www.sciencedirect.com/topics/computer-science/active-directory-object) 4 | 5 | [https://blog.backslasher.net/active-directory-object-specific-aces.html](https://blog.backslasher.net/active-directory-object-specific-aces.html) 6 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Set-ADFunctionalLevels.ps1: -------------------------------------------------------------------------------- 1 | Set-ADFunctionalLevels { 2 | 3 | # DRAFT - WORK IN PROGRESS 4 | 5 | [CmdletBinding()] 6 | param( 7 | # Domain functional level 8 | [Parameter()] 9 | [System.DirectoryServices.ActiveDirectory.DomainMode] 10 | $DomainFunctionalLevel, 11 | 12 | # Forest functional level 13 | [Parameter()] 14 | [System.DirectoryServices.ActiveDirectory.DomainMode] 15 | $ForestFunctionalLevel 16 | ) 17 | 18 | Import-Module ActiveDirectory 19 | 20 | if ($PSBoundParameters.ContainsKey('DomainFunctionalLevel') -and -not $PSBoundParameters.ContainsKey('ForestFunctionalLevel')) { 21 | # Domain Functional Level 22 | $PDCe = Get-ADDomainController -Discover -Service PrimaryDC 23 | Get-ADDomain -Identity $PDCe.Domain | Select-Object domainMode, DistinguishedName 24 | Set-ADDomainMode -Identity $PDCe.Domain -Server $PDCe.HostName[0] -DomainMode $DomainFunctionalLevel -WhatIf 25 | Get-ADDomain -Identity $PDCe.Domain | Select-Object domainMode, DistinguishedName 26 | } 27 | 28 | if ($PSBoundParameters.ContainsKey('ForestFunctionalLevel') -and -not $PSBoundParameters.ContainsKey('DomainFunctionalLevel')) { 29 | # Forest Functional Level 30 | $Forest = Get-ADForest 31 | $Forest.ForestMode 32 | Set-ADForestMode -Identity $Forest -Server $Forest.SchemaMaster -ForestMode $ForestFunctionalLevel -WhatIf 33 | (Get-ADForest).ForestMode 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Active Directory/Domain Services/Test-DNSName.ps1: -------------------------------------------------------------------------------- 1 | <# Test-DNSName.ps1 2 | .Description 3 | A high performance DNS resolver in PowerShell, written by Justin Grote at 4 | https://gist.github.com/JustinGrote/d1421208bf1dea22664fc6a198219047 5 | 6 | #> 7 | 8 | using namespace System.Net 9 | using namespace System.Threading.Tasks 10 | using namespace System.Management.Automation 11 | using namespace System.Collections.Generic 12 | 13 | function Test-DNSName ([String[]]$hostnames, [int]$Timeout = 3000) { 14 | <# 15 | .SYNOPSIS 16 | Given a list of DNS names, returns the ones that actually resolve to an actual name 17 | #> 18 | #A dictionary allows us to be strong typed as well as perform faster 19 | $taskIndex = [Dictionary[String,Task]]::new() 20 | #Because GetHostAddresses doesn't remember the origin, we have to use a hashtable to remember which request is which 21 | [Task[]]$dnsNames = $hostnames.foreach{ 22 | #This returns immediately with a task object, which we save to the dictionary and then output in order to then wait on. 23 | $task = [net.dns]::GetHostAddressesAsync($PSItem) 24 | [Void]$taskIndex.Add($PSItem,$task) 25 | $task 26 | } 27 | try { 28 | #Wait for the tasks to complete or the timeout is reached, whichever comes first 29 | [void][Task]::WaitAll($dnsNames,$Timeout) 30 | } catch [MethodInvocationException] { 31 | Write-Debug "WaitAll Error: $PSItem" 32 | } 33 | #Loop through the available keys and only return items that had an actual result (rather than errored) 34 | $taskIndex.keys.foreach{ 35 | write-debug $PSItem 36 | if ($taskIndex[$PSItem].Result) {$PSItem} 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Active Directory/Find-DeepestOU.ps1: -------------------------------------------------------------------------------- 1 | function Find-DeepestOU { 2 | <# 3 | .SYNOPSIS 4 | Find the deepst level of OU 5 | .DESCRIPTION 6 | Trying to recreate a concept for a recursively looping script that a co-worker described to me. I'm not getting it yet! 7 | #> 8 | param ( 9 | [string]$OU = (Get-ADRootDSE).rootDomainNamingContext, 10 | [int]$CurrentDepth = 0 11 | ) 12 | 13 | $CurrentDeepest = $CurrentDepth 14 | $CurrentDepth++ 15 | 16 | $SubOUs = Get-ADOrganizationalUnit -Filter * -SearchBase $OU -SearchScope OneLevel 17 | if ($SubOUs.Count -eq 0) { 18 | Return @{ 19 | Depth = $CurrentDeepest 20 | OU = $OU 21 | } 22 | 23 | foreach ($ou in $SubOUs) { 24 | Return (Find-Deepest $ou $CurrentDepth) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Active Directory/Get-ADObjectFromPipeline.ps1: -------------------------------------------------------------------------------- 1 | function Get-ADObjectFromPipeline { 2 | <# 3 | .SYNOPSIS 4 | Determines the type of an object passed to the pipeline and returns the object as an ADObject. 5 | .DESCRIPTION 6 | Determines the type of an object passed to the pipeline and returns the object as an ADObject. Potentially 7 | useful for normalizing input to other functions. 8 | #> 9 | [CmdletBinding()] 10 | param ( 11 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 12 | [ValidateNotNullOrEmpty()] 13 | $Identity 14 | ) 15 | 16 | begin { 17 | Import-Module ActiveDirectory 18 | $GlobalCatalog = Get-ADDomainController -Discover -Service GlobalCatalog 19 | 20 | if ($Identity -is [Microsoft.ActiveDirectory.Management.ADUser]) { 21 | # We have an ADUser object 22 | # Might want to normalize the type to an ADObject IF we can get sidHistory from an ADObject 23 | } 24 | if ($Identity -is [Microsoft.ActiveDirectory.Management.ADComputer]) { 25 | # We have an ADComputer object 26 | # Might want to normalize the type to an ADObject IF we can get sidHistory from an ADObject 27 | } 28 | if ($Identity -is [string]) { 29 | # Find an AD object and determine its type 30 | $Identity = Get-ADObject -Filter "Name -eq `"$Identity`"" 31 | } 32 | $IdentityType = $Identity.ObjectClass 33 | } 34 | 35 | process { 36 | switch ($IdentityType) { 37 | 'user' { 38 | # Not Complete 39 | $User = Get-ADUser -Identity $Identity -Properties PrimaryGroup,SidHistory 40 | } 41 | 'computer' { 42 | # Not Complete 43 | $Computer = Get-ADComputer -Identity $Identity -Properties PrimaryGroup,SidHistory 44 | } 45 | Default { 46 | Write-Error "Identity type not supported." 47 | } 48 | } 49 | } 50 | 51 | end { 52 | # Do something and/or return the resulting object to the pipeline. 53 | if ($User) { 54 | $User 55 | } 56 | if ($Computer) { 57 | $Computer 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Active Directory/Get-LAPSPasswords.ps1: -------------------------------------------------------------------------------- 1 | Get-ADComputer -SearchBase 'OU=Member Servers,DC=DOMAINNAME,DC=org' ` 2 | -Properties Name, ms-Mcs-AdmPwd, ms-Mcs-AdmPwdExpirationTime -Filter { Name -notlike '*xen*' } | ` 3 | Select-Object Name, ms-Mcs-AdmPwd, ms-Mcs-AdmPwdExpirationTime | Sort-Object Name | Out-GridView 4 | -------------------------------------------------------------------------------- /Active Directory/Get-OverlappingOUName.ps1: -------------------------------------------------------------------------------- 1 | function Get-OverlappingOUName { 2 | [CmdletBinding()] 3 | param ( 4 | 5 | ) 6 | 7 | begin { 8 | Import-Module ActiveDirectory 9 | } 10 | 11 | process { 12 | $OUs = Get-ADOrganizationalUnit -Filter * 13 | $OverlappingOUNames = $OUs | Group-Object -Property Name | Where-Object { $_.Count -gt 1 } 14 | } 15 | 16 | end { 17 | $OverlappingOUNames 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Active Directory/MapGuidsToGpoNames.ps1: -------------------------------------------------------------------------------- 1 | function Get-GuidToGpoName { 2 | <# 3 | .SYNOPSIS 4 | This script maps GUIDs to GPO display names in the SYSVOL share on a domain controller. 5 | 6 | .DESCRIPTION 7 | This script looks at every GPO directory in a domain's SYSVOL share and 8 | inspects the XML contents to map GUID directory names to GPO display names. 9 | 10 | .PARAMETER Path 11 | The path to the Policies directory in the SYSVOL share on a domain controller. 12 | 13 | .EXAMPLE 14 | Get-GuidToGpoName -Path \\dc1\sysvol\domain.com\Policies 15 | This example will return a table of GPO display names and their corresponding GUIDs. 16 | #> 17 | 18 | [CmdletBinding()] 19 | param( 20 | [parameter(Mandatory = $true)] 21 | [String] 22 | $Path 23 | ) 24 | 25 | begin { 26 | 27 | } 28 | 29 | process { 30 | $Results = @{} 31 | Get-ChildItem -Recurse -Include backup.xml $Path | ForEach-Object { 32 | $GUID = $_.Directory.Name 33 | $XML = [xml](Get-Content $_) 34 | $DN = $XML.GroupPolicyBackupScheme.GroupPolicyObject.GroupPolicyCoreSettings.DisplayName.InnerText 35 | $Results.Add($DN, $GUID) 36 | } 37 | $Results | Format-Table Name, Value -AutoSize 38 | } 39 | 40 | end { 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Active Directory/No-RSAT/README.md: -------------------------------------------------------------------------------- 1 | # No RSAT 2 | 3 | See [NoRSAT](https://github.com/SamErde/NoRSAT). 4 | -------------------------------------------------------------------------------- /Active Directory/README.md: -------------------------------------------------------------------------------- 1 | # PowerShell Scripts and Tools for Active Directory 2 | 3 | This repository is a collection of scripts for working with Active Directory. Some of them are recently built and 4 | maintained; others are old, purpose-built scripts that I kept for historical purposes; and a few of these may be 5 | scripts that I have found on the web over the years and didn't want to loose track of. 6 | 7 | I will try to keep track of actively maintained scripts below: 8 | 9 | ## Get-ADUserEncryptionTypes 10 | 11 | [Get-ADUserEncryptionTypes.ps1](./Get-ADUserEncryptionTypes.ps1): A tool to help track user encryption types in Active Directory and work towards using AES instead of less secure types like RC4 and DES. 12 | 13 | ## Get-GPOsMissingPermissions 14 | 15 | [Get-GPOsMissingPermissions.ps1](./Get-GPOsMissingPermissions.ps1): A tool to help find GPOs that are missing permissions for either Authenticated Users or Domain Computers. 16 | 17 | ## Get-OrganizationalUnitDepth 18 | 19 | [Get-OrganizationalUnitDepth.ps1](./Get-OrganizationalUnitDepth.ps1): A tool to report the deepest levels of OUs in your Active Directory hierarchy so you can audit and plan for a simpler structure. 20 | 21 | ## Test-IsMemberOfProtectedUsers 22 | 23 | [Test-IsMemberOfProtectedUsers](./Test-IsMemberOfProtectedUsers.ps1): A tool to check if a user is a member of the Protected Users group in Active Directory. 24 | -------------------------------------------------------------------------------- /Active Directory/Resolve SID in Event Logs.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamErde/PowerShell/81285216c3fa137d9fbdab30e4a4c101118856d9/Active Directory/Resolve SID in Event Logs.ps1 -------------------------------------------------------------------------------- /Active Directory/SIDHistory/Get-ADObjectWithSIDHistory.ps1: -------------------------------------------------------------------------------- 1 | function Get-ADObjectWithSIDHistory { 2 | <# 3 | .SYNOPSIS 4 | Get all Active Directory objects that have SID history. 5 | 6 | .DESCRIPTION 7 | This function gets all Active Directory objects that have SID history. It can optionally be filtered to only get 8 | user, computer, or group objects. 9 | 10 | .EXAMPLE 11 | Get-AllAdSidHistory 12 | 13 | This example gets all Active Directory objects that have SID history. 14 | 15 | .EXAMPLE 16 | Get-AllAdSidHistory -Type User 17 | 18 | This example gets all Active Directory user objects that have SID history. 19 | 20 | .EXAMPLE 21 | Get-AllAdSidHistory -Type Computer 22 | 23 | This example gets all Active Directory computer objects that have SID history. 24 | 25 | .EXAMPLE 26 | Get-AllAdSidHistory -Type Group 27 | 28 | This example gets all Active Directory group objects that have SID history. 29 | 30 | .EXAMPLE 31 | Get-AllAdSidHistory -Type All 32 | 33 | This example gets all Active Directory objects that have SID history, regardless of type. 34 | 35 | .NOTES 36 | Author: Sam Erde, Sentinel Technologies, Inc. 37 | Version: 0.1.0 38 | Modified: 2025-01-10 39 | #> 40 | [CmdletBinding()] 41 | [OutputType('Microsoft.ActiveDirectory.Management.ADObject[]')] 42 | param ( 43 | # Type of objects to get. Default is 'All'. 44 | [Parameter()] 45 | [ValidateSet('All', 'User', 'Computer', 'Group')] 46 | [string] 47 | $Type = 'All' 48 | ) 49 | 50 | begin { 51 | if (-not (Get-Module -Name ActiveDirectory)) { 52 | Write-Verbose -Message 'Importing ActiveDirectory module.' 53 | Import-Module ActiveDirectory 54 | Write-Verbose -Message '------------------------------' 55 | Write-Verbose -Message "Beginning ${MyInvocation.InvocationName}..." 56 | } 57 | 58 | $BaseFilter = 'SIDHistory -like "*"' 59 | switch ($Type) { 60 | 'User' { $Filter = "$BaseFilter -and objectClass -eq 'user'" } 61 | 'Computer' { $Filter = "$BaseFilter -and objectClass -eq 'computer'" } 62 | 'Group' { $Filter = "$BaseFilter -and objectClass -eq 'group'" } 63 | 'All' { $Filter = $BaseFilter } 64 | } 65 | } # end begin 66 | 67 | process { 68 | # Get all ActiveDirectory objects that have SID history. 69 | [Microsoft.ActiveDirectory.Management.ADObject[]]$ADObjectList = Get-ADObject -Filter $Filter -Properties SIDHistory | Select-Object * -ExpandProperty SIDHistory 70 | } # end process 71 | 72 | end { 73 | $ADObjectList 74 | } # end end 75 | 76 | } # end function 77 | -------------------------------------------------------------------------------- /Active Directory/SIDHistory/Get-AllADSIDHistoryDetail.ps1: -------------------------------------------------------------------------------- 1 | function Get-AllAdSidHistoryDetail { 2 | [CmdletBinding()] 3 | [OutputType([hashtable])] 4 | param () 5 | 6 | begin { 7 | if (-not (Get-Module -Name ActiveDirectory)) { 8 | Write-Verbose -Message 'Importing ActiveDirectory module.' 9 | Import-Module ActiveDirectory 10 | Write-Verbose -Message '------------------------------' 11 | Write-Verbose -Message "Beginning ${MyInvocation.InvocationName}..." 12 | } 13 | 14 | $DomainSIDMapping = Get-TrustedDomainSIDMapping 15 | $SourceDomainAssociatedObjects = @{} 16 | } # end begin 17 | 18 | process { 19 | # Get all Active Directory objects that have a value in SID history. 20 | $ADObjectsWithSIDHistory = Get-ADObject -Filter 'SIDHistory -like "*"' -Properties SIDHistory 21 | 22 | # Loop through each ADObject and loop through each SID history item on that object. 23 | # For each SID history item, remove the RID and add the domain SID to the $SourceDomains hash table. 24 | foreach ($ADObject in $ADObjectsWithSIDHistory) { 25 | foreach ($SIDHistory in $ADObject.SIDHistory) { 26 | $DomainSID = $SIDHistory.Substring(0, $SIDHistory.LastIndexOf('-')) 27 | # If the domain SID is in the $DomainSIDMapping hash table, use the domain name as the key. If not, use the DomainSID as the key in the $SourceDomains hash table. 28 | $Domain = if ($DomainSIDMapping[$DomainSID]) { 29 | $DomainSIDMapping[$DomainSID].Value 30 | } else { 31 | $DomainSID 32 | } 33 | # Add the domain name as the key and the ADObject as the value to the $SourceDomainAssociatedObjects hash table. 34 | $SourceDomainAssociatedObjects.Add( 35 | $Domain, 36 | ([System.Collections.Generic.List[object]]).Add($ADObject) 37 | ) 38 | } 39 | } 40 | } # end process 41 | 42 | end { 43 | $SourceDomainAssociatedObjects 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Active Directory/SIDHistory/Get-AllADSIDHistorySourceDomain.ps1: -------------------------------------------------------------------------------- 1 | function Get-AllAdSidHistorySourceDomain { 2 | <# 3 | .SYNOPSIS 4 | Get a list of source domains from all Active Directory objects that have SID history. 5 | 6 | .DESCRIPTION 7 | This function gets all Active Directory objects that have SID history and returns a list of source domain SIDs. 8 | 9 | .EXAMPLE 10 | Get-AllAdSidHistorySourceDomains 11 | 12 | .NOTES 13 | Author: Sam Erde, Sentinel Technologies, Inc. 14 | Version: 0.0.1 15 | Modified: 2024-11-14 16 | #> 17 | [CmdletBinding()] 18 | param ( 19 | 20 | ) 21 | 22 | begin { 23 | if (-not (Get-Module -Name ActiveDirectory)) { 24 | Write-Verbose -Message 'Importing ActiveDirectory module.' 25 | Import-Module ActiveDirectory 26 | Write-Verbose -Message '------------------------------' 27 | Write-Verbose -Message "Beginning ${MyInvocation.InvocationName}..." 28 | } 29 | 30 | $SIDHistoryList = [ordered]@{} 31 | $DomainSIDs = New-Object -TypeName System.Collections.Generic.List[System.String] 32 | } # end begin 33 | 34 | process { 35 | # Get all ActiveDirectory objects that have SID history. 36 | $ADObjectsWithSIDHistory = Get-ADObject -Filter { SIDHistory -like '*' } -Properties SIDHistory | Select-Object * -ExpandProperty SIDHistory 37 | 38 | foreach ($object in $ADObjectsWithSIDHistory) { 39 | # Create a hash table of DistinguishedName and SIDHistory for each object. 40 | $SIDHistoryList.Add($object.DistinguishedName, $object.SIDHistory) 41 | # Create a de-duplicated list of source domain SIDs from the SIDHistory attribute of each object. 42 | $DomainSIDs.Add( $($object.SIDHistory.Substring(0, $SID.LastIndexOf('-'))) ) 43 | } 44 | } # end process 45 | 46 | end { 47 | 48 | } # end end 49 | 50 | } # end function 51 | -------------------------------------------------------------------------------- /Active Directory/Test-IsWellKnownSID.ps1: -------------------------------------------------------------------------------- 1 | function Test-IsWellKnownSid { 2 | <# 3 | .SYNOPSIS 4 | Check if a SID is a well-known SID. 5 | 6 | .DESCRIPTION 7 | Check if a SID or a SID string is a well-known SID. Returns a Boolean response. 8 | 9 | .PARAMETER SID 10 | The SID to test. This can be a SecurityIdentifier object or a string that will be converted into a SID object. 11 | 12 | .EXAMPLE 13 | Test-IsWellKnownSid -SID (New-Object System.Security.Principal.SecurityIdentifier("S-1-5-10")) 14 | 15 | .EXAMPLE 16 | Test-IsWellKnownSid -SID "S-1-5-10" 17 | 18 | .OUTPUTS 19 | System.Boolean 20 | #> 21 | [CmdletBinding()] 22 | [OutputType([bool])] 23 | param ( 24 | # Accepts a SecurityIdentifier object or a string that will be converted into a SID object. 25 | [Parameter( 26 | Mandatory = $true, 27 | ValueFromPipeline = $true, 28 | ValueFromPipelineByPropertyName = $true 29 | )] 30 | $SID 31 | ) 32 | 33 | begin { 34 | [bool]$IsWellKnownSID = $false 35 | } 36 | 37 | process { 38 | # If the SID paramemter is a [string] type, convert it to a SecurityIdentifier object. 39 | if ( $SID -is [string] ) { 40 | try { 41 | $SID = New-Object System.Security.Principal.SecurityIdentifier($SID) 42 | } catch { 43 | Write-Error "Failed to convert `"$SID`" to a SecurityIdentifier object." 44 | break 45 | } 46 | } 47 | 48 | if ( $SID.ToString() -eq "S-1-5-10" ) { 49 | $IsWellKnownSID = $true 50 | } 51 | 52 | # Compare the SID to all the well-known SID types. 53 | foreach ( $type in [Enum]::GetNames( [System.Security.Principal.WellKnownSidType] ) ) { 54 | if ( $SID.IsWellKnown($type) ) { 55 | $IsWellKnownSID = $true 56 | } 57 | } 58 | } 59 | 60 | end { 61 | $IsWellKnownSID 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Citrix/Reset-CitrixWorkspace.ps1: -------------------------------------------------------------------------------- 1 | function Reset-CitrixWorkspace { 2 | <# 3 | .SYNOPSIS 4 | Reset the Citrix Receiver or Workspace App 5 | 6 | .DESCRIPTION 7 | This script resets the Citrix Receiver or Workspace app and all of the current user's Citrix shortcuts. 8 | 9 | .EXAMPLE 10 | Reset-CitrixWorkspace 11 | 12 | .NOTES 13 | Author: Sam Erde, Sentinel Technologies 14 | Version: 0.0.3 15 | Modified: 2024-11-7 16 | 17 | .LINK 18 | https://support.citrix.com/s/article/CTX140149-how-to-reset-receiver-or-citrix-workspace-app-using-the-command-line 19 | 20 | #> 21 | [CmdletBinding(SupportsShouldProcess = $false)] 22 | param () 23 | 24 | # Define static variables 25 | $ShortcutFolder = "${env:APPDATA}\Microsoft\Windows\Start Menu\Programs\Citrix" 26 | $CleanupToolPath = "${env:ProgramFiles(x86)}\Citrix\ICA Client\SelfServicePlugin\CleanUp.exe" 27 | $SelfServicePath = "${env:ProgramFiles(x86)}\Citrix\ICA Client\SelfServicePlugin\SelfService.exe" 28 | 29 | # Run the Citrix Clean-up Tool 30 | if (Test-Path -Path $CleanupToolPath -PathType Leaf) { 31 | Start-Process -FilePath $CleanupToolPath -ArgumentList '/silent -cleanUser' 32 | } else { 33 | Write-Error -Message "The Cleanup tool was not found at $CleanupToolPath." 34 | return 35 | } 36 | 37 | # Remove the Citrix shortcut folder in the current user's start menu 38 | try { 39 | Remove-Item -Path $ShortcutFolder -Recurse -Force -ErrorAction Stop 40 | } catch { 41 | Write-Warning -Message "Failed to remove the Citrix shortcut folder in the user's start menu.`n$_" 42 | return 43 | } 44 | 45 | # Wait for shortcut folder to come back. Time out after 30 seconds if not. 46 | $StartTime = Get-Date 47 | while (-not (Test-Path $ShortcutFolder)) { 48 | $StepTime = (Get-Date) 49 | if ( ($StepTime - $StartTime).Seconds -ge 30 ) { 50 | Write-Warning -Message 'Timed out while waiting for the Citrix folder to be recrated in the Start Menu.' 51 | break 52 | } 53 | Write-Information -MessageData 'Waiting for the folder to be recreated...' -InformationAction Continue 54 | Start-Sleep 5 55 | } 56 | 57 | # Poll for new shortcuts after waiting 5 seconds 58 | Start-Sleep 5 59 | 60 | # Restart the SelfService executable and contact the server to refresh application details. This step should recreate shortcuts 61 | Start-Process -FilePath $SelfServicePath -ArgumentList '-poll' 62 | 63 | } 64 | 65 | Reset-CitrixWorkspace 66 | -------------------------------------------------------------------------------- /DDI/DNS Reverse Lookup from CSV and Add Column with Hostname.ps1: -------------------------------------------------------------------------------- 1 | $file = "" 2 | $csv = Import-Csv $file 3 | foreach ($row in $csv) { 4 | $IP = $row.SourceIP 5 | $row.SourceName = ([System.Net.DNS]::GetHostbyAddress($IP)).Hostname 6 | } 7 | $csv | Export-Csv "results.csv" -NoTypeInformation 8 | -------------------------------------------------------------------------------- /DDI/Get Hostnames from CSV IP Addresses.ps1: -------------------------------------------------------------------------------- 1 | # Import a CSV that should at least have a "SourceIP" column and a "Hostname" column. 2 | $csv = ".\IPAddresses.csv" 3 | 4 | # Import the CSV to a variable so we can update it and write the information back to the CSV at the end. 5 | $IPAddressList = Import-Csv -Path $csv 6 | # Using PowerShell 7's foreach-parallel. Should wrap this in a block that checks which version of PS is being used. 7 | $IPAddressList | foreach-object { 8 | $ip = $_.SourceIP 9 | try { 10 | $_.Hostname = ([System.Net.Dns]::GetHostEntry($ip)).HostName 11 | } 12 | catch { 13 | Write-Error $error[0] #.Exception.Message.Split(':')[1] 14 | } 15 | } 16 | # Write the data back to the CSV with the hostnames added. 17 | $IPAddressList | Export-Csv -Path $csv 18 | -------------------------------------------------------------------------------- /DDI/Get-DomainSubdomains.ps1: -------------------------------------------------------------------------------- 1 | function Get-DomainSubdomains { 2 | <# 3 | .SYNOPSIS 4 | Discover sub-domains in a domain. 5 | 6 | .DESCRIPTION 7 | This function uses the VirusTotal API to discover subdomains in a given domain namespace. 8 | 9 | .PARAMETER Domain 10 | The domain to check for subdomains. 11 | 12 | .EXAMPLE 13 | Get-DomainSubdomains -Domain 'example.com' 14 | 15 | .NOTES 16 | This requires a VirusTotal API key. 17 | 18 | #> 19 | [CmdletBinding()] 20 | param ( 21 | # The domain to discover subdomains in 22 | [Parameter(Mandatory, Position = 0, ValueFromPipeline)] 23 | [string] 24 | $Domain, 25 | 26 | # Your VirusTotal API key. (Need to make this more secure, but it's a working POC.) 27 | [Parameter(Position = 1)] 28 | $VtApiKey 29 | ) 30 | 31 | begin { 32 | 33 | } 34 | 35 | process { 36 | $IrmParams = @{ 37 | Uri = "https://www.virustotal.com/api/v3/domains/$Domain/subdomains" 38 | Method = 'GET' 39 | Headers = @{ 40 | 'X-ApiKey' = $VtApiKey 41 | 'Content-Type' = 'application/json' 42 | } 43 | } 44 | $Subdomains = ( (Invoke-RestMethod @IrmParams) | ConvertFrom-Json).data 45 | $Subdomains.id 46 | 47 | <# Output 👀 48 | elections.x.com 49 | transparency-staging.x.com 50 | about-dev.x.com 51 | careers-dev.x.com 52 | engineering.x.com 53 | gdpr-dev.x.com 54 | insights.x.com 55 | legal-dev.x.com 56 | marketing-dev.x.com 57 | partners-dev.x.com 58 | #> 59 | 60 | } 61 | 62 | end { 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /DDI/Get-ServiceRecords.ps1: -------------------------------------------------------------------------------- 1 | # WIP 2 | 3 | # Get all DNS zones 4 | $zones = Get-DnsServerZone -ComputerName (Get-Domain) | Where-Object { $_.ZoneType -eq 'Primary' } 5 | 6 | # Loop through each zone and get SRV records 7 | foreach ($zone in $zones) { 8 | $srvRecords = Get-DnsServerResourceRecord -ZoneName $zone.ZoneName -RRType SRV | Where-Object { $_.HostName -notmatch 'ldap' -and $_.HostName -notmatch 'gc' -and $_.HostName -notmatch 'kerberos' } 9 | foreach ($record in $srvRecords) { 10 | Write-Host "Name: $($record.HostName), Target: $($record.RecordData.Target), Port: $($record.RecordData.Port), Priority: $($record.RecordData.Priority), Weight: $($record.RecordData.Weight)" 11 | Write-Host "$zone" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DDI/README.md: -------------------------------------------------------------------------------- 1 | # DNS, DHCP, and IPAM (DDI) 2 | 3 | Scripts for working with DNS, DHCP, and IPAM. 4 | -------------------------------------------------------------------------------- /DDI/Remove-DhcpAllLeases.ps1: -------------------------------------------------------------------------------- 1 | function Remove-DhcpAllLeases { 2 | [CmdletBinding(SupportsShouldProcess)] 3 | [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', 'Remove-DhcpAllLeases',Justification='Removing ALL of leases.')] 4 | param ( 5 | # The hostname of the DHCP server 6 | [Parameter(Required, Position = 0)] 7 | [string] 8 | $ComputerName 9 | ) 10 | 11 | Write-Host "`nWARNING! You are about to remove all DHCP leases from the server $ComputerName." -ForegroundColor White -BackgroundColor DarkRed -NoNewLine 12 | Write-Host "" # Clear the colors 13 | $AreYouSure = Read-Host -Prompt "Enter `'yes'` to proceed or any other key to abort" 14 | if ($AreYouSure -ne 'yes') { 15 | # End the script 16 | break 17 | } 18 | 19 | $Scopes = Get-DhcpServerv4Scope -ComputerName $ComputerName 20 | 21 | foreach ($scope in $Scopes) { 22 | $Leases = Get-DhcpServerv4Lease -ScopeId $scope.scopeid -ComputerName $ComputerName 23 | foreach($lease in $leases) { 24 | if ($PSCmdlet.ShouldProcess($ComputerName, ("Removing all DHCP leases on $ComputerName"))) { 25 | Remove-DhcpServerv4Lease -IPAddress $lease.IPAddress -ComputerName $ComputerName 26 | Write-Output "Removed $($lease.IPAddress) from scope." 27 | } else { 28 | Write-Output "WhatIf: Removing $lease from $scope." 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DDI/Resolve-IPs.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Description: Resolve the hostnames for a list of IP addresses. 3 | 4 | To Do: 5 | - Add version handling so PowerShell 7 can take advantage of parallel for loops 6 | - Add error handling and logging option 7 | #> 8 | 9 | # Add IP addresses, one per line, without additional quotes 10 | $ListOfIPs = @' 11 | 12 | '@ -split [Environment]::NewLine 13 | $ResultList = @() 14 | 15 | 16 | foreach ($IP in $ListOfIPs) { 17 | $ErrorActionPreference = 'silentlycontinue' 18 | $Result = $null 19 | 20 | Write-Host "Resolving $IP" -ForegroundColor Green 21 | $result = [System.Net.Dns]::gethostentry($IP) 22 | 23 | If ($Result) { 24 | $ResultList += "$IP," + [string]$Result.HostName 25 | } Else { 26 | $ResultList += "$IP,unresolved" 27 | } 28 | } 29 | 30 | $ResultList 31 | -------------------------------------------------------------------------------- /DDI/Update-DnsServerList.ps1: -------------------------------------------------------------------------------- 1 | function Update-DnsServerList { 2 | <# 3 | .SYNOPSIS 4 | Check the DNS search order in a client's network interface and replace old DNS server IP addresses with new DNS server IP addresses. 5 | #> 6 | [CmdletBinding()] 7 | param ( 8 | # An array of old DNS server IP addresses 9 | [Parameter(Mandatory)] 10 | [ipaddress[]] 11 | $OldDnsServers, 12 | 13 | # An array of new DNS server IP addresses 14 | [Parameter(Mandatory)] 15 | [ipaddress[]] 16 | $NewDnsServers 17 | ) 18 | 19 | $NetworkAdapters = Get-CimInstance -Class Win32_NetworkAdapterConfiguration -Filter 'IPEnabled=True' 20 | foreach ($netadapter in $NetworkAdapters) { 21 | [ipaddress[]]$ClientDnsServerSearchOrder = $netadapter.DnsServerSearchOrder 22 | if (Compare-Object -ReferenceObject $ClientDnsServerSearchOrder -DifferenceObject $OldDnsServers -IncludeEqual -ExcludeDifferent) { 23 | $NetAdapterConfig = Get-CimInstance -Class Win32_NetworkAdapterConfiguration -Filter "Index = $._Index" 24 | $NetAdapterConfig.SetDnsServerSearchOrder($($NewDnsServers.IPAddressToString -join ',')) 25 | 26 | IpConfig /FlushDns 27 | IpConfig /RegisterDns 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /DDI/Validate IP Address.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | SNIPPET: An easy way to validate an IP address. 3 | https://twitter.com/mdjxkln/status/1416381792899141636 4 | 5 | There are sometimes problems with this approach, so RegEx may still be the best approach. 6 | 7 | [] Responds differently to strings vs integers. 8 | [] Follows standard of inserting zeroes in octects where an apparent value is not specified. (Example: [IPAddress] 2.2) 9 | #> 10 | 11 | $IP = '10.253.26.1' 12 | $IP -eq ([IPAddress]$IP).IPAddressToString 13 | # RESULT: True 14 | 15 | $IP = '1' 16 | $IP -eq ([IPAddress]$IP).IPAddressToString 17 | # RESULT: False 18 | 19 | $IP = '300.1.1.1' 20 | $IP -eq ([IPAddress]$IP).IPAddressToString 21 | # ERROR: Connot convert value "300.1.1.1" to type "System.Net.IPAddress". Error: "An invalid IP address was specified." 22 | 23 | [IPAddress] '10.253.26.1' 24 | <# OUTPUT: 25 | Address : 18545930 26 | AddressFamily : InterNetwork 27 | ScopeId : 28 | IsIPv6Multicast : False 29 | IsIPv6LinkLocal : False 30 | IsIPv6SiteLocal : False 31 | IsIPv6Teredo : False 32 | IsIPv4MappedToIPv6 : False 33 | IPAddressToString : 10.253.26.1 34 | #> 35 | -------------------------------------------------------------------------------- /Defender/MDI/Disable-NetAdapterLso.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Disable Large Send Offload on all network adapters. 4 | 5 | .DESCRIPTION 6 | This script disabled Large Send Offload (LSO) on all network adapters. It resolves a potential issue with Microsoft Defender 7 | for Identity (MDI) in which you might receive a health alert that states "Some netowkr traffic is not being analyzed." 8 | 9 | WARNING: Depending on your configuration, this might cause a brief loss of network connectivity when the adapter configuration changes. 10 | 11 | .NOTE 12 | Reference: https://docs.microsoft.com/en-us/defender-for-identity/troubleshooting-known-issues#vmware-virtual-machine-sensor-issue 13 | #> 14 | 15 | # Disable Large Send Offload (LSO) if it is enabled on domain controllers' virtual NICs. 16 | Write-Output "`nDisabling Large Send Offload..." 17 | (Get-NetAdapterAdvancedProperty).Where({ $_.DisplayName -Match '^Large*' }) | Disable-NetAdapterLso -Verbose -WhatIf 18 | 19 | Write-Output "`nNetwork adapters with Large Send Offload enabled: `n" 20 | Get-NetAdapterAdvancedProperty | Where-Object { $_.DisplayName -Match '^Large*' } 21 | -------------------------------------------------------------------------------- /Defender/MDI/Install-MDI.ps1: -------------------------------------------------------------------------------- 1 | # WORKING DRAFT - Don't save your access key here in plain text. 2 | 3 | # Install Defender for Identity Sensor (works on Windows Server Core). Install logs are at %AppData%\Local\Temp. 4 | $AccessKey = '' 5 | .\"Azure ATP sensor Setup.exe" /quiet NetFrameworkCommandLineArguments="/q" AccessKey=$AccessKey 6 | 7 | <# Review notes and script the creation of gMSAs and/or service accounts, and give credit: 8 | https://dirteam.com/sander/2022/03/23/howto-programmatically-add-a-microsoft-defender-for-identity-action-account-to-active-directory/ 9 | #> 10 | 11 | <# 12 | - Verify the machine has connectivity to the relevant Defender for Identity cloud service endpoint(s). https://learn.microsoft.com/en-us/defender-for-identity/configure-proxy#enable-access-to-defender-for-identity-service-urls-in-the-proxy-server 13 | - Extract the installation files from the zip file. Installing directly from the zip file will fail. 14 | - Run Azure ATP sensor setup.exe with elevated privileges (Run as administrator) and follow the setup wizard. 15 | - Install KB 3047154 for Windows Server 2012 R2 only. 16 | #> 17 | -------------------------------------------------------------------------------- /Defender/MDI/README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Defender XDR 2 | 3 | Scripts for working with Microsoft Defender XDR (Defender for Endpoint, Defender for Identity, and Defender for Office 365.) 4 | 5 | ## Defender for Identity 6 | 7 | ### Install-MDI.ps1 8 | 9 | For now this is just a quick script to install the Microsoft Defender for Identity on Windows Server Core. When done, it will also remove the old Microsoft Advanced Threat Analytics sensor if that is present. Use `Disable-NetAdapterLso` to disable LSO for all network adapters before installing MDI. 10 | 11 | ## To-Do 12 | 13 | - Configure Directory Services Advanced Auditing events according to the guidance as described in 14 | eg: Descendant Computer Objects (Schema-Id-Guid: bf967a86-0de6-11d0-a285-00aa003049e2) 15 | - Configure the Directory Services Object Auditing events according to the guidance as described in 16 | -------------------------------------------------------------------------------- /Defender/MDI/Remove-AtaSensor.ps1: -------------------------------------------------------------------------------- 1 | # Remove Advanced Threat Analytics (ATA) after Microsoft Defender for Identity (MDI) is installed. 2 | $ATA = Get-CimInstance -Class Win32_Product | Where-Object { $_.Name -like 'Microsoft Advanced Threat *' } 3 | $MDI = Get-CimInstance -Class Win32_Product | Where-Object { $_.Name -eq 'Azure Advanced Threat Protection Sensor' } 4 | 5 | if ($MDI) { 6 | Write-Output "Installation found: $ATA" 7 | $ATA.Uninstall() 8 | } 9 | 10 | # NOTE: Removal of the MDI Sensor seems to work better by running "Azure Advanced Threat Analytics Sensor.msi /uninstall" 11 | 12 | # Remove old version: 13 | & 'C:\ProgramData\Package Cache\{40d9b2a4-2356-4746-91dc-246f3b6b5bcb}\Azure ATP Sensor Setup.exe' /uninstall /quiet 14 | -------------------------------------------------------------------------------- /Entra/Get Entra Applications.ps1: -------------------------------------------------------------------------------- 1 | # Get a list of all Entra Applications and the most recent interactive sign-in for each 2 | Connect-MgGraph -Scopes AuditLog.Read.All 3 | 4 | Get-MgApplication -All | 5 | ForEach-Object { 6 | $x = Get-MgAuditLogSignIn -Filter "appId eq '$($_.AppId)'" -Top 1 -OrderBy 'createdDateTime desc' 7 | [pscustomobject]@{Id = $_.Id; DisplayName = $_.DisplayName; LastSignIn = $x.CreatedDateTime } 8 | } 9 | -------------------------------------------------------------------------------- /Entra/Get License Utilization Report.ps1: -------------------------------------------------------------------------------- 1 | # by Daniel Bradley 2 | # https://github.com/orgs/msgraph/discussions/95 3 | 4 | Connect-MgGraph -Scope Reports.Read.All 5 | 6 | $Report = Invoke-MgGraphRequest -Method GET -Uri "/beta/reports/azureADPremiumLicenseInsight" -OutputType PSObject 7 | 8 | $Report 9 | $Report.p1FeatureUtilizations 10 | $Report.p2FeatureUtilizations 11 | -------------------------------------------------------------------------------- /Entra/Get-DSReg.ps1: -------------------------------------------------------------------------------- 1 | function Get-DSReg { 2 | <# 3 | .SYNOPSIS 4 | Convert the output of dsregcmd.exe to a PowerShell object. 5 | #> 6 | $DSReg = [PSCustomObject]@{} 7 | $DSRegCmdOutput = (dsregcmd /status | Select-String '(^.*?) : (.*$)').Matches.Value 8 | foreach ($line in $DSRegCmdOutput) { 9 | $Detail = $line.Split(':', 2) 10 | $DetailName = ($Detail[0]).Replace(' ', '').Replace('-', '').Trim() 11 | $RawValue = ($Detail[1]).Trim() 12 | switch ($RawValue) { 13 | 'NO' { $CleanValue = $false } 14 | 'YES' { $CleanValue = $true } 15 | 'NOT SET' { $CleanValue = $null } 16 | 'none' { $CleanValue = $null } 17 | Default { $CleanValue = $RawValue } 18 | } 19 | 20 | $DSReg | Add-Member -MemberType NoteProperty -Name $DetailName -Value $CleanValue 21 | } 22 | 23 | $DSReg 24 | } 25 | -------------------------------------------------------------------------------- /Entra/Get-EntraTenantInfo.ps1: -------------------------------------------------------------------------------- 1 | function Get-EntraTenantInfo { 2 | [CmdletBinding()] 3 | param ( 4 | [Parameter()] 5 | [string] 6 | $DomainName 7 | ) 8 | 9 | ( Invoke-WebRequest -Uri https://login.windows.net/$DomainName/.well-known/openid-configuration ).Content | ConvertFrom-Json 10 | } 11 | -------------------------------------------------------------------------------- /Entra/Install Graph Modules for Identity.ps1: -------------------------------------------------------------------------------- 1 | # Install just the Graph PowerShell modules related to managing Entra ID: 2 | $IdentityModules = (@' 3 | Microsoft.Graph.DirectoryObjects 4 | Microsoft.Graph.Users 5 | Microsoft.Graph.Users.Actions 6 | Microsoft.Graph.Users.Functions 7 | Microsoft.Graph.Groups 8 | Microsoft.Graph.Identity.DirectoryManagement 9 | Microsoft.Graph.Identity.Governance 10 | Microsoft.Graph.Identity.SignIns 11 | Microsoft.Graph.Applications 12 | '@).Split([Environment]::NewLine) 13 | 14 | # Check if the pre-requisite modules are installed and install them if needed 15 | foreach ($module in $IdentityModules) { 16 | if (!(Get-Module -Name $module -ListAvailable)) { 17 | Install-Module -Name $module -Scope CurrentUser 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Entra/README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Entra 2 | 3 | Scripts for working with Microsoft Entra services and capabilities. 4 | -------------------------------------------------------------------------------- /Entra/Remove Entra ID Duplicate Devices - Windows.ps1: -------------------------------------------------------------------------------- 1 | Connect-MgGraph 2 | # Find duplicate devices by pulling all and grouping by DisplayName, then only keep a list where there are more than one entries with the same display name. 3 | $DuplicateDevices = Get-MgDevice -All | Group-Object DisplayName | Where-Object {$_.Count -gt 1 -and $_.OperatingSystem -eq "Windows"} | Sort-Object Count 4 | 5 | foreach ($group in $DuplicateDevices) { 6 | # For each group of duplicate DisplayName, 7 | # Expand the group object and sort it by the approximate last sign-in timestamp, 8 | # Then exclude the most recently active one so it is kept when the rest are removed. 9 | $OldDuplicates = $group | Select-Object -ExpandProperty Group | Sort-Object ApproximateLastSigninDateTime | Select-Object -SkipLast 1 10 | # Having excluded the most recent object, remove each "old" duplicate device in the group. 11 | $OldDuplicates | ForEach-Object { Remove-MgDevice -DeviceId $_.Id } 12 | } 13 | -------------------------------------------------------------------------------- /Entra/Test-Microsoft.Entra-Module.ps1: -------------------------------------------------------------------------------- 1 | Get-Module -ListAvailable -Refresh -Name Microsoft.Entra* | Format-Table Name, RequiredModules 2 | 3 | Write-Host ("{0} seconds:`tList commands with '`Get-Command -Module Microsoft.Entra.*' `n" -f [math]::Round( (Measure-Command { 4 | Get-Command -Module Microsoft.Entra.* 5 | } ).TotalSeconds, 2)) 6 | 7 | @('Applications', 'Authentication', 'DirectoryManagement', 'Governance', 'Groups', 'Reports', 'SignIns', 'Users') | ForEach-Object { 8 | Write-Host ("{0} seconds:`tMicrosoft.Entra.$_" -f [math]::Round((Measure-Command { 9 | Import-Module -Name "Microsoft.Entra.$_" 10 | }).TotalSeconds), 2) 11 | } 12 | -------------------------------------------------------------------------------- /Exchange/Basic Exchange Health Checks.ps1: -------------------------------------------------------------------------------- 1 | # Some basic Exchange Server checks to run after maintenance. 2 | Write-Information "Service Health" 3 | Test-ServiceHealth 4 | Write-Information "MAPI Connectivity" 5 | Test-MAPIConnectivity 6 | Write-Information "Mailbox Database Copy Status" 7 | Get-MailboxDatabaseCopyStatus 8 | Write-Information "Cluster Node Status" 9 | Get-ClusterNode 10 | Write-Information "Replication Health" 11 | Test-ReplicationHealth 12 | Write-Information "Server Component State" 13 | Get-ServerComponentState -Identity $env:COMPUTERNAME 14 | -------------------------------------------------------------------------------- /Exchange/Exchange Org Config Starter.ps1: -------------------------------------------------------------------------------- 1 | # Hybrid: The ACLableSyncedObjectEnabled parameter specifies whether remote mailboxes in hybrid environments are stamped as ACLableSyncedMailboxUser. 2 | Set-OrganizationConfig -ACLableSyncedObjectEnabled $True 3 | 4 | # EXO: Turn off focused inbox 5 | Set-OrganizationConfig -FocusedInboxOn $false 6 | 7 | # EXO: Enable unified audit log 8 | Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $true 9 | Get-AdminAuditLogConfig | Format-List UnifiedAuditLogIngestionEnabled 10 | -------------------------------------------------------------------------------- /Exchange/Get Email Addresses with a Digit Before the At Sign.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Get all Exchange mailboxes that have an alias that ends with a digit or a primary email address that has a digit 4 | immediately preceding the @ symbol. 5 | .DESCRIPTION 6 | Get all Exchange mailboxes that have an alias that ends with a digit or a primary email address that has a digit 7 | immediately preceding the @ symbol. 8 | .NOTES 9 | Author: Sam Erde 10 | Modified: 2024-06-17 11 | Version: 0.0.2 12 | #> 13 | 14 | $NumberAts = Get-Mailbox -ResultSize Unlimited -SortBy alias | Where-Object { 15 | $_.alias -match '\d$' -or $_.PrimarySmtpAddress -match '^.*\d@.*$' 16 | } 17 | $NumberAts | 18 | Select-Object DisplayName, alias, EmailAddressPolicyEnabled, WindowsEmailAddress, PrimarySmtpAddress, 19 | @{Name = 'SmtpAddresses'; Expression = { $_.emailaddresses.smtpAddress -join ', ' } } | 20 | ConvertTo-Csv -NoTypeInformation -Delimiter ';' | 21 | Set-Clipboard 22 | -------------------------------------------------------------------------------- /Exchange/Get Receive Connectors with External Relay Permissions.ps1: -------------------------------------------------------------------------------- 1 | $ReceiveConnectors = (Get-ExchangeServer | Get-ReceiveConnector).Where({$_.Enabled -eq $true}) | Sort-Object Name,Identity 2 | $ExternalRelays = @() 3 | foreach ($rc in $ReceiveConnectors) { 4 | $dn = $rc.DistinguishedName 5 | if (Get-ADPermission -Identity $dn -User "NT AUTHORITY\ANONYMOUS LOGON" | Where-Object {$_.ExtendedRights.RawIdentity -eq "MS-Exch-SMTP-Accept-Any-Recipient"}) { 6 | $ExternalRelays += $rc 7 | } 8 | } 9 | 10 | $ExternalRelays | Select-Object Name,Server,Enabled -Unique 11 | -------------------------------------------------------------------------------- /Exchange/Get-AliasConflicts.ps1: -------------------------------------------------------------------------------- 1 | function Get-AliasConflicts { 2 | <# 3 | .SYNOPSIS 4 | Get all conflicting recipient aliases in Exchange. 5 | .DESCRIPTION 6 | Get all instances of name conflicts where multiple Exchange recipients have the same alias. 7 | .NOTES 8 | Author: Sam Erde 9 | Modified: 2024-06-18 10 | Version: 0.0.3 11 | #> 12 | [CmdletBinding()] 13 | [OutputType([Microsoft.Exchange.Data.Directory.Management.ReducedRecipient], [Microsoft.PowerShell.Commands.GroupInfo])] 14 | param ( 15 | # Optionally specify the Resultsize parameter as an integer or 'Unlimited' 16 | [ValidatePattern('^(\d+|Unlimited)$')] 17 | $Resultsize = 'Unlimited', 18 | 19 | # Optionally copy the results to the clipboard as a CSV 20 | [switch] 21 | $Clip 22 | ) 23 | 24 | Write-Information "Checking $Resultsize recipients..." 25 | $Recipients = Get-Recipient -Resultsize $Resultsize -SortBy alias 26 | $OverlappingAliases = $Recipients | Group-Object -Property alias | Where-Object { $_.Count -gt 1 } 27 | 28 | # Replace domainName.tld below. This replace statement is simply used to add commas between recipient IDs. 29 | $AliasConflicts = $OverlappingAliases | 30 | Select-Object Count, 31 | @{Name = 'Alias'; Expression = { $_.Name } }, 32 | @{Name = 'ConflictedRecipients'; Expression = { [string]($_.Group) -Replace ' domainName.tld/', ', domainName.tld/' } } 33 | 34 | if ($Clip) { 35 | $AliasConflicts | ConvertTo-Csv -NoTypeInformation -Delimiter ';' | Set-Clipboard 36 | } 37 | 38 | $AliasConflicts 39 | } 40 | -------------------------------------------------------------------------------- /Exchange/Get-ExchangeVirtualDirectories.ps1: -------------------------------------------------------------------------------- 1 | function Get-ExchangeVirtualDirectories { 2 | <# 3 | .SYNOPSIS 4 | Get all Exchange Server virtual directories on the current server. 5 | .DESCRIPTION 6 | A fun way to find every cmdlet that gets Exchange virtual directories and then pipe that list 7 | to run it on the current server. 8 | #> 9 | foreach ( $command in ( (Get-Command "Get*VirtualDirectory" -CommandType Function).Name ) ) { 10 | & $command -server $env:COMPUTERNAME | Format-List Name,InternalUrl,ExternalUrl 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Exchange/Get-MailboxDatabaseSize.ps1: -------------------------------------------------------------------------------- 1 | function Get-MailboxDatabaseSize { 2 | [CmdletBinding()] 3 | [Alias('Get-DatabaseSize')] 4 | param () 5 | 6 | Get-MailboxDatabase -Status | Sort-Object Name | ` 7 | Select-Object Name, 8 | @{Name = 'DB Size (Gb)'; Expression = { $_.DatabaseSize.ToGb() } }, 9 | @{Name = 'DB Free Whitespace (Gb)'; Expression = { $_.AvailableNewMailboxSpace.ToGb() } } 10 | } 11 | -------------------------------------------------------------------------------- /Exchange/MailboxPermissionsAllFolders.ps1: -------------------------------------------------------------------------------- 1 | $user = Read-Host -Prompt 'Enter the username of the mailbox that you are granting access to' 2 | $newuser = Read-Host -Prompt 'Enter the username of the user gaining access' 3 | 4 | 5 | ForEach ($folder in (Get-MailboxFolderStatistics -identity $user)) { 6 | 7 | $foldername = "$user" + $folder.identity.replace('\', ':\') 8 | 9 | Add-MailboxFolderPermission $foldername -User $newuser -AccessRights $ar 10 | 11 | } 12 | -------------------------------------------------------------------------------- /Exchange/README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Exchange Server 2 | 3 | Scripts for working with Exchange Server on-premises. 4 | -------------------------------------------------------------------------------- /Exchange/Remove Disabled Users from Distribution Groups.ps1: -------------------------------------------------------------------------------- 1 | $Groups = Get-DistributionGroup -ResultSize Unlimited 2 | 3 | foreach ($group in $Groups) { 4 | Write-Information "$group" 5 | Get-DistributionGroupMember $group | 6 | Where-Object { $_.RecipientType -like '*User*' -and $_.ResourceType -eq $null } | 7 | Get-User | Where-Object { $_.UserAccountControl -match 'AccountDisabled' } | 8 | Remove-DistributionGroupMember $group -Confirm:$false 9 | } 10 | -------------------------------------------------------------------------------- /Exchange/Remove Old Exchange Server Log Files.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | A script to clear old Exchange logs files. 4 | .DESCRIPTION 5 | This script will clear all Exchange Server log files under the installation's "Logging" path that are older than a set 6 | number of days. (It will not touch database log files unless those are intentionally put in the worst possible location!) 7 | #> 8 | 9 | # Set the number of days to preserve logs from. 10 | $LogAgeDays = 14 11 | 12 | # Check for the existince of the Exchange logging folder under the install path. 13 | if (Test-Path -Path "$env:ExchangeInstallPath\Logging") { 14 | Set-Location -Path "$env:ExchangeInstallPath\Logging" -ErrorAction Stop 15 | 16 | # Get Exchang Server logs that are older than [nn] days and remove them. Any log files still in use will not be deleted. 17 | Get-ChildItem -Path "$env:ExchangeInstallPath\Logging" -Recurse -Include *.log | ` 18 | Where-Object { $_.LastWriteTime -lt ((Get-Date).AddDays(-$LogAgeDays)) } | ` 19 | Remove-Item -Confirm:$false -ErrorAction SilentlyContinue 20 | } 21 | -------------------------------------------------------------------------------- /Exchange/Reset-HealthMailboxes.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | You can safely clear out Exchange Health Mailboxes when there is corruption, errors, or orphaned health mailboxes. 4 | 5 | ## Step 1 6 | 7 | Get all health mailboxes from CN=Monitoring Mailboxes,CN=Microsoft Exchange System Objects,DC=DOMAINNAME,DC=org 8 | 9 | ## Step 2 10 | 11 | Delete them all 12 | 13 | ## Step 3 14 | 15 | Restart the ExchangeHM Service 16 | -------------------------------------------------------------------------------- /Exchange/Set-JunkMailSettings.ps1: -------------------------------------------------------------------------------- 1 | $mbxs = Get-Mailbox "Sam Erde" #-Resultsize 10 2 | foreach ($mbx in $mbxs) { 3 | $mbxconfig = Get-MailboxJunkEmailConfiguration $mbx 4 | $mbxconfig.TrustedSendersAndDomains 5 | write-output " `n" 6 | $mbxconfig.TrustedSendersAndDomains += "","" 7 | Set-MailboxJunkEmailConfiguration -Identity $mbx -TrustedSendersAndDomains $mbxConfig.TrustedSendersAndDomains #-BlockedSendersAndDomains $mbxconfig.BlockedSendersAndDomains 8 | } 9 | -------------------------------------------------------------------------------- /Exchange/StartMaintenance.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Start Exchange Server maintenance, with recommended commands from Microsoft's KB at: 3 | https://learn.microsoft.com/en-us/exchange/high-availability/manage-ha/manage-dags?view=exchserver-2019#performing-maintenance-on-dag-members 4 | #> 5 | 6 | $CurrentServer = "$env:computername" 7 | # Get a random number between 1 and [the count of servers in the DAG]. 8 | $Random = Get-Random -Maximum ( (Get-DatabaseAvailabilityGroup).Servers.Name.Count ) -Minimum 1 9 | 10 | # Select a random server name from the list of servers in the DAG, excluding the current server's name. 11 | # Subtract 1 to account for the array index starting at 0 instead of 1. 12 | # Depends on the ActiveDirectory PowerShell module to get the FQDN. 13 | $AlternateServer = ((Get-DatabaseAvailabilityGroup).Servers).Where({$_.Name -ne $CurrentServer}).Name | Select-Object -Index ($Random -1) 14 | $AlternateServerFQDN = "$AlternateServer.$((Get-ADDomain).DNSRoot)" 15 | 16 | # ===== BEGIN MAINTENANCE ===== 17 | 18 | # Drain the transport queues 19 | Set-ServerComponentState $CurrentServer -Component HubTransport -State Draining -Requester Maintenance 20 | Restart-Service MSExchangeTransport 21 | # Drain the UM call queue if that service is in use. 22 | # Set-ServerComponentState $CurrentServer -Component UMCallRouter -State Draining -Requester Maintenance 23 | Set-Location -Path $ExScripts 24 | # Start DAG server maintenance. 25 | .\StartDagServerMaintenance.ps1 -ServerName $CurrentServer -MoveComment Maintenance -PauseClusterNode 26 | # Rediret messages to a random alternate server in the DAG. 27 | Redirect-Message -Server $CurrentServer -Target $AlternateServerFQDN 28 | # Set all server components offline for maintenance. 29 | Set-ServerComponentState $CurrentServer -Component ServerWideOffline -State Inactive -Requester Maintenance -------------------------------------------------------------------------------- /Exchange/StopMaintenance.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Stop Exchange Server maintenance, with recommended commands from Microsoft's KB at: 3 | https://learn.microsoft.com/en-us/exchange/high-availability/manage-ha/manage-dags?view=exchserver-2019#performing-maintenance-on-dag-members 4 | #> 5 | 6 | $CurrentServer = "$env:computername" 7 | 8 | # END MAINTENANCE 9 | Set-ServerComponentState $CurrentServer -Component ServerWideOffline -State Active -Requester Maintenance 10 | # Set-ServerComponentState $CurrentServer -Component UMCallRouter -State Active -Requester Maintenance 11 | Set-Location $ExScripts 12 | .\StopDagServerMaintenance.ps1 -serverName $CurrentServer 13 | Set-ServerComponentState $CurrentServer -Component HubTransport -State Active -Requester Maintenance 14 | Restart-Service MSExchangeTransport 15 | Get-ServerComponentState $CurrentServer | Format-Table Component,State -Autosize 16 | -------------------------------------------------------------------------------- /General/Copy-ToVMwareGuest.ps1: -------------------------------------------------------------------------------- 1 | function Copy-FileToVMwareGuest { 2 | [CmdletBinding()] 3 | param ( 4 | [Parameter()] 5 | [string] 6 | $vCenterServer, 7 | [Parameter()] 8 | [string] 9 | $GuestVM, 10 | [Parameter()] 11 | [string] 12 | $SourcePath, 13 | [Parameter()] 14 | [string] 15 | $DestinationPath = 'C:\Temp' 16 | ) 17 | Import-Module VMware.PowerCLI 18 | $Null = Set-PowerCLIConfiguration -InvalidCertificateAction Prompt -DefaultVIServerMode Multiple -Confirm:$False -Scope Session 19 | Connect-VIServer -Server $vCenterServer -Credential (Get-Credential -Message 'Enter credentials for vCenter.') 20 | 21 | $GuestCredential = (Get-Credential -Message 'Enter administrative credentials for the guest VM.') 22 | 23 | try { 24 | Copy-VMGuestFile -LocalToGuest -Source $SourcePath -Destination $DestinationPath -VM $GuestVM -GuestCredential $GuestCredential -Force 25 | } catch { 26 | $Error 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /General/ExperimentalFeatures.ps1: -------------------------------------------------------------------------------- 1 | Get-ExperimentalFeature | Sort-Object Enabled, Name | Format-Table Name, Enabled, Description 2 | Write-Output 'See for more information about experimental features in PowerShell.' 3 | -------------------------------------------------------------------------------- /General/Get Latest PowerShell Download URLs.ps1: -------------------------------------------------------------------------------- 1 | # Get the URLs to download the latest builds of PowerShell 2 | $latest = (Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest') 3 | $latest.assets.browser_download_url 4 | -------------------------------------------------------------------------------- /General/Get-NtpSourceType.ps1: -------------------------------------------------------------------------------- 1 | $NTPTypePattern = "^Type: (?'TypeValue'\w+) \({1}\w+\)" 2 | $MatchNTP = w32tm.exe /query /configuration | Select-String -Pattern $NTPTypePattern 3 | $NTPSourceType = $MatchNTP.Matches.Groups[1].Value 4 | $NTPSourceType 5 | # Should return 'NT5DS' for a domain joined machine, 'NTP' for others, etc. 6 | 7 | # Get the NTP Server and Type from the registry 8 | $RegistryPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\W32Time' 9 | [PSCustomObject]@{ 10 | Type = (Get-ItemProperty -Path "$RegistryPath\Parameters" -Name Type).Type 11 | Server = (Get-ItemProperty -Path "$RegistryPath\Parameters" -Name NtpServer).NtpServer 12 | LastKnownGoodTime = [datetime]::FromFileTime( (Get-ItemProperty -Path "$RegistryPath\Config" -Name LastKnownGoodTime).LastKnownGoodTime ) 13 | } 14 | -------------------------------------------------------------------------------- /General/Get-PingResultsHashTable.ps1: -------------------------------------------------------------------------------- 1 | function Get-PingResultsHashTable { 2 | [CmdletBinding()] 3 | param ( 4 | # A server name or array of server names 5 | [Parameter(Mandatory)] 6 | [string[]] 7 | $ServerList 8 | ) 9 | 10 | # Build an ordered hash table that has the server name as the key and the IP address [and any errors] as the value. 11 | $Servers = [ordered]@{} 12 | $ServerList | ForEach-Object { 13 | $Results = Test-NetConnection $_ -InformationLevel Detailed 14 | if ($Results.PingSucceeded) { 15 | $Servers.Add( $Results.ComputerName, $($Results.ResolvedAddresses.Where( 16 | { $_.AddressFamily -eq 'InterNetwork' }).IPAddressToString) ) 17 | } else { 18 | $Servers.Add( $Results.ComputerName, "$($Results.ResolvedAddresses.Where( 19 | {$_.AddressFamily -eq 'InterNetwork'}).IPAddressToString): $($Error[0].Exception.Message)" ) 20 | } 21 | } 22 | $Servers 23 | } 24 | -------------------------------------------------------------------------------- /General/Install-PowerShellAsDotNetTool.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Install PowerShell as a .NET global tool, which does not require Administrator privileges. 4 | .DESCRIPTION 5 | This script installs PowerShell as a .NET global tool, which does not require Administrator privileges. It begins by 6 | downloading the required .NET Tool install script, which is then used to install PowerShell. 7 | .NOTES 8 | Author: Sam Erde (@SamErde) 9 | Date: 2025-02-12 10 | .LINK 11 | https://github.com/SamErde/PowerShell/General 12 | #> 13 | 14 | [CmdletBinding()] 15 | param () 16 | 17 | # Download the .NET CLI install script if it is not found. 18 | if ( (Get-Command -Name 'dotnet' -ErrorAction SilentlyContinue) ) { 19 | Write-Verbose 'dotnet is already installed.' 20 | } else { 21 | $DownloadPath = Join-Path -Path $env:TEMP -ChildPath 'dotnet-install.ps1' 22 | try { 23 | Invoke-WebRequest 'https://dot.net/v1/dotnet-install.ps1' -OutFile $DownloadPath 24 | Unblock-File -Path $DownloadPath 25 | } catch { 26 | Write-Error "Failed to download dotnet-install.ps1 to '$DownloadPath'." 27 | throw $_ 28 | } 29 | 30 | # Install the dotnet tool. The script installs the latest LTS release by default. 31 | # The current stable release is required for PowerShell 7.5, which depends on .NET 9. 32 | try { 33 | .$DownloadPath -InstallDir '~/.dotnet' -Channel 'STS' # use of 'Current' is deprecated. 34 | } catch { 35 | throw $_ 36 | } 37 | 38 | } 39 | 40 | # Install PowerShell and add $HOME\.dotnet\tools to the PATH. 41 | try { 42 | dotnet tool install --global PowerShell 43 | $env:PATH += ';' + [System.IO.Path]::Combine($HOME, '.dotnet', 'tools') 44 | } catch { 45 | throw $_ 46 | } 47 | 48 | # Clean up. 49 | if (Test-Path -Path $DownloadPath) { 50 | Remove-Item -Path $DownloadPath -Confirm:$false 51 | } 52 | Remove-Variable -Name DownloadPath 53 | -------------------------------------------------------------------------------- /General/Measure-CommandInventory.ps1: -------------------------------------------------------------------------------- 1 | function Measure-CommandCount { 2 | # Count the number of unique commands available. 3 | (Get-Command | Sort-Object -Property Source, Version | Group-Object -Property Name, Source -NoElement | 4 | Measure-Object).Count 5 | } 6 | 7 | function Measure-ModuleInventory { 8 | <# 9 | List all available modules and the commands in each one. 10 | #> 11 | $Inventory = [ordered]@{} 12 | 13 | $Commands = Get-Command | Sort-Object -Property Source, Name, Version | 14 | Select-Object Source, Name, Version | Group-Object -Property Source, Name -NoElement | 15 | Select-Object -ExpandProperty Name 16 | 17 | $Commands | ForEach-Object { 18 | $Module, $Command = $_ -split ', ' 19 | 20 | if ([string]::IsNullOrEmpty($Module)) { 21 | $Module = '_UnnamedSourceModule_' 22 | } 23 | 24 | if ($Inventory.Contains($Module)) { 25 | $Inventory[$Module] += $Command 26 | } else { 27 | $Inventory[$Module] = @($Command) 28 | } 29 | } 30 | 31 | $Inventory 32 | } 33 | 34 | $Inventory = Measure-ModuleInventory 35 | $Inventory.Count 36 | 37 | $CommandCount = 0 38 | foreach ($item in $Inventory.GetEnumerator()) { 39 | $CommandCount += $item.Value.Count 40 | } 41 | $CommandCount 42 | -------------------------------------------------------------------------------- /General/Purge-InstalledModules.ps1: -------------------------------------------------------------------------------- 1 | # Cleaning up multiple version of modules that are not needed or I wanted to reset. 2 | $Modules = @( 3 | 'Az', 4 | 'Az.Batch', 5 | 'Az.Cdn', 6 | 'Az.Compute', 7 | 'Az.CosmosDB', 8 | 'Az.Databricks', 9 | 'Az.DataFactory', 10 | 'Az.DataLakeStore', 11 | 'Az.FrontDoor', 12 | 'Az.FrontDoor', 13 | 'Az.KeyVault', 14 | 'Az.MachineLearningServices', 15 | 'Az.Maintenance', 16 | 'Az.Migrate', 17 | 'Az.Migration', 18 | 'Az.MySql', 19 | 'Az.Network', 20 | 'Az.Nginx', 21 | 'Az.RedisCache' 22 | 'Az.Sql', 23 | 'Az.SqlVirtualMachine', 24 | 'Az.StackHCI', 25 | 'Az.StorageMover', 26 | 'Az.StorageSync', 27 | 'Az.Synapse', 28 | 'ClipboardTools', 29 | 'Curl2PS', 30 | 'F7History', 31 | 'Microsoft.Graph', 32 | 'Microsoft.Graph.Beta.Applications', 33 | 'Microsoft.Graph.Beta.DirectoryObjects', 34 | 'Microsoft.Graph.Beta.Groups', 35 | 'Microsoft.Graph.Beta.Groups', 36 | 'Microsoft.Graph.Beta.Groups', 37 | 'Microsoft.Graph.Beta.Identity.DirectoryManagement', 38 | 'Microsoft.Graph.Beta.Identity.Governance', 39 | 'Microsoft.Graph.Beta.Identity.Governance', 40 | 'Microsoft.Graph.Beta.Identity.SignIns', 41 | 'Microsoft.Graph.Beta.Reports', 42 | 'Microsoft.Graph.Beta.Reports', 43 | 'Microsoft.Graph.Beta.Users', 44 | 'Microsoft.Graph.Beta.Users', 45 | 'Microsoft.Graph.Beta.Users.Actions', 46 | 'Microsoft.Graph.Beta.Users.Functions', 47 | 'Microsoft.Graph.CrossDeviceExperiences', 48 | 'Microsoft.Graph.DeviceManagement.Actions', 49 | 'Microsoft.Graph.DeviceManagement.Administration', 50 | 'Microsoft.Graph.DeviceManagement.Enrollment', 51 | 'Microsoft.Graph.DeviceManagement.Functions', 52 | 'Microsoft.Graph.Devices.CorporateManagement', 53 | 'Microsoft.Graph.Devices.ServiceAnnouncement', 54 | 'OdmApi', 55 | 'PSAdvantage' 56 | ) 57 | $Modules | ForEach-Object { Write-Output $_ ; Uninstall-Module $_ -Force -AllVersions -AllowPrerelease -ErrorAction SilentlyContinue } 58 | Get-InstalledModule | Format-Table Version, Name 59 | -------------------------------------------------------------------------------- /General/Set-EnvironmentVariable.ps1: -------------------------------------------------------------------------------- 1 | function Set-EnvironmentVariable { 2 | [Alias('sev')] 3 | [CmdletBinding()] 4 | param ( 5 | # The name of the environment variable to set. 6 | [Parameter(Mandatory)] 7 | [string]$Name, 8 | 9 | # The value of environment variable to set. 10 | [Parameter(Mandatory)] 11 | [string] 12 | $Value, 13 | 14 | # The target of the environment variable to set. 15 | [Parameter()] 16 | [System.EnvironmentVariableTarget] 17 | $Target 18 | ) 19 | 20 | begin { 21 | 22 | } 23 | 24 | process { 25 | [Environment]::SetEnvironmentVariable($Name, $Value, $Target) 26 | } 27 | 28 | end { 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /General/Test-IsLocalAccountSession.ps1: -------------------------------------------------------------------------------- 1 | function Test-IsLocalAccountSession { 2 | <# 3 | .SYNOPSIS 4 | Tests if the current session is running under a local user account or a domain account. 5 | .DESCRIPTION 6 | This function returns True if the current session is a local user or False if it is a domain user. 7 | .EXAMPLE 8 | Test-IsLocalAccountSession 9 | .EXAMPLE 10 | if ( (Test-IsLocalAccountSession) ) { Write-Host "You are running this script under a local account." -ForeGroundColor Yellow } 11 | #> 12 | [CmdletBinding()] 13 | 14 | $CurrentSID = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value 15 | $LocalSIDs = (Get-LocalUser).SID.Value 16 | if ($CurrentSID -in $LocalSIDs) { 17 | Return $true 18 | } 19 | } 20 | 21 | # This could be one function that accepts either a name or a SID as a parameter, but using SIDs just makes sense! 22 | function Test-IsLocalAccountByName { 23 | [CmdletBinding()] 24 | 25 | #Get the current username 26 | $User = [Security.Principal.WindowsIdentity]::GetCurrent().Name 27 | 28 | # Check if the user name contains a backslash (\) 29 | if ($User -match '\\') { 30 | # Check if the domain name is equal to the computer name 31 | if ( ($User.split('\'))[0] -eq ($env:computername) ) { 32 | # The current session is running under a local user account 33 | Return $true 34 | } 35 | else { 36 | # Domain account 37 | Return $false 38 | } 39 | } 40 | else { 41 | # The current session is running in a local user account without a domain prefix 42 | Return $true 43 | } 44 | } -------------------------------------------------------------------------------- /General/Test-IsPalindrome.ps1: -------------------------------------------------------------------------------- 1 | function Test-IsPalindrome { 2 | <# 3 | .SYNOPSIS 4 | Test a string to see if it is a palindrome. 5 | 6 | .DESCRIPTION 7 | This function tests a string to see if it is a palindrome. A palindrome is a word, phrase, number, or other sequence of characters that reads the same forward and backward. 8 | 9 | .PARAMETER String 10 | The string to test. 11 | 12 | .EXAMPLE 13 | Test-IsPalindrome -String "racecar" 14 | 15 | The string "racecar" is a palindrome, so the function returns $true. 16 | 17 | .EXAMPLE 18 | '() ()' | Test-IsPalindrome 19 | 20 | The string "() ()" is not a palindrome, so the function returns $false. 21 | 22 | .EXAMPLE 23 | '() )(' | Test-IsPalindrome 24 | 25 | The string "() )(" is a palindrome, so the function returns $true. 26 | #> 27 | [CmdletBinding()] 28 | [OutputType([bool])] 29 | param ( 30 | # The string to test 31 | [Parameter(Mandatory, Position = 0, ValueFromPipeline)] 32 | [ValidateNotNullOrEmpty()] 33 | [string] 34 | $String 35 | ) 36 | 37 | process { 38 | $ReversedString = -join ($String.ToLower().ToCharArray() | ForEach-Object { $_ })[-1.. - ($String.Length)] 39 | "`n$String || $ReversedString" | Write-Verbose 40 | $Result = $String -eq $ReversedString 41 | } 42 | 43 | end { 44 | $Result 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /General/Test-ModuleInstall.ps1: -------------------------------------------------------------------------------- 1 | function Test-ModuleInstall { 2 | [CmdletBinding(SupportsShouldProcess)] 3 | param ( 4 | # Name of the module to check and install. 5 | [Parameter(Mandatory, Position = 0)] 6 | [string] 7 | $Name 8 | ) 9 | 10 | if (Get-Command -Module $Name -ErrorAction SilentlyContinue) { 11 | Write-Output "$Name module is already installed." 12 | return 13 | } 14 | 15 | # Check all installed modules. 16 | if (Get-Module -Name $Name -ListAvailable -ErrorAction SilentlyContinue) { 17 | Write-Output "The `'$Name`' module is already installed." 18 | return 19 | } 20 | 21 | # Check if running with administrator rights before installing. 22 | $isAdministrator = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) 23 | if (-not $isAdministrator) { 24 | Write-Output "Please run as an administrator to install the `'$Name`' module." 25 | return 26 | } 27 | 28 | # Ask user if they want to install the module 29 | if ($PSCmdlet.ShouldProcess("$Name module", 'Install')) { 30 | Write-Verbose "Installing `'$Name`' module..." 31 | Install-Module -Name "$Name" -Scope CurrentUser -Force -AllowClobber -Verbose 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Microsoft 365/.codacy/codacy.yaml: -------------------------------------------------------------------------------- 1 | runtimes: 2 | - dart@3.7.2 3 | - go@1.22.3 4 | - java@17.0.10 5 | - node@22.2.0 6 | - python@3.11.11 7 | tools: 8 | - dartanalyzer@3.7.2 9 | - eslint@8.57.0 10 | - lizard@1.17.19 11 | - pmd@7.11.0 12 | - pylint@3.3.6 13 | - revive@1.7.0 14 | - semgrep@1.78.0 15 | - trivy@0.59.1 16 | -------------------------------------------------------------------------------- /Microsoft 365/Check Teams Client cloudEnvironment.ps1: -------------------------------------------------------------------------------- 1 | # Check which cloudEnvironment's Teams client is installed (worldwide, gcc, gcc-high) 2 | Get-Content $env:UserProfile"\AppData\Roaming\Microsoft\Teams\settings.json" | ConvertFrom-Json | Select-Object Version,Ring,Environment,CloudEnvironment -------------------------------------------------------------------------------- /Microsoft 365/Check for new CSV of Microsoft product names and service plan identifiers.ps1: -------------------------------------------------------------------------------- 1 | # Check Microsoft Learn for a new download of the Product Names and Service Plan Identifiers CSV file. 2 | $Response = Invoke-WebRequest -Uri 'https://learn.microsoft.com/en-us/entra/identity/users/licensing-service-plan-reference' 3 | $LicensingCsvDownloadLink = ( $Response.Links.Where({ $_.href -match 'licensing.csv' -or $_.href -match 'Product%20names%20and%20service%20plan' }) ).href 4 | $LicensingCsvDownloadLink 5 | -------------------------------------------------------------------------------- /Microsoft 365/Connect-ExchangeOnlineWorkaround.ps1: -------------------------------------------------------------------------------- 1 | function Connect-ExchangeOnlineWorkaround { 2 | # Get a token and connect to Exchange Online 3 | # Inspired by https://david-homer.blogspot.com/2025/01/exchange-online-management-powershell.html 4 | 5 | <# To Do: 6 | - Choose different environments to connect to 7 | - Add error handling 8 | #> 9 | $ExoMsalPath = [System.IO.Path]::GetDirectoryName((Get-InstalledModule -Name 'ExchangeOnlineManagement').Path) 10 | Add-Type -Path "$ExoMsalPath\Microsoft.IdentityModel.Abstractions.dll" 11 | Add-Type -Path "$ExoMsalPath\Microsoft.Identity.Client.dll" 12 | [Microsoft.Identity.Client.IPublicClientApplication] $ExoApplication = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create('fb78d390-0c51-40cd-8e17-fdbfab77341b').WithDefaultRedirectUri().Build() 13 | $ExoAuthToken = $ExoApplication.AcquireTokenInteractive([string[]]'https://outlook.office365.com/.default').ExecuteAsync().Result 14 | Connect-ExchangeOnline -AccessToken $ExoAuthToken.AccessToken -UserPrincipalName $ExoAuthToken.Account.Username 15 | } 16 | -------------------------------------------------------------------------------- /Microsoft 365/Get Viva Insights Users with no Manager.ps1: -------------------------------------------------------------------------------- 1 | # Install the required modules if they are not already installed 2 | try { 3 | if (-not (Get-Module -Name 'Microsoft.Graph.Authentication' -ListAvailable)) { 4 | Install-Module -Name 'Microsoft.Graph.Authentication' -Scope CurrentUser -Force -AllowClobber 5 | } 6 | if (-not (Get-Module -Name 'Microsoft.Graph.Users' -ListAvailable)) { 7 | Install-Module -Name 'Microsoft.Graph.Users' -Scope CurrentUser -Force -AllowClobber 8 | } 9 | } catch { 10 | Write-Warning "Failed to find or install the required modules. $_" 11 | return 12 | } 13 | 14 | # Import the required modules 15 | Import-Module -Name 'Microsoft.Graph.Authentication' 16 | Import-Module -Name 'Microsoft.Graph.Users' 17 | 18 | # Connect to Microsoft Graph with a specific tenant ID and permission scopes. 19 | $TenantId = "__________" 20 | Connect-Graph -TenantId $TenantId -NoWelcome -Scopes "User.Read.All","User.ReadWrite.All" 21 | 22 | # Get all users 23 | $Users = Get-MgUser -All:$true -ConsistencyLevel eventual 24 | 25 | # Create an array to store the gathered user details 26 | [System.Collections.Generic.List[PSCustomObject]]$Details = @() 27 | 28 | # Loop through each user and get specific details. Only capturing the manager and Viva Insights license (WORKPLACE_ANALYTICS) if they are assigned. 29 | foreach ($u in $users) { 30 | $Details.Add( [PSCustomObject]@{ 31 | DisplayName = $u.DisplayName 32 | Title = $u.JobTitle 33 | Department = $u.Department 34 | Manager = if (Get-MgUserManager -UserId $($u.id) -ErrorAction Ignore) { ( Get-MgUser -UserId $((Get-MgUserManager -UserId $($u.id)).Id) ).DisplayName } else { $null } 35 | DirectReportCount = (Get-MgUserDirectReport -UserId $($u.Id)).Count 36 | VivaInsightsLicense = if ( (Get-MgUserLicenseDetail -UserId $($u.id)).SkuPartNumber -contains 'WORKPLACE_ANALYTICS' ) { $true } else { $false} 37 | UserPrincipalName = $u.UserPrincipalName 38 | CompanyName = $u.CompanyName 39 | } ) 40 | } 41 | 42 | # Show all users who are licensed for Viva Insights (WORKPLACE_ANALYTICS) but do not have a manager assigned in Entra ID 43 | $NoManager = $Details | Where-Object { 44 | ( $_.VivaInsightsLicense -eq $true -and [string]::IsNullOrEmpty($_.Manager) ) 45 | } 46 | # Show the results as a table 47 | $NoManager | Format-Table -AutoSize 48 | 49 | # Get disabled users that still have a Viva Insights license. 50 | $DisabledUsers = Get-MgUser -All:$true -Filter "accountEnabled eq false" -ConsistencyLevel eventual 51 | foreach ($u in $DisabledUsers) { 52 | if ( (Get-MgUserLicenseDetail -UserId $($u.id)).SkuPartNumber -contains 'WORKPLACE_ANALYTICS' ) { $true } else { $false} 53 | } 54 | -------------------------------------------------------------------------------- /Microsoft 365/Get-UserTeamsMembership.ps1: -------------------------------------------------------------------------------- 1 | function Get-UserTeamsMembership { 2 | <# 3 | .SYNOPSIS 4 | Get all teams that the given user is a member of in Microsoft Teams. 5 | 6 | .DESCRIPTION 7 | This function retrieves all teams that the specified user is a member of in Microsoft Teams. 8 | It connects to the Microsoft Teams and Microsoft Graph services using the provided tenant ID. 9 | 10 | .PARAMETER UserId 11 | The UserID of the user whose team memberships you want to retrieve. 12 | 13 | .PARAMETER TenantId 14 | The tenant ID to connect to. 15 | 16 | .EXAMPLE 17 | Get-UserTeamsMembership -UserId jdoe@example.com -TenantId $TenantId 18 | 19 | Retrieves all teams that the user with UserID 'jdoe@example.com' 20 | 21 | #> 22 | [CmdletBinding()] 23 | param ( 24 | # The UserID of the user whose team memberships you want to retrieve. 25 | [Parameter(Mandatory, ValueFromPipelineByPropertyName)] 26 | [string]$UserId, 27 | 28 | # The tenant ID to connect to. 29 | [Parameter(Mandatory)] 30 | [string]$TenantId 31 | ) 32 | 33 | begin { 34 | # Ensure we're connected to the required services 35 | try { 36 | Connect-MicrosoftTeams -TenantId $TenantId -ErrorAction Stop 37 | Connect-MgGraph -TenantId $TenantId -ErrorAction Stop 38 | } catch { 39 | Write-Error "Failed to connect to Microsoft 365 services: $_" 40 | return 41 | } 42 | } 43 | 44 | process { 45 | try { 46 | $User = Get-MgUser -UserId $UserId -ErrorAction Stop 47 | Get-Team -User $User.Id 48 | } catch { 49 | Write-Error "Failed to get team memberships for user '$UserId': $_" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Microsoft 365/GetAllTenantUsers.ps1: -------------------------------------------------------------------------------- 1 | # PowerShell script to get all users in Microsoft 365 tenant using Microsoft Graph API 2 | # This script requires the Microsoft.Graph PowerShell module 3 | 4 | # Check if Microsoft.Graph module is installed and install if needed 5 | if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Users)) { 6 | Write-Host "Microsoft.Graph.Users module not found. Installing..." 7 | Install-Module Microsoft.Graph.Users -Scope CurrentUser -Force 8 | } 9 | 10 | # Import required modules 11 | Import-Module Microsoft.Graph.Users 12 | 13 | # Connect to Microsoft Graph 14 | Connect-MgGraph -Scopes "User.Read.All" -NoWelcome 15 | 16 | # Get all users from the tenant 17 | $users = Get-MgUser -All 18 | 19 | # Display user information 20 | $users | Select-Object DisplayName, UserPrincipalName, Id, JobTitle, Department | Format-Table -AutoSize 21 | 22 | # Export to CSV file (Optional) 23 | $csvPath = Join-Path -Path $PSScriptRoot -ChildPath "TenantUsers_$(Get-Date -Format 'yyyyMMdd').csv" 24 | $users | Select-Object DisplayName, UserPrincipalName, Id, Mail, JobTitle, Department, AccountEnabled | Export-Csv -Path $csvPath -NoTypeInformation 25 | 26 | Write-Host "Total users found: $($users.Count)" 27 | Write-Host "Users exported to: $csvPath" 28 | 29 | # Disconnect from Microsoft Graph 30 | Disconnect-MgGraph -------------------------------------------------------------------------------- /Microsoft 365/Power Apps/Expression to Build a Planner Task URL.txt: -------------------------------------------------------------------------------- 1 | concat('',triggerOutputs()?['body/title'],'') -------------------------------------------------------------------------------- /Microsoft 365/README.md: -------------------------------------------------------------------------------- 1 | # Microsoft 365 2 | 3 | Scripts for working with Microsoft 365 services. 4 | -------------------------------------------------------------------------------- /Microsoft 365/Set-EXOExternalEmailWarnings.ps1: -------------------------------------------------------------------------------- 1 | Install-Module ExchangeOnlineManagement 2 | Import-Module ExchangeOnlineManagement 3 | # Update the line below with the UPN of an Exchange Online administrator 4 | Connect-ExchangeOnline -UserPrincipalName 'EXOAdministrator@domain.com' 5 | 6 | # Enable a mailtip that is displayed when sending an email that includes external recipients: 7 | Set-OrganizationConfig -MailTipsExternalRecipientsTipsEnabled $true 8 | Get-OrganizationConfig | Format-List MailTipsExternalRecipientsTipsEnabled 9 | 10 | # Enable a mailtip in Outlook and Outlook mobile that tags mesages from external senders: 11 | Set-ExternalInOutlook -Enabled $true 12 | -------------------------------------------------------------------------------- /Microsoft 365/Setup-SensitivityLabels.ps1: -------------------------------------------------------------------------------- 1 | # https://learn.microsoft.com/en-us/entra/identity/users/groups-assign-sensitivity-labels?tabs=microsoft 2 | 3 | $grpUnifiedSetting = Get-MgBetaDirectorySetting | Where-Object { $_.Values.Name -eq 'EnableMIPLabels' } 4 | $grpUnifiedSetting.Values 5 | 6 | $params = @{ 7 | Values = @( 8 | @{ 9 | Name = 'EnableMIPLabels' 10 | Value = 'True' 11 | }, 12 | @{ 13 | Name = 'EnableGroupCreation' 14 | Value = 'False' 15 | } 16 | ) 17 | } 18 | 19 | Update-MgBetaDirectorySetting -DirectorySettingId $grpUnifiedSetting.Id -BodyParameter $params 20 | 21 | $Setting = Get-MgBetaDirectorySetting -DirectorySettingId $grpUnifiedSetting.Id 22 | $Setting.Values 23 | 24 | # https://learn.microsoft.com/en-us/purview/create-sensitivity-labels?tabs=classic-label-scheme#create-and-configure-sensitivity-labels 25 | 26 | Connect-IPPSSession -ShowBanner:$false -BypassMailboxAnchoring -UserPrincipalName $MyUPN 27 | Execute-AzureAdLabelSync 28 | 29 | 30 | 31 | # https://learn.microsoft.com/en-us/purview/sensitivity-labels-sharepoint-onedrive-files 32 | Import-Module -Name Microsoft.Online.SharePoint.PowerShell 33 | Set-SPOTenant -EnableAIPIntegration $true 34 | Set-SPOTenant -EnableSensitivityLabelforPDF $true 35 | -------------------------------------------------------------------------------- /Microsoft 365/Update-Office-Apps.ps1: -------------------------------------------------------------------------------- 1 | & "$env:ProgramFiles\Common Files\Microsoft Shared\ClickToRun\OfficeC2RClient.exe" /Update User -------------------------------------------------------------------------------- /Opsgenie/Get-OnCallReport.ps1: -------------------------------------------------------------------------------- 1 | # Authentication headers are required for every API call 2 | $key = "" 3 | $header = @{Authorization = "GenieKey $key" } 4 | 5 | # Get all schedules from OpsGenie and save the data from the API response 6 | $Schedules = (Invoke-RestMethod -Method GET -Headers $header -Uri "https://api.opsgenie.com/v2/schedules").data 7 | 8 | # Get the URIs for each schedule. Using an arraylist type is recommended if you know that you'll be adding 9 | # or removing objects from the array. Adding an item to an arraylist will output the index of the new item 10 | # by default, so using "$null =" prevents that output from appearing. 11 | $ScheduleUris = [System.Collections.ArrayList]@() 12 | foreach ($item in $Schedules) { 13 | $null = $ScheduleUris.Add("https://api.opsgenie.com/v2/schedules/$($item.id)") 14 | } # End of ScheduleUris arraylist 15 | 16 | # Create an arraylist that contains details of each team's on call schedule. 17 | $OnCalls = [System.Collections.ArrayList]@() 18 | foreach ($url in $ScheduleUris) { 19 | 20 | # Get all of the desired information by calling the relevant APIs (schedules, users, and contacts). 21 | # The user API can reference either the users' id value or their username value. 22 | $ScheduleData = (Invoke-RestMethod -Method GET -headers $header -uri "$url").data 23 | $OnCallData = (Invoke-RestMethod -Method GET -headers $header -uri "$url/on-calls").data 24 | $UserId = $OnCallData.onCallParticipants.id 25 | $UserData = (Invoke-RestMethod -Method GET -headers $header -uri "https://api.opsgenie.com/v2/users/$Userid").Data 26 | $ContactData = (Invoke-RestMethod -Method GET -headers $header -uri "https://api.opsgenie.com/v2/users/$Userid/contacts").Data 27 | 28 | # Create a temporary custom object that contains the collated details 29 | $OnCallDetails = [PSCUstomObject]@{ 30 | #TeamId = "" # Optionalo, but not needed 31 | #TeamName = "" # Optionalo, but not needed 32 | #ScheduleId = $ScheduleData.id # Optionalo, but not needed 33 | ScheduleName = $ScheduleData.name 34 | OnCallUser = $UserData.fullName 35 | OnCallEmail = ($ContactData.Where({$_.Method -eq "email"})).to 36 | OnCallPhone = ($ContactData.Where({$_.Method -eq "voice"})).to 37 | } # Finished populating temporary custom object 38 | 39 | # Add the object to the $OnCalls arraylist and clear the temporary object. Using $null to suppress host output. 40 | $null = $OnCalls.Add($OnCallDetails) 41 | $OnCallDetails = $null 42 | } # End foreach $ScheduleUris 43 | 44 | Return $OnCalls | Sort-Object -Property ScheduleName 45 | -------------------------------------------------------------------------------- /Profile and Prompt/Check for VSCode in Script or Profile.ps1: -------------------------------------------------------------------------------- 1 | if ($psEditor) { 2 | # In Visual Studio Code (PowerShell Extension). Do things specific to this environment: 3 | } 4 | -------------------------------------------------------------------------------- /Profile and Prompt/Comply/README.md: -------------------------------------------------------------------------------- 1 | # Comply Theme for Oh My Posh 2 | 3 | Comply is a simple Oh My Posh theme for users who only want essential information in their prompt. 4 | 5 | ![A preview of the Comply theme for Oh My Posh](comply.png) 6 | 7 | ## Features 8 | 9 | - A user glyph (👤) that changes color from cyan to orange when running as root/admin. 10 | 11 | - A segment that shows the execution time of the last command. It automatically formats the time as milliseconds, seconds, minutes, or hours (etc) when relevant. 12 | 13 | - The current path displayed after a folder glyph (📁). When in your home folder's path, the standard ~ character takes its place to reduce length and provide some privacy when sharing screenshots (eg: "C:\Users\JaneDoe\Downloads" becomes "~\Downloads"). 14 | 15 | - Git repository and branch information (when relevant). 16 | 17 | - The prompt character appears by itself on a new line, which makes the command history easier to read. 18 | - The prompt character changes from a white `>` to an orange `#` when running as admin/root, which should familiar to users who run `sudo` or `enable`. 19 | 20 | - A transient prompt: the full prompt is rendered only for the current command. All previously executed command lines have a simplified `>>` prompt in yellow to reduce clutter and improve readability. 21 | 22 | ## Screen Shots 23 | 24 | ### Comply theme running as a standard user 25 | 26 | ![Sample screen shot running as standard user.](comply-example-standard-user.png) 27 | 28 | ### Comply theme running as an elevated admin/root user 29 | 30 | ![Sample screen shot running as a privileged user.](comply-example-admin-user.png) 31 | 32 | ## Getting Started 33 | 34 | To use the Comply theme, download '[comply.omp.json](comply.omp.json)' and add it to your PowerShell profile. 35 | 36 | For more information, visit [ohmyposh.dev](https://ohmyposh.dev). 37 | -------------------------------------------------------------------------------- /Profile and Prompt/Comply/comply-example-admin-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamErde/PowerShell/81285216c3fa137d9fbdab30e4a4c101118856d9/Profile and Prompt/Comply/comply-example-admin-user.png -------------------------------------------------------------------------------- /Profile and Prompt/Comply/comply-example-standard-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamErde/PowerShell/81285216c3fa137d9fbdab30e4a4c101118856d9/Profile and Prompt/Comply/comply-example-standard-user.png -------------------------------------------------------------------------------- /Profile and Prompt/Comply/comply.omp.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json", 3 | "version": 3, 4 | "palettes": { 5 | "template": "{{ if .Root }}admin{{ else }}user{{ end }}", 6 | "list": { 7 | "admin": { 8 | "promptColor": "#ffffff", 9 | "userColor": "#ff6200", 10 | "executionTimeColor": "lightYellow" 11 | }, 12 | "user": { 13 | "promptColor": "#0CA0D8", 14 | "userColor": "#45F1C2", 15 | "executionTimeColor": "lightYellow" 16 | } 17 | } 18 | }, 19 | "blocks": [ 20 | { 21 | "type": "prompt", 22 | "alignment": "left", 23 | "segments": [ 24 | { 25 | "type": "session", 26 | "template": "\ueb99 ", 27 | "style": "plain", 28 | "foreground": "p:userColor" 29 | }, 30 | { 31 | "type": "executiontime", 32 | "template": "  {{ .FormattedMs }} ", 33 | "style": "plain", 34 | "foreground": "p:executionTimeColor", 35 | "properties": { 36 | "threshold": 500, 37 | "style": "round", 38 | "always_enabled": true 39 | } 40 | }, 41 | { 42 | "type": "path", 43 | "template": "\uf07b {{ .Path }} ", 44 | "style": "plain", 45 | "foreground": "#0CA0D8", 46 | "properties": { 47 | "cache_duration": "none", 48 | "folder_separator_icon": "/", 49 | "style": "full" 50 | } 51 | }, 52 | { 53 | "type": "git", 54 | "template": "{{ .UpstreamIcon }}{{ .HEAD }}{{ if gt .StashCount 0 }} \ueb4b {{ .StashCount }}{{ end }} ", 55 | "style": "plain", 56 | "powerline_symbol": "\ue0b0", 57 | "foreground": "#ffffff", 58 | "properties": { 59 | "cache_duration": "none", 60 | "fetch_stash_count": true, 61 | "fetch_upstream_icon": true 62 | } 63 | } 64 | ] 65 | }, 66 | { 67 | "type": "prompt", 68 | "alignment": "left", 69 | "newline": true, 70 | "segments": [ 71 | { 72 | "template": "{{if .Root}}# {{ else }}> {{ end }}", 73 | "type": "root", 74 | "style": "plain", 75 | "foreground": "p:promptColor" 76 | }, 77 | { 78 | "type": "text", 79 | "template": "{{if .Root}}{{ else }}> {{ end }}", 80 | "style": "plain", 81 | "foreground": "p:promptColor" 82 | } 83 | ] 84 | } 85 | ], 86 | "transient_prompt": { 87 | "template": ">> ", 88 | "foreground": "yellow" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Profile and Prompt/Comply/comply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamErde/PowerShell/81285216c3fa137d9fbdab30e4a4c101118856d9/Profile and Prompt/Comply/comply.png -------------------------------------------------------------------------------- /Profile and Prompt/Simple prompt with history.ps1: -------------------------------------------------------------------------------- 1 | function PromptPlain { 2 | Write-Host ('PS ' + $(Get-Date) + " [$((Get-History).Count)] " + "$pwd>") #-ForegroundColor White 3 | Write-Host '>' -NoNewline #-ForegroundColor White 4 | return ' ' 5 | } 6 | 7 | function Prompt { 8 | Write-Host "[$((Get-History).Count)] " -NoNewline -ForegroundColor Cyan 9 | Write-Host "$([math]::Ceiling((((Get-History)[-1]).EndExecutionTime - ((Get-History)[-1]).StartExecutionTime).TotalMilliseconds))ms " -NoNewline -ForegroundColor Yellow 10 | Write-Host "$($PWD.ToString().Replace($HOME,'~'))" -ForegroundColor Cyan 11 | Write-Host '>' -NoNewline -ForegroundColor White 12 | return ' ' 13 | } 14 | -------------------------------------------------------------------------------- /Profile and Prompt/task - Edit PowerShell Profiles.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Edit My Profiles", 3 | "type": "shell", 4 | "command": "code", 5 | "args": [ 6 | "$((Resolve-Path $PROFILE.CurrentUserAllHosts).Path)", 7 | "$(((Resolve-Path $PROFILE.CurrentUserAllHosts).Path).Replace('profile.ps1','Microsoft.VSCode_profile.ps1'))" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerShell 2 | 3 | 4 | [![GitHub stars](https://img.shields.io/github/stars/samerde/powershell?cacheSeconds=3600)](https://github.com/samerde/powershell/stargazers/) 5 | [![GitHub contributors](https://img.shields.io/github/contributors/samerde/powershell.svg)](https://github.com/samerde/powershell/graphs/contributors/) 6 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 7 | ![GitHub top language](https://img.shields.io/github/languages/top/SamErde/PowerShell) 8 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/ae92f0d929de494690e712b68fb3b52c)](https://app.codacy.com/gh/SamErde/PowerShell/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 9 | [![MegaLinter](https://github.com/samerde/powershell/workflows/MegaLinter/badge.svg?branch=main)](https://github.com/samerde/powershell/actions?query=workflow%3AMegaLinter+branch%3Amain) 10 | ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/SamErde/PowerShell/PSScriptAnalyzer.yml?label=PSScriptAnalyzer) 11 | 12 | 13 | Welcome to my collection of PowerShell scripts, functions, snippets, prompts, and more. I have been adding new things regularly and slowing going through old code to make sure it's decent enough to share! 😆 Most of this will be focused on Active Directory, Entra ID, Exchange Server, Microsoft 365, and general PowerShell tooling. You never know what other nuggets we'll find here, though. Enjoy, and PRs are welcomed! 14 | 15 | > [!IMPORTANT] 16 | > The code in this repository is shared as-is with no guarantee of any kind. Always be sure to review code and test before running in production. Change management is your friend! 17 | -------------------------------------------------------------------------------- /Script Logging/Transcripting.ps1: -------------------------------------------------------------------------------- 1 | # Be sure to set the $TranscriptDir 2 | 3 | $TranscriptLog = $env:computername + '_' + $env:username + '_' + (Get-Date -UFormat '%Y%m%d') 4 | Start-Transcript -LiteralPath "$TranscriptDir$TranscriptLog.log" -Append 5 | 6 | function Enable-PSTranscription { 7 | [CmdletBinding()] 8 | param( 9 | $OutputDirectory, [Switch] $IncludeInvocationHeader 10 | ) 11 | 12 | ## Ensure the base path exists 13 | $basePath = "HKLM:\Software\Policies\Microsoft\Windows\PowerShell\Transcription" 14 | if (-not (Test-Path $basePath)) { 15 | $null = New-Item $basePath -Force 16 | } 17 | 18 | ## Enable transcription 19 | Set-ItemProperty $basePath -Name EnableTranscripting -Value 1 20 | 21 | ## Set the output directory 22 | if ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey("OutputDirectory")) { 23 | Set-ItemProperty $basePath -Name OutputDirectory -Value $OutputDirectory 24 | } 25 | 26 | ## Set the invocation header 27 | if ($IncludeInvocationHeader) { 28 | Set-ItemProperty $basePath -Name IncludeInvocationHeader -Value 1 29 | } 30 | } 31 | 32 | function Disable-PSTranscription { 33 | Remove-Item HKLM:\Software\Policies\Microsoft\Windows\PowerShell\Transcription -Force -Recurse 34 | } 35 | -------------------------------------------------------------------------------- /Script Logging/Transcription.ps1: -------------------------------------------------------------------------------- 1 | # Get command history 2 | (Get-History).Where( {$_.StartExecutionTime -gt ((Get-Date).Date)} ).CommandLine 3 | 4 | 5 | # Start a transcript 6 | $TranscriptLog = $env:computername+"_"+$env:username+"_"+(Get-Date -UFormat "%Y%m%d") 7 | Start-Transcript -LiteralPath "$TranscriptDir$TranscriptLog.log" -Append 8 | 9 | 10 | # Enable PowerShell Transcription 11 | function Enable-PSTranscription { 12 | $basePath = "HKLM:\Software\Policies\Microsoft\Windows\PowerShell\Transcription" 13 | if (-not (Test-Path $basePath)) { $null = New-Item $basePath -Force } 14 | Set-ItemProperty $basePath -Name EnableTranscripting -Value 1 15 | Set-ItemProperty $basePath -Name OutputDirectory -Value "$env:USERPROFILE\OneDrive\PSTranscripts\$env:COMPUTERNAME\" 16 | Set-ItemProperty $basePath -Name EnableInvocationHeader -Value 1 17 | 18 | $basePath = "HKLM:\Software\Policies\Microsoft\PowerShellCore\Transcription" 19 | if(-not (Test-Path $basePath)) { $null = New-Item $basePath -Force } 20 | Set-ItemProperty $basePath -Name EnableTranscripting -Value 1 21 | Set-ItemProperty $basePath -Name OutputDirectory -Value "$env:USERPROFILE\OneDrive\PSTranscripts\$env:COMPUTERNAME\PSCore\" 22 | Set-ItemProperty $basePath -Name EnableInvocationHeader -Value 1 23 | } 24 | Enable-PSTranscription 25 | -------------------------------------------------------------------------------- /Script Logging/Write-This.ps1: -------------------------------------------------------------------------------- 1 | function Write-This { 2 | # Write a string of text to the host and a log file simultaneously. 3 | [CmdletBinding()] 4 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Support Using Write-Host')] 5 | [OutputType([string])] 6 | param ( 7 | # The message to display and write to a log 8 | [Parameter(Mandatory)] 9 | [string] 10 | $LogText, 11 | 12 | # Type of output to send 13 | [Parameter()] 14 | [ValidateSet('Both','HostOnly','LogOnly')] 15 | [string] 16 | $Output = 'Both' 17 | ) 18 | 19 | switch ($Output) { 20 | Both { 21 | Write-Host "$LogText" 22 | [void]$LogStringBuilder.AppendLine($LogText) 23 | } 24 | HostOnly { 25 | Write-Host "$LogText" 26 | } 27 | LogOnly { 28 | [void]$LogStringBuilder.AppendLine($LogText) 29 | } 30 | } 31 | } # end function Write-This 32 | -------------------------------------------------------------------------------- /Snippets/Average Time to Run a Command.ps1: -------------------------------------------------------------------------------- 1 | function Get-AverageExecutionTime { 2 | <# 3 | .SYNOPSIS 4 | Get the average execution time for running a script block multiple times. 5 | 6 | .DESCRIPTION 7 | Get the average time that it takes to execute a scriptblock. By default, the script block is run 50 times. 8 | 9 | .NOTES 10 | 11 | 12 | .EXAMPLE 13 | [scriptblock]$Scriptblock = { $TypeAccellerators = [System.Management.Automation.PSObject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::Get | Sort-Object } 14 | Get-AvgExecutionTime $Scriptblock 15 | 16 | #> 17 | [CmdletBinding()] 18 | param ( 19 | [Parameter( 20 | Mandatory = $true, 21 | HelpMessage = 'Specify a scriptblock to measure the execution time of.' 22 | )] 23 | [scriptblock]$Scriptblock, 24 | 25 | [Parameter( 26 | Mandatory = $false, 27 | HelpMessage = 'Specify the number of times to run the scriptblock. (Default 50)' 28 | )] 29 | [int]$Count = 50 30 | ) 31 | 32 | begin { 33 | [timespan]$TotalTime = New-TimeSpan 34 | } 35 | 36 | process { 37 | # Run the command $Count times 38 | for ($i = 0; $i -lt $Count; $i++) { 39 | # Get the current time before execution 40 | $StartTime = Get-Date 41 | 42 | # Execute the command 43 | $Scriptblock.Invoke() 44 | 45 | # Get the current time after execution 46 | $EndTime = Get-Date 47 | 48 | $RunTime = New-TimeSpan -Start $StartTime -End $EndTime 49 | Write-Verbose "The total runtime for run $($i + 1) was $($RunTime.TotalMilliseconds)." 50 | 51 | #Or.....just use this! 52 | # $RunTime = Measure-Command -Expression { $Scriptblock.Invoke() } 53 | 54 | # Calculate the execution time and add it to the total time 55 | $TotalTime = $TotalTime.Add($RunTime) 56 | } 57 | 58 | # Calculate the average execution time 59 | $AverageTime = $($TotalTime.Milliseconds) / $Count 60 | } 61 | 62 | end { 63 | Write-Verbose "The average execution time for your scriptblock was $($AverageTime.Milliseconds) ms." 64 | Return $AverageTime 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Snippets/Check if a key exists in a hash table.ps1: -------------------------------------------------------------------------------- 1 | # Check if a string exists as a key in a hash table 2 | $StringToCheck = 'SomeString' 3 | if ($null -eq $HashTable[$StringToCheck]) { 4 | Write-Output "The key [$StringToCheck] does not exist in the hash table" 5 | } else { 6 | Write-Output "The key [$StringToCheck] exists in the hash table" 7 | } 8 | -------------------------------------------------------------------------------- /Snippets/Compare Two Files with Code.ps1: -------------------------------------------------------------------------------- 1 | function Diffy { 2 | code --diff $args[0] $args[1] 3 | } 4 | -------------------------------------------------------------------------------- /Snippets/Error Information.ps1: -------------------------------------------------------------------------------- 1 | $Error[0].Exception.GetType().FullName 2 | -------------------------------------------------------------------------------- /Snippets/Examples of Using .NET Types.ps1: -------------------------------------------------------------------------------- 1 | # .NET Type Examples: 2 | [System.IO.Path]::GetTempPath() 3 | [System.Globalization.RegionInfo]::CurrentRegion 4 | [IO.File]::ReadAllText($FilePath) 5 | [System.IO.Path]::Combine($Path1, $Path2) 6 | 7 | # Using Is Null or Empty 8 | if ([string]::IsNullOrEmpty($customNames)) { 9 | # Do something 10 | } 11 | 12 | # Load the .NET assembly to make it available to the whole script or session 13 | Add-Type -AssemblyName 'System.DirectoryServices' 14 | Add-Type -AssemblyName 'System.Net.NetworkInformation' 15 | # Works when offline, returns the DNS domain name, not the NetBIOS domain name: 16 | ( [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties() ).DomainName 17 | # Requires domain connectivity, returns the DNS domain name, not the NetBIOS domain name: 18 | [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name 19 | # Find a global catalog server: 20 | [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Forest.FindGlobalCatalog().Name 21 | # Find a domain controller: 22 | [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().FindDomainController().Name 23 | -------------------------------------------------------------------------------- /Snippets/ForEach Differences.ps1: -------------------------------------------------------------------------------- 1 | # ForEach and ForEach-Object are not the same. 2 | # Also see example of looking for $_ in an array. 3 | # https://poshoholic.com/2007/08/31/essential-powershell-understanding-foreach-addendum/ 4 | 5 | foreach ($character in [char[]]'Poshoholic') { 6 | if (@('a', 'e', 'i', 'o', 'u') -contains $character ) { 7 | continue 8 | } 9 | $character 10 | } 11 | 12 | 13 | # Does not work the same as above example because the IF scriptblock 14 | # continues after the first vowel but a consonant is found first. 15 | [char[]]'Poshoholic' | ForEach-Object { 16 | if (@('a', 'e', 'i', 'o', 'u') -contains $_ ) { 17 | continue 18 | } 19 | $_ 20 | } 21 | 22 | # A "fix" for the above scriptblock 23 | [char[]]'Poshoholic' | ForEach-Object { 24 | if ((@('a', 'e', 'i', 'o', 'u') -contains $_) -eq $false ) { 25 | $_ 26 | } 27 | } 28 | 29 | <# Scripting Guy recommendation is that you: 30 | 31 | Only use the ForEach-Object cmdlet if you are concerned about saving memory as follows: 32 | 33 | While the loop is running (because only one of the evaluated objects is loaded into memory at one time). 34 | 35 | If you want to start seeing output from your loop faster (because the cmdlet starts the loop the second it has the first 36 | object in a collection versus waiting to gather them all like the ForEach construct). 37 | 38 | You should use the ForEach loop construct in the following situations: 39 | 40 | If you want the loop to finish executing faster (notice I said finish faster and not start showing results faster). 41 | 42 | You want to Break/Continue out of the loop (because you can't with the ForEach-Object cmdlet). This is especially true 43 | if you already have the group of objects collected into a variable, such as large collection of mailboxes. 44 | 45 | https://devblogs.microsoft.com/scripting/weekend-scripter-powershell-speed-improvement-techniques/ 46 | #> 47 | -------------------------------------------------------------------------------- /Snippets/Get Script Path, Name.ps1: -------------------------------------------------------------------------------- 1 | Function Get-ScriptPath { 2 | <# 3 | .SYNOPSIS 4 | Get-ScriptPath returns the path of the current script. 5 | .OUTPUTS 6 | System.String 7 | #> 8 | [CmdletBinding()] 9 | [OutputType([string])] 10 | Param() 11 | 12 | Begin { 13 | Remove-Variable ScriptPath 14 | } 15 | Process { 16 | If ($psEditor) { Split-Path -Path $psEditor.GetEditorContext().CurrentFile.Path } # Visual Studio Code 17 | ElseIf ($MyInvocation.MyCommand.CommandType -eq 'ExternalScript') { Split-Path -Path $My$MyInvocation.MyCommand.Source } # PS1 converted to EXE 18 | ElseIf ($null -ne $HostInvocation) { $HostInvocation.MyCommand.Path } # SAPIEN PowerShell Studio 19 | ElseIf ($psISE) { Split-Path -Path $psISE.CurrentFile.FullPath } # Windows PowerShell ISE 20 | ElseIf ($MyInvocation.PSScriptRoot) { $MyInvocation.PSScriptRoot } # Windows PowerShell 3.0+ 21 | ElseIf ($MyInvocation.MyCommand.Path) { Split-Path -Path $MyInvocation.MyCommand.Path -Parent } # Windows PowerShell 22 | Else { 23 | Write-Error "Unable to resolve script's file path." 24 | Exit 1 25 | } 26 | } 27 | } 28 | 29 | Function Get-ScriptName { 30 | <# 31 | .SYNOPSIS 32 | Get-ScriptName returns the name of the current script. 33 | .OUTPUTS 34 | System.String 35 | #> 36 | [CmdletBinding()] 37 | [OutputType([string])] 38 | Param() 39 | Begin { 40 | Remove-Variable ScriptName 41 | } 42 | Process { 43 | If ($psEditor) { Split-Path -Path $psEditor.GetEditorContext().CurrentFile.Path -Leaf } # Visual Studio Code Host 44 | ElseIf ($psEXE) { [System.Diagnotics.Process]::GetCurrentProcess.Name } # PS1 converted to EXE 45 | ElseIf ($null -ne $HostInvocation) { $HostInvocation.MyCommand.Name } # SAPIEN PowerShell Studio 46 | ElseIf ($psISE) { $psISE.CurrentFile.DisplayName.Trim('*') } # Windows PowerShell ISE 47 | ElseIf ($MyInvocation.MyCommand.Name) { $MyInvocation.MyCommand.Name } # Windows PowerShell 48 | Else { 49 | Write-Error "Uanble to resolve script's file name." 50 | Exit 1 51 | } 52 | } 53 | } 54 | 55 | [string]$ScriptPath = Get-ScriptPath # Get the current script path 56 | [string]$ScriptName = Get-ScriptName # Get the current script name 57 | 58 | $ScriptPath 59 | $ScriptName 60 | -------------------------------------------------------------------------------- /Snippets/Get-Command Examples.ps1: -------------------------------------------------------------------------------- 1 | Get-Command -Module Locksmith -Syntax 2 | Get-Command -Module Locksmith -FullyQualifiedModule 3 | Get-Command -Module Locksmith -ShowCommandInfo 4 | Get-Command -Module Locksmith -ArgumentList 5 | Get-Command -Module Locksmith -ListImported 6 | Get-Command -Module Locksmith -ParameterName Domain 7 | Get-Command -Module Locksmith -ParameterType int 8 | Get-Command -Module Locksmith -ParameterType string 9 | -------------------------------------------------------------------------------- /Snippets/Get-InputProperties.ps1: -------------------------------------------------------------------------------- 1 | function Get-InputProperties { 2 | <# 3 | .SYNOPSIS 4 | Check an input object to see if it contains specific properties. 5 | .DESCRIPTION 6 | This script checks an input object to see if it contains specific properties. 7 | This could be useful when validating inputs such as CSV columns or custom objects. 8 | 9 | THIS CONCEPT AND POTENTIAL USE CASE IS NOT COMPLETE 10 | #> 11 | [CmdletBinding()] 12 | [OutputType([String])] 13 | param ( 14 | $InputObjectObject 15 | ) 16 | 17 | begin { 18 | # Create a sample input object if none is provided 19 | if (-not $PSBoundParameters.Contains('Input') ) { 20 | $InputObject = [PSCustomObject]@{ 21 | GivenName = 'Sam' 22 | MiddleName = '' 23 | SurName = 'Erde' 24 | DisplayName = 'Sam Erde' 25 | Suffix = '' 26 | } 27 | } 28 | } 29 | 30 | process { 31 | 32 | if ([bool]$InputObject.PSObject.Properties['GivenName']) { 33 | $GivenName = $InputObject.GivenName 34 | } 35 | if ([bool]$InputObject.PSObject.Properties['SurName']) { 36 | $SurName = $InputObject.SurName 37 | } 38 | if ([bool]$InputObject.PSObject.Properties['Nickname']) { 39 | $Nickname = $InputObject.Nickname 40 | } else { 41 | "The property 'Nickname' was not present." 42 | } 43 | } 44 | 45 | end { 46 | "$GivenName $SurName ($Nickname)" | Out-Null 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Snippets/Get-PSUptime.ps1: -------------------------------------------------------------------------------- 1 | function Get-PSUptime { 2 | <# 3 | .SYNOPSIS 4 | Get the current system uptime. 5 | .DESCRIPTION 6 | Return the current system uptime in your desired unit of measurement. 7 | .PARAMETER Units 8 | Specify the units of time to return system uptime in. 9 | Accepted values: 'Days','Hours','Minutes','Seconds','Milliseconds','Microseconds','Nanoseconds' 10 | .EXAMPLE 11 | Get-Uptime -Units Seconds 12 | .Example 13 | Get-Uptime 14 | .NOTES 15 | Inspired by @md's blog at https://xkln.net/blog/getting-uptime-with-powershell---the-fast-way/ 16 | #> 17 | [CmdletBinding()] 18 | [Alias('Uptime')] 19 | param ( 20 | # The type of units to return 21 | [Parameter()] 22 | [ValidateSet('Days', 'Hours', 'Minutes', 'Seconds', 'Milliseconds', 'Microseconds', 'Nanoseconds')] 23 | [string] 24 | $Units 25 | ) 26 | if ( $Units ) { 27 | [TimeSpan]::FromTicks([System.Diagnostics.Stopwatch]::GetTimestamp()).$("Total$Units") 28 | } else { 29 | [TimeSpan]::FromTicks([System.Diagnostics.Stopwatch]::GetTimestamp()).ToString("d' Days 'h' Hours 'm' Minutes 's' Seconds'") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Snippets/How to Catch Multiple Exception Types.ps1: -------------------------------------------------------------------------------- 1 | # Path to a non-existent file 2 | $Path = 'C:\Temp\Non-Existent.csv' 3 | 4 | # Catch specific exception types and catch multiple types at once 5 | try { 6 | Import-Csv -Path $path -ErrorAction Stop 7 | } catch [System.IO.DirectoryNotFoundException], [System.IO.FileNotFoundException] { 8 | Write-Output "The path or file was not found: [$path]" 9 | } catch [System.IO.IOException] { 10 | Write-Output "IO error with the file: [$path]" 11 | } 12 | -------------------------------------------------------------------------------- /Snippets/How to Split String into Two Variables.ps1: -------------------------------------------------------------------------------- 1 | [string]$domain , [string]$user = "earth\guy" -split '\\' 2 | $domain ; $user 3 | 4 | 5 | '\r|\n|\r\n' 6 | (\r(?!\n)|\r?\n) -------------------------------------------------------------------------------- /Snippets/Install Multiple Modules at Once.ps1: -------------------------------------------------------------------------------- 1 | $PSRepository = 'PSGallery' 2 | 3 | $RequiredResource = @{ 4 | 5 | 'platyPS' = @{ 6 | Repository = $PSRepository 7 | } 8 | 'Pester' = @{ 9 | Repository = $PSRepository 10 | } 11 | 'Microsoft.Graph.Users' = @{ 12 | Repository = $PSRepository 13 | } 14 | 'Microsoft.Graph.Authentication' = @{ 15 | Repository = $PSRepository 16 | Version = '2.25.0' 17 | } 18 | 'Az.Accounts' = @{ 19 | Repository = 'MAR' 20 | } 21 | 22 | } 23 | 24 | Install-PSResource -RequiredResource $RequiredResource 25 | -------------------------------------------------------------------------------- /Snippets/Level-Up Notes/Strings - Split.ps1: -------------------------------------------------------------------------------- 1 | # This is an array 2 | $Food = @( 'Apples', 'Bananas', 'Cherries', 'Durian', 'Eggplant' ) 3 | 4 | $Food.Count 5 | 6 | # This is a string 7 | $Food = 'Apples,Bananas,Cherries,Durian,Eggplant' 8 | 9 | $Food 10 | $Food.Count 11 | 12 | # Let's slice this up 13 | $Food.Split(',') 14 | 15 | # The split method has 3 overloads: the separator, the number of items to return, and the options. 16 | # The default is to return all items, so we can just use the separator. 17 | $Food.Split(',', 1) 18 | 19 | $Food.Split(',', 2) 20 | 21 | $Food.Split(',', 3) 22 | 23 | # The split method returns an array of strings, and we can reference the individual elements using the index. 24 | ( $Food.Split(',') ).Count 25 | $Food.Split(',')[0] 26 | $Food.Split(',')[2] 27 | $Food.Split(',')[-1] # Get the last item. Negative index counts from the end of the array. 28 | $Food.Split(',')[-2] # Get the 2nd to last item. 29 | 30 | # Note that the separator is removed from the split strings: 31 | $Food.Split('p') 32 | # The split method is case-sensitive. 33 | $Food.Split('P') 34 | # We can use the ToLower() method to convert the string to lowercase before splitting OR we can use the -Split operator. 35 | 36 | # The -split operator, which is a case-insensitive version of the split method. 37 | $Food -split 'p' 38 | $Food -split 'P' 39 | -------------------------------------------------------------------------------- /Snippets/New Variable Notes.ps1: -------------------------------------------------------------------------------- 1 | 2 | # Use it to create a variable in a different scope. The -Scope parameter can be clearer than using $global:foo = 1 3 | New-Variable -Scope Global -Name VariableName 4 | 5 | # Use it to create a constant variable or a read-only variable 6 | New-Variable -Name pi -Value $([math]::Pi) -Option constant 7 | 8 | # Append to a very long command to output the result into an output variable, 9 | # rather than modify the beginning to add $variable = my command. 10 | # You can often also use the common parameter -OutVariable instead of this. 11 | Get-ChildItem | Set-Variable -Name VariableName 12 | 13 | # Use when you want to use a variable to dynamically set the name of a new variable. 14 | 15 | # Example 1: Create a variable for each item in an array. 16 | $List = @('01', '02', '03', '04', '05') 17 | foreach ($item in $List) { 18 | New-Variable -Name "Node$item" -Value "server$item" -Verbose 19 | } 20 | 21 | # Example 2: This example creates "hostEntry1_name" = "server01" (for 1 - 5). 22 | $CurrentResult = @('server01', 'server02', 'server03', 'server04', 'server05') 23 | $RecordHostCounter = 0 24 | foreach ($item in $CurrentResult) { 25 | $RecordHostCounter++ 26 | Set-Variable -Name ('hostEntry{0}_name' -f $RecordHostCounter) -Value $item -Verbose 27 | } 28 | 29 | # Use it to put the contents of the clipboard into a variable. 30 | Get-Clipboard | Set-Variable -Name ClipboardContents 31 | 32 | # Of course, you can also still use this, but will not get as much control over the variable that is created. 33 | Get-Clipboard -OutVariable ClipboardContents 34 | 35 | # Use to copy variable from one PowerShell instance to another via the clipboard. 36 | # Example: Check for admin permissions. If they have them, open an elevated prompt and 37 | # carry the contents of a variable into the elevated window via the clipboard. 38 | 39 | <# Other Possible Uses 40 | - Use it to more easily create a variable from a string in an array. 41 | 42 | - Use New-Variable when you want to put a space in a variable name--but that is a BAD idea! 43 | 44 | - Use it to create variables in loops. Example: Loop through a list of vCenter servers to connect to and create 45 | a different variable for each server. 46 | #> 47 | -------------------------------------------------------------------------------- /Snippets/Predictive PSReadLine.md: -------------------------------------------------------------------------------- 1 | # Get command autocomplete suggestions with PSReadLine 2 | 3 | First, force an upgrade of the module which is quasi-bound to our currently deployed build of Windows 10. FMI: [https://github.com/PowerShell/PowerShellGetv2/issues/644#issuecomment-592137971](https://github.com/PowerShell/PowerShellGetv2/issues/644#issuecomment-592137971) 4 | 5 | 1. Close ALL PowerShell instances. 6 | 7 | 2. Open an elevated command prompt. 8 | 9 | 3. Install the latest version of PSReadLine 10 | 11 | To update the Windows PowerShell 5.1 module, run: 12 | 13 | ```powershell 14 | powershell.exe -NoProfile -command "Install-Module PSReadLine -Force -SkipPublisherCheck -AllowPrerelease" 15 | ``` 16 | 17 | To update the PowerShell Core 7+ module, run: 18 | 19 | ```powershell 20 | pwsh.exe -NoProfile -command "Install-Module PSReadLine -Force -SkipPublisherCheck -AllowPrerelease" 21 | ``` 22 | 23 | 4. Add the following block to your PowerShell profile[s]. Note that Windows PowerShell 5.1 does not support the 'HistoryAndPlugin' predictive text source. 24 | 25 | ```powershell 26 | # Set PSReadLine Preferences 27 | Set-PSReadLineOption -PredictionViewStyle ListView 28 | if ($host.Version -like "5.*") { 29 | Set-PSReadLineOption -PredictionSource History 30 | } 31 | else { 32 | Set-PSReadLineOption -PredictionSource HistoryAndPlugin 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /Snippets/Relaunch Script as Admin.ps1: -------------------------------------------------------------------------------- 1 | $isAdmin = [System.Security.Principal.WindowsPrincipal]::new( 2 | [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators') 3 | 4 | if (-not $isAdmin) { 5 | $params = @{ 6 | FilePath = 'powershell' # or pwsh if Core 7 | Verb = 'RunAs' 8 | ArgumentList = @( 9 | '-NoExit' 10 | '-ExecutionPolicy ByPass' 11 | '-File "{0}"' -f $PSCommandPath 12 | ) 13 | } 14 | 15 | Start-Process @params 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /Snippets/RemoteServerSessionLoop2.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | 4 | .DESCRIPTION 5 | 6 | .NOTES 7 | 8 | #> 9 | 10 | #Update this list of servers or replace the statically generated array with a Get-ExchangeServer or Get-ADDomainController type of cmdlet. 11 | $servers = @('', '', '') 12 | $Creds = Get-Credential 13 | 14 | if ($session) { Remove-PSSession $session } 15 | 16 | #Loop through each server in the list, open a PowerShell remoting session, then show the name and status of the session. Skip (continue) to the next server if a connection fails. 17 | foreach ($server in $servers) { 18 | $session = New-PSSession -ComputerName $server -Name $server -Credential $Creds 19 | 20 | Try { 21 | Write-Information "Connecting to $server... " -InformationAction Continue 22 | Enter-PSSession $session 23 | } Catch { 24 | Write-Warning "Failed to enter the PSSession for $server. Skipping." -WarningAction Continue 25 | Continue 26 | } 27 | Write-Output $session.State 28 | 29 | <# 30 | Code to be run on each remote server go here. 31 | #> 32 | Write-Information 'Inner code.' -InformationAction Continue 33 | 34 | #Cleanup and then show the current PSSession state. 35 | Exit-PSSession 36 | Remove-PSSession $session 37 | Write-Information "$session.ComputerName $session.State `n`n" -InformationAction Continue 38 | } 39 | -------------------------------------------------------------------------------- /Snippets/Resolve SID in Event Logs.ps1: -------------------------------------------------------------------------------- 1 | # When you have a security principal 2 | ((Get-WinEvent -FilterHashtable @{LogName = 'System'; ID = 1501 } -MaxEvents 1).UserId).Translate([System.Security.Principal.NTAccount]).Value 3 | 4 | # When you have a SID as a string 5 | [System.Security.Principal.SecurityIdentifier]::new($sid).Translate([System.Security.Principal.NTAccount]).value 6 | -------------------------------------------------------------------------------- /Snippets/Restart a script as administrator.ps1: -------------------------------------------------------------------------------- 1 | if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( 2 | [Security.Principal.WindowsBuiltInRole] 'Administrator')) { 3 | Start-Process powershell.exe "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs -Wait 4 | } 5 | -------------------------------------------------------------------------------- /Snippets/Return Multiple Objects from Function.ps1: -------------------------------------------------------------------------------- 1 | function Get-Food { 2 | $TreeFruit = @('Apple', 'Orange', 'Peach') 3 | $Squash = @('Pumpkin', 'Acorn Squash', 'Winter Squash') 4 | $RootVegetable = @('Potato', 'Sweet Potato', 'Turnip', 'Radish') 5 | 6 | Return @{ 7 | TreeFruit = $TreeFruit 8 | Squash = $Squash 9 | RootVegetable = $RootVegetable 10 | } 11 | } 12 | $Results = Get-Food 13 | 14 | Write-Output @" 15 | Tree Fruit: 16 | $($Results['TreeFruit'] -join ', ') 17 | 18 | Squash: 19 | $($Results['Squash'] -join ', ') 20 | 21 | Root Vegetables: 22 | $($Results['RootVegetable'] -join ', ') 23 | "@ 24 | 25 | 26 | # Problematic solution that simply returns multiple objects 27 | function Get-Food { 28 | $TreeFruit = @('Apple', 'Orange', 'Peach') 29 | $Squash = @('Pumpkin', 'Acorn Squash', 'Winter Squash') 30 | $RootVegetable = @('Potato', 'Sweet Potato', 'Turnip', 'Radish') 31 | 32 | Return @($TreeFruit, $Squash, $RootVegetable) 33 | # Or just: $TreeFruit, $Squash, $RootVegetable 34 | } 35 | $Food = Get-Food 36 | 37 | # Recreate the arrays that might contain the food found with imatch: 38 | $TreeFruit = $Food -imatch 'Apple' 39 | $Squash = $Food -imatch 'Squash' 40 | $RootVegetable = $Food -imatch 'Turnip' 41 | 42 | # Recreate the arrays by index and hope you get the order right: 43 | $TreeFruit = $Food[0] 44 | $Squash = $Food[1] 45 | $RootVegetable = $Food[2] 46 | 47 | Write-Output @" 48 | Tree Fruit: 49 | $($TreeFruit.GetEnumerator()) 50 | 51 | Squash: 52 | $($Squash.GetEnumerator()) 53 | 54 | Root Vegetable: 55 | $($RootVegetable.GetEnumerator()) 56 | "@ 57 | -------------------------------------------------------------------------------- /Snippets/Run a Job in the Background and Stream Output to Host.ps1: -------------------------------------------------------------------------------- 1 | # Start a background job and stream its output to the host console 2 | $Job = Start-ThreadJob -ScriptBlock { 3 | Get-Process | 4 | ForEach-Object { Start-Sleep -ms 200; $_ } | 5 | oss | Write-Host 6 | } -StreamingHost $Host 7 | Get-Job $Job 8 | -------------------------------------------------------------------------------- /Snippets/Show-ExampleStatusWithStopwatch.ps1: -------------------------------------------------------------------------------- 1 | function Show-ExampleProgressUpdates { 2 | # When looping through a large number of objects, show periodic progress updates with a stopwatch 3 | begin { 4 | $StopWatchSegment = New-Object System.Diagnostics.Stopwatch 5 | $StopWatchSegment.Start() 6 | $StopWatchTotal = New-Object System.Diagnostics.Stopwatch 7 | $StopWatchTotal.Start() 8 | Write-Output 'Started processing!' 9 | } 10 | process { 11 | 12 | foreach ($object in 1..100) { 13 | $ProcessedCount++ 14 | if ($ProcessedCount % 10 -eq 0) { 15 | Write-Output "`t $ProcessedCount objects processed." 16 | Start-Sleep -Seconds 1 17 | } 18 | 19 | if ($StopWatchSegment.Elapsed.TotalSeconds -gt 5) { 20 | Write-Output "Processed $ProcessedCount objects in $($StopWatchSegment.Elapsed.TotalSeconds) seconds. (Total: $($StopWatchTotal.Elapsed.TotalSeconds) seconds)." 21 | $StopWatchSegment.Restart() 22 | } 23 | } 24 | 25 | } 26 | end { 27 | $StopWatchSegment.Stop() 28 | $StopWatchTotal.Stop() 29 | Remove-Variable StopWatchSegment, StopWatchTotal 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Snippets/Storing Functions in Hash Tables.ps1: -------------------------------------------------------------------------------- 1 | $items = @{ 2 | 'laptop' = { $args[0] * 600 } 3 | 'rasppi' = { param($x = 1) $x * 5 } 4 | 'arduino' = { param($x = 1) $x * 50 } 5 | } 6 | 7 | $quantity = 4 8 | $value = &$items['laptop'] $quantity 9 | 'The value of {0} laptops is ${1}' -f $quantity, $value 10 | 11 | $quantity = 2 12 | $value = &$items['rasppi'] $quantity 13 | 'The value of {0} pi is ${1}' -f $quantity, $value 14 | 15 | 16 | 17 | $Files = @{ 18 | OpenSSL = Join-Path -Path $Home -ChildPath "Documents\Tools\OpenSSL-Win64-3.0.0\bin\openssl.exe" 19 | Config = Join-Path -Path $WorkingDir -ChildPath "$ShortName.txt" 20 | Csr = Join-Path -Path $WorkingDir -ChildPath "$ShortName.csr" 21 | Key = Join-Path -Path $WorkingDir -ChildPath "$ShortName.key" 22 | KeyTmp = Join-Path -Path $WorkingDir -ChildPath "$ShortName.key.tmp" 23 | Cert = Join-Path -Path $WorkingDir -ChildPath "$ShortName.crt" 24 | PFX = Join-Path -Path $WorkingDir -ChildPath "$ShortName.pfx" 25 | } 26 | 27 | $Files.OpenSSL 28 | $Files.Config 29 | -------------------------------------------------------------------------------- /Snippets/StringBuilder Examples.ps1: -------------------------------------------------------------------------------- 1 | $sb = [System.Text.StringBuilder]::New() 2 | $sb.Append('a').AppendFormat('{0} {1}', 'c', 'd').AppendLine() 3 | 4 | $items = Get-ChildItem -Recurse | Where-Object { $_.Name -match '^*.ps1$' } 5 | 6 | foreach ($script in $items) { 7 | $null = $sb.Append((Get-Content -Path $script.FullName -Raw)) 8 | $null = $sb.AppendLine('') 9 | } 10 | 11 | $sb.ToString() | Out-File -FilePath $FilePath -Encoding utf8 -Force 12 | -------------------------------------------------------------------------------- /Snippets/Test-Administrator.ps1: -------------------------------------------------------------------------------- 1 | function Test-Administrator { 2 | # PowerShell 5.x only runs on Windows so use .NET types to determine isAdminProcess 3 | # Or if we are on v6 or higher, check the $IsWindows pre-defined variable. 4 | if (($PSVersionTable.PSVersion.Major -le 5) -or $IsWindows) { 5 | $currentUser = [Security.Principal.WindowsPrincipal]([Security.Principal.WindowsIdentity]::GetCurrent()) 6 | return $currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) 7 | } else { 8 | # Must be Linux or OSX, so use the id util. Root has userid of 0. 9 | return 0 -eq (id -u) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Snippets/Test-IsLocalAdmin.ps1: -------------------------------------------------------------------------------- 1 | (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) 2 | -------------------------------------------------------------------------------- /Snippets/Test-Obsolete.ps1: -------------------------------------------------------------------------------- 1 | function Test-Obsolete { 2 | <# 3 | .SYNOPSIS 4 | Test an obsolete parameter. 5 | 6 | .DESCRIPTION 7 | Test an obsolete parameter. 8 | 9 | .EXAMPLE 10 | Test-Obsolete -Mode 0 11 | 12 | This example shows how to use the obsolete parameter. The Mode parameter is deprecated and will be removed in a future version. 13 | 14 | .NOTES 15 | Author: Sam Erde 16 | Version: 0.0.1 17 | Modified: 2025-02-12 18 | #> 19 | [CmdletBinding()] 20 | param ( 21 | # Test mode parameter. 22 | [Parameter(Mandatory)] 23 | [Obsolete("'Mode' is being replaced by a more flexible set of parameters. It will be removed in a future release.`n`nPlease use 'Get-Help Test-Obsolete' or visit for more information.")] 24 | [ValidateNotNullOrEmpty()] 25 | [ValidateRange(0, 5)] 26 | [int16] 27 | $Mode 28 | ) 29 | 30 | Write-Output "You chose mode ${Mode}." 31 | 32 | } # end function Test-Obsolete 33 | -------------------------------------------------------------------------------- /Snippets/Test-RedText.ps1: -------------------------------------------------------------------------------- 1 | ${esc} = [char]0x1b 2 | "${esc}[91m" + 'This is red text.' + "${esc}[0m" 3 | -------------------------------------------------------------------------------- /Snippets/Test-Type.ps1: -------------------------------------------------------------------------------- 1 | function Test-Type { 2 | param( 3 | [Parameter(Mandatory)] 4 | $Object 5 | ) 6 | 7 | switch ( ($Object.GetType().Name) ) { 8 | String { 'String' } 9 | Int32 { 'Int32' } 10 | Int64 { 'Int64' } 11 | Double { 'Double' } 12 | Boolean { 'Bool' } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Snippets/Update PSReadLine.md: -------------------------------------------------------------------------------- 1 | # Update PSReadLine 2 | 3 | Close ALL instances of PowerShell, including VS Code, and then run this from an elevated command prompt: 4 | 5 | ``` shell 6 | pwsh.exe -NoProfile -command "Install-Module PSReadLine -Force -SkipPublisherCheck -AllowPrerelease" 7 | powershell.exe -NoProfile -command "Install-Module PSReadLine -Force -SkipPublisherCheck 8 | ``` 9 | 10 | Add this to your PowerShell profiles. Windows PowerShell does not support 'HistoryAndPlugin'. 11 | 12 | ## Set PSReadLine Preferences 13 | 14 | ```powershell 15 | Set-PSReadLineOption -PredictionViewStyle ListView 16 | if ($host.Version -like "5.*") { 17 | Set-PSReadLineOption -PredictionSource History 18 | } 19 | else { 20 | Set-PSReadLineOption -PredictionSource HistoryAndPlugin 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /Snippets/Use TLS in Scripts.ps1: -------------------------------------------------------------------------------- 1 | # Interesting notes: https://stackoverflow.com/questions/41618766/powershell-invoke-webrequest-fails-with-ssl-tls-secure-channel 2 | 3 | 4 | # Works with .NET Framework 4.5 or newer 5 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 6 | 7 | # Compatibility with systems that have anything older than .NET Frameworks 4.5 8 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 9 | 10 | 11 | 12 | # ========== Enable multiple versions of SSL/TLS for the session ========== # 13 | # SecurityProtocol is an Enum with the [Flags] attribute, so you can do this: 14 | [Net.ServicePointManager]::SecurityProtocol = 15 | [Net.SecurityProtocolType]::Tls13 -bor ` 16 | [Net.SecurityProtocolType]::Tls12 17 | 18 | # Also works: 19 | [Net.ServicePointManager]::SecurityProtocol = 'Tls13, TLS12, Tls11' 20 | 21 | # Also works: 22 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls13, [Net.SecurityProtocolType]::Tls12, [Net.SecurityProtocolType]::Tls11 23 | 24 | # Valid protocol type values, but should not longer be used: Tls11, Tls, Ssl3 25 | 26 | 27 | # ========== Disable Certificate Validation ========== # 28 | if (-not ([System.Management.Automation.PSTypeName]'ServerCertificateValidationCallback').Type) { 29 | $certCallback = @' 30 | using System; 31 | using System.Net; 32 | using System.Net.Security; 33 | using System.Security.Cryptography.X509Certificates; 34 | public class ServerCertificateValidationCallback 35 | { 36 | public static void Ignore() 37 | { 38 | if (ServicePointManager.ServerCertificateValidationCallback == null) 39 | { 40 | ServicePointManager.ServerCertificateValidationCallback += 41 | delegate 42 | ( 43 | Object obj, 44 | X509Certificate certificate, 45 | X509Chain chain, 46 | SslPolicyErrors errors 47 | ) 48 | { 49 | return true; 50 | }; 51 | } 52 | } 53 | } 54 | '@ 55 | Add-Type $certCallback 56 | } 57 | [ServerCertificateValidationCallback]::Ignore() 58 | # ========== Disable Certificate Validation ========== # 59 | -------------------------------------------------------------------------------- /Snippets/Using NET Types in PS.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Get Forest, Domain, and DNS information (etc) with .NET type accelerators:" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": { 14 | "vscode": { 15 | "languageId": "polyglot-notebook" 16 | } 17 | }, 18 | "outputs": [], 19 | "source": [ 20 | "$forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()\n", 21 | "$forest.Domains\n" 22 | ] 23 | } 24 | ], 25 | "metadata": { 26 | "language_info": { 27 | "name": "csharp" 28 | } 29 | }, 30 | "nbformat": 4, 31 | "nbformat_minor": 2 32 | } 33 | -------------------------------------------------------------------------------- /Snippets/Ways to Suppress Output.ps1: -------------------------------------------------------------------------------- 1 | $null = Invoke-Something 2 | [void](Invoke-Something) 3 | Invoke-Something > $null 4 | Invoke-Something | Out-Null -------------------------------------------------------------------------------- /Windows/Activate and Get License.ps1: -------------------------------------------------------------------------------- 1 | # Snippets to activate Windows and get the license key 2 | # Found from internet. Need to review and cleanup. 3 | 4 | # Activate Windows 5 | $ProductKey = (Get-CimInstance -ClassName SoftwareLicensingService).OA3xOriginalProductKey 6 | Invoke-Expression "cscript /b C:\Windows\System32\slmgr.vbs -ipk $ProductKey" 7 | Start-Sleep 5 8 | Invoke-Expression 'cscript /b C:\Windows\System32\slmgr.vbs -ato' 9 | 10 | 11 | 12 | # Define the registry key path and value 13 | $registryPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\MfaRequiredInClipRenew' 14 | $registryValueName = 'Verify Multi-factor Authentication in ClipRenew' 15 | $registryValueData = 0 # DWORD value of 0 16 | $sid = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-4') 17 | # SID for the Everyone group 18 | # or SID S-1-5-4 for the interactive group 19 | 20 | # Check if the registry key already exists 21 | if (-not (Test-Path -Path $registryPath)) { 22 | # If the key doesn't exist, create it and set the DWORD value 23 | New-Item -Path $registryPath -Force | Out-Null 24 | Set-ItemProperty -Path $registryPath -Name $registryValueName -Value $registryValueData -Type DWORD 25 | Write-Output 'Registry key created and DWORD value added.' 26 | } else { 27 | Write-Output 'Registry key already exists. No changes made.' 28 | } 29 | 30 | # Add read permissions for SID (S-1-1-0, Everyone) to the registry key with inheritance 31 | $acl = Get-Acl -Path $registryPath 32 | $ruleSID = New-Object System.Security.AccessControl.RegistryAccessRule($sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow') 33 | $acl.AddAccessRule($ruleSID) 34 | Set-Acl -Path $registryPath -AclObject $acl 35 | Write-Output "Added 'Interactive' group and SID ($sid) with read permissions (with inheritance) to the registry key." 36 | 37 | #Remove the # below to make sure it will kick off the scheduled task on already enrolled devices 38 | Start-Process "$env:SystemRoot\system32\ClipRenew.exe" 39 | 40 | $ProductKey = (Get-CimInstance -ClassName SoftwareLicensingService).OA3xOriginalProductKey 41 | -------------------------------------------------------------------------------- /Windows/Clear-QuickAccessList.ps1: -------------------------------------------------------------------------------- 1 | # Clear the Quick Access list in Windows Explorer 2 | $QuickAccessFilePath = "$env:APPDATA\Microsoft\Windows\Recent\AutomaticDestinations\f01b4d95cf55d32a.automaticDestinations-ms" 3 | if (Test-Path -Path $QuickAccessFilePath) { 4 | Remove-Item $QuickAccessFilePath -Confirm:$false -Force 5 | } 6 | -------------------------------------------------------------------------------- /Windows/ConvertTo-StringFromCertificate.ps1: -------------------------------------------------------------------------------- 1 | # Convert a B64 encoded certificate file to a string for use in scripts. 2 | function ConvertTo-StringFromCertificate { 3 | [CmdletBinding()] 4 | param ( 5 | [string]$FileName 6 | ) 7 | 8 | begin { 9 | if (!(Test-Path -Path $FileName)) { 10 | Write-Output 'The filename was not found.' 11 | Break 12 | } 13 | } 14 | 15 | process { 16 | $certFile = $FileName 17 | $cert = [IO.File]::ReadAllText($certFile) 18 | $cert = $cert.replace('-----BEGIN CERTIFICATE-----', '') 19 | $cert = $cert.replace('-----END CERTIFICATE-----', '') 20 | $cert = $cert.replace("`r", '') 21 | $cert = $cert.replace("`n", '') 22 | } 23 | end { 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Windows/Copy-ToPSSession.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Copy files to a server using PowerShell remoting sessions instead of SMB. 4 | 5 | .DESCRIPTION 6 | A method to copy files to a server using PowerShell remoting sessions instead of SMB, 7 | which should be off/blocked as a best practice in secure networks. 8 | 9 | .NOTES 10 | The parameters below could be setup to accept objects from the pipeline. 11 | #> 12 | 13 | function Copy-ItemToRemote { 14 | [CmdletBinding()] 15 | param ( 16 | [Parameter()] 17 | [string]$RemoteComputer, 18 | [Parameter()] 19 | [string]$SourcePath, 20 | [Parameter()] 21 | [string]$DestinationPath, 22 | [Parameter(Mandatory = $false)] 23 | $Session 24 | ) 25 | 26 | if (-not $Session) { 27 | $Session = New-PSSession -ComputerName $RemoteComputer #-Credential 28 | } 29 | try { 30 | Copy-Item -Path $SourcePath -Target $DestinationPath -ToSession $Session 31 | } catch { 32 | $Error 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Windows/Disable NetBIOS per NIC.ps1: -------------------------------------------------------------------------------- 1 | Set-ItemProperty HKLM:\SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces\tcpip* -Name NetbiosOptions -Value 2 2 | -------------------------------------------------------------------------------- /Windows/Disable-QuickAccessList.ps1: -------------------------------------------------------------------------------- 1 | # Disable the Quick Access list in Windows Explorer 2 | if (-not (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer' -Name 'HubMode')) { 3 | New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer' -Name 'HubMode' -PropertyType REG_DWORD -Value 1 4 | } 5 | -------------------------------------------------------------------------------- /Windows/Get-BITSDownload.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Quickly download a file using BITS. 4 | 5 | .DESCRIPTION 6 | BITS can be used to speed up transfers, used as an alternative to Invoke-WebRequest, 7 | or used to provide resiliency when a download is interrupted. 8 | #> 9 | 10 | function Get-BITSDownload { 11 | [CmdletBinding()] 12 | param ( 13 | 14 | ) 15 | 16 | $url = 'http://files.net/test/file1.test' 17 | $output = "$PSScriptRoot\file1.test" 18 | $start_time = Get-Date 19 | 20 | Import-Module BitsTransfer 21 | 22 | Start-BitsTransfer -Source $url -Destination $output 23 | # OR 24 | Start-BitsTransfer -Source $url -Destination $output -Asynchronous 25 | 26 | Write-Output "Time taken: $((Get-Date).Subtract($start_time).Seconds) second(s)" 27 | } 28 | -------------------------------------------------------------------------------- /Windows/Get-FileHashes.ps1: -------------------------------------------------------------------------------- 1 | $csv = 'H:\files.csv' 2 | $path = 'H:\Cleaned' 3 | $photos = Get-ChildItem -Path $path -Exclude *.db -Recurse -File 4 | foreach ($file in $photos) { 5 | $fInfoProperty = [ordered]@{ 6 | Name = $file.Name 7 | FullName = $file.FullName 8 | Length = $file.Length 9 | Hash = (Get-FileHash -Algorithm MD5 -Path $file.FullName).Hash 10 | } 11 | $fInfo = New-Object -TypeName psobject -Property $fInfoProperty 12 | $fInfo | Export-Csv -Path $csv -Append -NoTypeInformation 13 | } 14 | -------------------------------------------------------------------------------- /Windows/Get-NtpClientConfig-Alt (WIP).ps1: -------------------------------------------------------------------------------- 1 | # Work in Progress 2 | # See original at https://github.com/samerde/powershell/windows/Get-NtpClientConfig.ps1 3 | 4 | function Test-ThisApproach { 5 | $computerName = 'ABC-V-12345' 6 | 7 | $Hive = [Microsoft.Win32.RegistryHive]::LocalMachine 8 | $Path = 'SYSTEM\CurrentControlSet\Services\W32Time' 9 | 10 | try { 11 | $BaseKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($Hive, $ComputerName) 12 | $SubKey = $BaseKey.OpenSubKey($Path) 13 | if (!$SubKey) { 14 | Write-Warning "Registry key '$Path' does not exist." -WarningAction Stop 15 | } else { 16 | $Result = foreach ($ValueName in $SubKey.GetValueNames()) { 17 | [PsCustomObject]@{ 18 | 'ValueName' = if (!$ValueName -or $ValueName -eq '@') { '(Default)' } else { $ValueName } 19 | 'ValueData' = $SubKey.GetValue($ValueName) 20 | 'ValueKind' = $SubKey.GetValueKind($ValueName) 21 | } 22 | } 23 | } 24 | } catch { 25 | throw 26 | } finally { 27 | if ($SubKey) { $SubKey.Close() } 28 | if ($BaseKey) { $BaseKey.Close() } 29 | } 30 | 31 | # output on screen 32 | $Result | Sort-Object ValueName 33 | 34 | # or output to CSV file 35 | # $Result | Sort-Object ValueName | Export-Csv -Path 'X:\output.csv' -NoTypeInformation 36 | } 37 | -------------------------------------------------------------------------------- /Windows/Get-NtpClientConfig.ps1: -------------------------------------------------------------------------------- 1 | function Get-NtpClientConfig { 2 | <# 3 | .SYNOPSIS 4 | Get basic NTP client configuration details from Windows. 5 | 6 | .DESCRIPTION 7 | This function gets the NTP source server, source type, and last known good time for the NTP client without requiring local administrator privileges. Note: Performing a remote check does require administrative privileges on the remote computer. 8 | 9 | .EXAMPLE 10 | Get-NtpConfig 11 | 12 | Gets the NTP client configuration from the local host. 13 | 14 | .EXAMPLE 15 | Get-NtpClientConfig -ComputerName 'COMPUTER01' 16 | 17 | Gets the NTP client configuration from COMPUTER01. 18 | 19 | .NOTES 20 | Author: Sam Erde 21 | Version: 0.1.0 22 | Modified: 2024-12-04 23 | #> 24 | [CmdletBinding()] 25 | param ( 26 | # The computer to query. 27 | [Parameter(ValueFromPipeline, Position = 0)] 28 | [ValidateNotNullOrEmpty()] 29 | [string]$ComputerName = $env:COMPUTERNAME 30 | ) 31 | 32 | begin { 33 | $RegistryPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\W32Time' 34 | } # end begin 35 | 36 | process { 37 | 38 | if ($ComputerName -match $env:COMPUTERNAME -or $ComputerName -eq 'localhost') { 39 | [PSCustomObject]@{ 40 | Type = (Get-ItemProperty -Path "$RegistryPath\Parameters" -Name Type).Type 41 | Server = (Get-ItemProperty -Path "$RegistryPath\Parameters" -Name NtpServer).NtpServer 42 | LastKnownGoodTime = [datetime]::FromFileTime( (Get-ItemProperty -Path "$RegistryPath\Config" -Name LastKnownGoodTime).LastKnownGoodTime ) 43 | } 44 | } else { 45 | Invoke-Command -ComputerName $ComputerName -ScriptBlock { 46 | [PSCustomObject]@{ 47 | Type = (Get-ItemProperty -Path "$RegistryPath\Parameters" -Name Type).Type 48 | Server = (Get-ItemProperty -Path "$RegistryPath\Parameters" -Name NtpServer).NtpServer 49 | LastKnownGoodTime = [datetime]::FromFileTime( (Get-ItemProperty -Path "$RegistryPath\Config" -Name LastKnownGoodTime).LastKnownGoodTime ) 50 | } 51 | } 52 | } 53 | } # end process 54 | 55 | end { 56 | # 57 | } # end end 58 | 59 | } # end function Get-NtpConfig 60 | -------------------------------------------------------------------------------- /Windows/New-gMSA.ps1: -------------------------------------------------------------------------------- 1 | # Create group managed service accounts with this one easy step! Administrators will hate you! 2 | # Takes two command line parameters, gmsa and servers 3 | # gmsa should be the name of the MSA - "msa.example" 4 | # servers should be the list of servers that will have access to use the MSA, in a comma-seperated list - dinfserver01,dinfserver02,dinfserver03 5 | # 6 | 7 | Function New-gMSA { 8 | param ( 9 | [Parameter(Mandatory=$true)][string]$gMSA, 10 | [Parameter(Mandatory=$true)][array]$Servers, 11 | [Parameter(Mandatory=$true)][System.Management.Automation.PSCredential]$Credential 12 | ) 13 | If ($gMSA.Length -gt 15) { 14 | Write-Output "gMSA name too long. 15 character maximum." 15 | Exit 16 | } 17 | Import-Module ActiveDirectory 18 | $Group = "MSA " + $gMSA 19 | $DNS = $gMSA + ".DOMAIN.org" 20 | New-ADGroup -Name $Group -GroupScope Global -DisplayName $Group -Description "Permission group for $gMSA" -Path "OU=gMSA Password Retrieval Groups,OU=Security Groups,DC=DOMAINNAME,DC=org" -Credential $Credential 21 | Add-ADGroupMember -Identity $Group -Members ($Servers | ForEach-Object {Get-ADComputer $_}) -Credential $Credential 22 | New-ADServiceAccount -Name $gMSA -DNSHostName $DNS -PrincipalsAllowedToRetrieveManagedPassword $Group -Path "CN=Managed Service Accounts,DC=DOMAINNAME,DC=org" -Credential $Credential 23 | } 24 | -------------------------------------------------------------------------------- /Windows/Push-DNSClientServerAddresses.ps1: -------------------------------------------------------------------------------- 1 | Import-Module ActiveDirectory 2 | $servers = Get-ADComputer -SearchBase "" -Server "" -SearchScope Subtree -Filter * 3 | foreach ($server in $servers) 4 | { 5 | # Connect to the server. 6 | $serverName = $server.Name 7 | Write-Output "Connecting to $serverName" 8 | try { 9 | # Create and connect to the PSSession. 10 | $s = New-PSSession -ComputerName $serverName 11 | Enter-PSSession $s -ErrorAction SilentlyContinue 12 | } 13 | catch { 14 | # Log the failure and continue the for loop on the next item. 15 | Write-Output "Failed connection to $serverName" 16 | Continue 17 | } 18 | 19 | # Connected to session. Now updated the DNS client server address on any interfaces that currently use a domain controller IP. 20 | try { 21 | Get-NetIPInterface | Get-DnsClientServerAddress | Where-Object {$_.ServerAddresses -like '10.10.10.*'} | ` 22 | Set-DnsClientServerAddress -ServerAddresses ("","","") -Verbose 23 | } 24 | catch { 25 | Write-Output "Failed to change the DNS client server address on $servername" 26 | } 27 | Exit-PSSession 28 | } # End foreach server loop. 29 | -------------------------------------------------------------------------------- /Windows/Set-DefaultPrinter.ps1: -------------------------------------------------------------------------------- 1 | function Set-DefaultPrinter { 2 | <# 3 | .SYNOPSIS 4 | Set the user's default printer in Windows. 5 | .DESCRIPTION 6 | Set the user's default printer in Windows. Shows a list of installed printers if none is specified. 7 | .PARAMETER PrinterName 8 | The name of an installed printer to set as the default. 9 | Includes tab-autocomplete to select from the currently installed printers. 10 | If no printer name is specified, a list of installed printers will be shown without making a change. 11 | .NOTES 12 | Author: Sam Erde 13 | Date Modified: 2024-06-11 14 | Version: 0.1.1 15 | 16 | Dedicated to Matt Dillon, our Intune Engineer Extraordinaire! 17 | #> 18 | [CmdletBinding()] 19 | param ( 20 | # Name of the printer to set as your default. Provides tab-autocomplete with names of installed printers. 21 | # The way this handles printer names with spaces in them feels sloppy, but works. Got any better ideas for me? 22 | [Parameter()] 23 | [ArgumentCompleter( { 24 | param ( $CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters ) 25 | [array]$Script:PrinterNames = "'$((Get-Printer).Name -join "','")'" -split ',' 26 | $Script:PrinterNames 27 | } 28 | )] 29 | [ValidateScript({ 30 | if ("'" + $_ + "'" -in $Script:PrinterNames) { 31 | $true 32 | } else { 33 | throw "`n$_ is not a valid printer name. Please use one of the following: $($Script:PrinterNames -join ', ')" 34 | } 35 | })] 36 | [string] 37 | $PrinterName 38 | ) 39 | 40 | if (-not $PSBoundParameters.ContainsValue($PrinterName)) { 41 | Get-Printer 42 | Write-Host "`nNo printer name was specified, so here is a list of installed printers." -ForegroundColor Green -BackgroundColor Black 43 | return 44 | } 45 | 46 | $UnquotedPrinter = $PrinterName -replace "'", '' 47 | $Printer = Get-CimInstance -Class Win32_Printer -Filter "Name=`'$UnquotedPrinter`'" 48 | 49 | # SetDefaultPrinter if the specified printer name is found 50 | if ($Printer) { 51 | Invoke-CimMethod -InputObject $Printer -MethodName SetDefaultPrinter 52 | } else { 53 | Write-Host "No printer with the name '$UnquotedPrinter' was found." -ForegroundColor Yellow -BackgroundColor Black 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Windows/Set-DefaultPrinter.tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Set-DefaultPrinter' { 2 | 3 | BeforeAll { 4 | . $PSScriptRoot\Set-DefaultPrinter.ps1 5 | } 6 | 7 | Context 'When no printer name is provided' { 8 | 9 | It 'Should list installed printers' { 10 | 11 | Mock Get-Printer { 12 | return @( 13 | [PSCustomObject]@{ Name = 'Printer1' } 14 | [PSCustomObject]@{ Name = 'Printer2' } 15 | ) 16 | } 17 | 18 | Mock Write-Host {} 19 | 20 | Set-DefaultPrinter 21 | 22 | Assert-MockCalled Get-Printer 23 | Assert-MockCalled Write-Host 24 | } 25 | } 26 | 27 | Context 'When a valid printer name is provided' { 28 | 29 | It 'Should set the default printer' { 30 | 31 | Mock Get-Printer { 32 | return @( 33 | [PSCustomObject]@{ Name = 'Printer1' } 34 | [PSCustomObject]@{ Name = 'Printer2' } 35 | ) 36 | } 37 | 38 | Mock Get-CimInstance { 39 | $PrinterMock = [Microsoft.Management.Infrastructure.CimInstance]::new('Win32_Printer', 'root/cimv2') 40 | $PrinterName = [Microsoft.Management.Infrastructure.CimProperty]::Create('Name', 'Printer1', [cimtype]::String, 'Property, ReadOnly') 41 | $PrinterMock.CimInstanceProperties.Add($PrinterName) 42 | 43 | return $PrinterMock 44 | } 45 | 46 | Mock Invoke-CimMethod { 47 | return $null 48 | } 49 | 50 | Set-DefaultPrinter -PrinterName 'Printer1' 51 | 52 | Assert-MockCalled Get-CimInstance -Exactly -Times 1 -Scope It -ParameterFilter { $Filter -eq "Name='Printer1'" } 53 | 54 | Assert-MockCalled Invoke-CimMethod -Exactly -Times 1 -Scope It -ParameterFilter { $MethodName -eq 'SetDefaultPrinter' } 55 | } 56 | } 57 | 58 | Context 'When an invalid printer name is provided' { 59 | 60 | It 'Should throw an error' { 61 | 62 | Mock Get-Printer { 63 | return @( 64 | [PSCustomObject]@{ Name = 'Printer1' } 65 | [PSCustomObject]@{ Name = 'Printer2' } 66 | ) 67 | } 68 | 69 | Mock Get-CimInstance { 70 | return $null 71 | } 72 | 73 | { Set-DefaultPrinter -PrinterName 'InvalidPrinter' } | Should -Throw 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Windows/Test-WinREEnabled.ps1: -------------------------------------------------------------------------------- 1 | function Test-WinREEnabled { 2 | # This requires admin rights to enable WinRE with reagentc.exe. 3 | [CmdletBinding()] 4 | param() 5 | 6 | $ReAgentXmlPath = 'C:\Windows\System32\Recovery\ReAgent.xml' 7 | $WinReEnabled = $false 8 | 9 | if (Test-Path $reAgentXmlPath) { 10 | $ReAgentXml = [xml](Get-Content $reAgentXmlPath) 11 | $WinReBcdId = $ReAgentXml.WindowsRE.WinreBCD.id 12 | 13 | if ([string]::IsNullOrEmpty($winreBcdId)) { 14 | Write-Verbose "No `'WindowsRE.WinreBCD`' id was found in $reAgentXmlPath`'." 15 | $WinReEnabled = $false 16 | } elseif ($winreBcdId -eq '00000000-0000-0000-0000-000000000000') { 17 | Write-Verbose "The `'WindowsRE.WinreBCD`' id in $reAgentXmlPath`' contains all zeroes." 18 | $WinReEnabled = $false 19 | } else { 20 | Write-Verbose "The `'WindowsRE.WinreBCD`' id in $reAgentXmlPath`' is not empty and does not contain all zeroes." 21 | $WinReEnabled = $true 22 | } 23 | } 24 | 25 | if ($WinReEnabled -eq $false) { 26 | reagentc.exe /enable 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Windows/Update-SysInternals.ps1: -------------------------------------------------------------------------------- 1 | function Update-SysInternals { 2 | [CmdletBinding()] 3 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Colors are beautiful')] 4 | param ( 5 | [Parameter()] 6 | [string]$DestinationPath = $PSScriptRoot 7 | ) 8 | 9 | begin { 10 | Write-Host $PSScriptRoot 11 | [string]$Uri = 'https://download.sysinternals.com/files/SysinternalsSuite.zip' 12 | } 13 | 14 | process { 15 | Write-Host "Downloading SysInternals to $DestinationPath..." -BackgroundColor DarkCyan -ForegroundColor Green 16 | # Use BITS to download sysinternals to the specified path or the current folder 17 | Start-BitsTransfer -Source $Uri -Destination $DestinationPath -Description 'Downloading SysInternals from live.sysinternals.com' -DisplayName 'Update-SysInternals' -Verbose -Asynchronous 18 | Get-BitsTransfer 19 | # Set the registry value to automatically accept the SysInternals EULA 20 | if (!(Test-Path 'HKCU:\Software\SysInternals')) { 21 | New-Item -Path 'HKCU:\Software\SysInternals' -ItemType Directory -Force 22 | } 23 | Set-ItemProperty -Path HKCU:\Software\SysInternals -Name EulaAccepted -Value 1 -Type DWORD 24 | 25 | Get-BitsTransfer 26 | } 27 | 28 | end { 29 | Write-Host 'Have fun!' -BackgroundColor DarkCyan -ForegroundColor Green 30 | } 31 | } 32 | --------------------------------------------------------------------------------