{"id":81986,"date":"2017-02-03T00:01:11","date_gmt":"2017-02-03T08:01:11","guid":{"rendered":"https:\/\/blogs.technet.microsoft.com\/heyscriptingguy\/?p=81986"},"modified":"2019-02-18T09:10:12","modified_gmt":"2019-02-18T16:10:12","slug":"psscriptanalyzer-deep-dive-part-4-of-4","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/scripting\/psscriptanalyzer-deep-dive-part-4-of-4\/","title":{"rendered":"PSScriptAnalyzer deep dive \u2013 Part 4 of 4"},"content":{"rendered":"<p><strong>Summary<\/strong>: Thomas Rayner, Microsoft Cloud and Datacenter Management MVP, shows how to write a custom PSScriptAnalyzer rule.<\/p>\n<p>Hello! I\u2019m Thomas Rayner, a Cloud and Datacenter Management Microsoft MVP, filling in for The Scripting Guy this week. You can find me on Twitter (<a target=\"_blank\" href=\"http:\/\/twitter.com\/MrThomasRayner\">@MrThomasRayner<\/a>), or posting on my blog, <a target=\"_blank\" href=\"http:\/\/workingsysadmin.com\">workingsysadmin.com<\/a>. This week, I\u2019m presenting a four-part series about how to\u00a0use PSScriptAnalyzer.<\/p>\n<p><a target=\"_blank\" href=\"https:\/\/blogs.technet.microsoft.com\/heyscriptingguy\/2017\/01\/31\/psscriptanalyzer-deep-dive-part-1-of-4\/\">Part 1 \u2013 Getting started with PSScriptAnalyzer<\/a><\/p>\n<p><a target=\"_blank\" href=\"https:\/\/blogs.technet.microsoft.com\/heyscriptingguy\/2017\/02\/01\/psscriptanalyzer-deep-dive-part-2-of-4\/\">Part 2 \u2013 Suppressing, including, excluding rules<\/a><\/p>\n<p><a target=\"_blank\" href=\"https:\/\/blogs.technet.microsoft.com\/heyscriptingguy\/2017\/02\/02\/psscriptanalyzer-deep-dive-part-3-of-4\/\">Part 3 &#8211; Wrapping PSScriptAnalyzer with Pester to get formatted results<\/a><\/p>\n<p>Part 4 \u2013 Writing custom rules<\/p>\n<p>This is Part 4, so let\u2019s look at how to write your own custom PSScriptAnalyzer rules.<\/p>\n<p>At this time, PSScriptAnalyzer comes with a total of 45 rules that are based on community best practices. PowerShell team members at Microsoft and the community developed these rules. The built-in rules are a great baseline, and a good starting point that will quickly tell you if a script or module has any glaring flaws before you get too deep into it. That\u2019s great, but what if you or your team has some more stringent standards, or you want to borrow the PSSA engine to check scripts for some other reason? You\u2019ll need a custom rule.<\/p>\n<p>Before we go further, here\u2019s the script that I\u2019m going to be testing today, saved as MyScript.ps1. It\u2019s pretty useless because I\u2019m just trying to highlight some PSSA functionality.<\/p>\n<p style=\"padding-left: 60px\"><code>function Get-Something {<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>param (\n[string]$Words<\/code><\/p>\n<p style=\"padding-left: 60px\"><code>)<\/code><\/p>\n<p style=\"padding-left: 60px\"><code>Write-Host \"You said $Words\"<\/code><\/p>\n<p style=\"padding-left: 60px\"><code>}<\/code><\/p>\n<p style=\"padding-left: 60px\"><code>function Get-MYVar {<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>param (\n[string]$VariableName<\/code><\/p>\n<p style=\"padding-left: 60px\"><code>)<\/code><\/p>\n<p style=\"padding-left: 60px\"><code>$results = $null<\/code><\/p>\n<p style=\"padding-left: 60px\"><code>Get-Variable -Name $VariableName<\/code><\/p>\n<p style=\"padding-left: 60px\"><code>}<\/code><\/p>\n<p>Like with most of my other pieces of example code in this series, and especially if you\u2019ve been following the rest of this series, you should already see some things that are going to trigger some PSSA rule violations.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/1-HSG-020317.png\"><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/1-HSG-020317.png\" alt=\"Result of running MyScript.ps1 to trigger PSSA rule violations\" width=\"619\" height=\"115\" class=\"alignnone size-full wp-image-81995\" \/><\/a><\/p>\n<p>I\u2019m declaring a variable that I never actually use, and I\u2019m using <code>Write-Host<\/code>. Both actions are violations of standard PSSA rules.<\/p>\n<p>Maybe there are more issues with my script, though. Perhaps in my organization, it is against my style and standards guidelines to have a function that has adjacent capital letters. Instead of having <code>Get-MYVar<\/code>, I should have <code>Get-MyVar<\/code>. Plenty of people support this rule because it increases readability. Instead of something like <code>Get-AzureRMVM<\/code>, you can have <code>Get-AzureRmVm<\/code>, which is more readable.<\/p>\n<p>PSSA didn\u2019t tell me about my function whose name has adjacent capital letters, though. I know that I can use the regex pattern \u2018[A-Z]{2,}\u2019 to detect two capital letters in a row, but how do I write a PSSA rule?<\/p>\n<p>To write custom PSScriptAnalyzer rules, you\u2019ll need at least basic knowledge of the PowerShell abstract syntax tree. The PowerShell abstract syntax tree is a tree-style representation of the code that makes up whatever you\u2019ve written. Tools that are built into .NET and PowerShell parse files for examination by tools like, but not limited to, PSSA. Doing a deep dive on the abstract syntax tree could be its own five-part series. If that\u2019s something you\u2019d like to see, contact me by using my information at the beginning of this post. If the demand is there, I will put one together. For now, I\u2019m just going to recommend that you do a little independent learning if what you see in this post is too far over your head. The abstract syntax tree is a bit of an abstract concept (pun intended) to get into, but a few really good blog posts and info pages are out there already.<\/p>\n<p>So, let\u2019s get into it. PSSA custom rules are just PowerShell functions. I\u2019m going to make a new file named MyRule.psm1 and start to build my function. Note that the file needs to be a .psm1. Otherwise, it won\u2019t work properly.<\/p>\n<p style=\"padding-left: 60px\"><code>function Test-FunctionCasing {<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>[CmdletBinding()]\n[OutputType([PSCustomObject[]])]\nparam (<\/code><\/p>\n<p style=\"padding-left: 120px\"><code>[Parameter(Mandatory)]\n[ValidateNotNullOrEmpty()]\n[System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>)<\/code><\/p>\n<p style=\"padding-left: 60px\"><code>}<\/code><\/p>\n<p>My custom rule is going to be named <code>Test-FunctionCasing<\/code>. Custom PSSA rules output <code>PSCustomObject<\/code> objects and take some form of abstract syntax tree object as input. For this scenario, I want to use the <code>ScriptBlockAst<\/code> objects in my script because that\u2019s the part of the tree that will give me what I need to check function names.<\/p>\n<p style=\"padding-left: 30px\"><strong>Note<\/strong>: I\u2019ve started a module, called AstHelper, that\u2019s geared towards helping people discover and use the abstract syntax tree. It\u2019s available on the PowerShell Gallery (<a target=\"_blank\" href=\"https:\/\/www.powershellgallery.com\/packages\/AstHelper\/0.1\">Find-Module AstHelper<\/a>), and if you\u2019d like to contribute, the source is on GitHub (<a target=\"_blank\" href=\"https:\/\/github.com\/ThmsRynr\/AstHelper\">https:\/\/github.com\/ThmsRynr\/AstHelper<\/a>). At this time, it\u2019s very much in it\u2019s infancy, but I still use it to discover what types of abstract syntax tree objects are in PowerShell scripts and modules, and what kind of objects are in there that are of \u201cAST type ___\u201d. Jason Shirk also built a cool module for exploring <span>abstract syntax tree<\/span> (<a target=\"_blank\" href=\"https:\/\/github.com\/lzybkr\/ShowPSAst\">https:\/\/github.com\/lzybkr\/ShowPSAst<\/a>).<\/p>\n<p>Back to our custom rule. I\u2019m going to add a process block next.<\/p>\n<p style=\"padding-left: 60px\"><code>process {<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>try {<\/code><\/p>\n<p style=\"padding-left: 180px\"><code>$functions = $ScriptBlockAst.FindAll( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] -and<\/code><\/p>\n<p style=\"padding-left: 150px\"><code>$args[0].Name -cmatch '[A-Z]{2,}' }, $true )<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>}<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>catch {<\/code><\/p>\n<p style=\"padding-left: 120px\"><code>$PSCmdlet.ThrowTerminatingError( $_ )<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>}<\/code><\/p>\n<p style=\"padding-left: 60px\"><code>}<\/code><\/p>\n<p>Here, I\u2019m getting all the functions that have a name that matches my regex pattern of \u201ctwo adjacent capital letters\u201d. I\u2019ve wrapped this in a <code>try<\/code> \/ <code>catch<\/code>, just in case. The <code>.FindAll()<\/code> syntax is somewhat robust, and it can be daunting if you are not familiar with it. Microsoft has it well documented at <a target=\"_blank\" href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/jj136376(v=vs.85).aspx\">Ast.FindAll Method (Func&lt;Ast,\u2002Boolean&gt;,\u2002Boolean)<\/a>.<\/p>\n<p>Now, I just need a <code>foreach<\/code> loop to go through all the functions that matched the pattern and report them to PSSA.<\/p>\n<p style=\"padding-left: 60px\"><code>foreach ( $function in $functions ) {<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>[PSCustomObject]@{<\/code><\/p>\n<p style=\"padding-left: 120px\"><code>Message\u00a0 = \"Avoid function names with adjacent caps in their name\"\nExtent\u00a0\u00a0 = $function.Extent\nRuleName = $PSCmdlet.MyInvocation.InvocationName\nSeverity = \"Warning\"\n}<\/code><\/p>\n<p style=\"padding-left: 60px\"><code>}<\/code><\/p>\n<p>All I need to do is specify a message, an extent (built in to the result stored in <code>$function<\/code>), the rule name that it violated (which is the name of the PowerShell function I\u2019m building), and severity.<\/p>\n<p>My entire, assembled rule looks like this.<\/p>\n<p style=\"padding-left: 60px\"><code>function Test-FunctionCasing {<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>[CmdletBinding()]\n[OutputType([PSCustomObject[]])]\nparam (<\/code><\/p>\n<p style=\"padding-left: 120px\"><code>[Parameter(Mandatory)]\n[ValidateNotNullOrEmpty()]\n[System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>)<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>process {<\/code><\/p>\n<p style=\"padding-left: 150px\"><code>try {<\/code><\/p>\n<p style=\"padding-left: 180px\"><code>$functions = $ScriptBlockAst.FindAll( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] -and<\/code><\/p>\n<p style=\"padding-left: 180px\"><code>$args[0].Name -cmatch '[A-Z]{2,}' }, $true )\nforeach ( $function in $functions ) {\n[PSCustomObject]@{\nMessage\u00a0 = \"Avoid function names with adjacent caps in their name\"\nExtent\u00a0\u00a0 = $function.Extent\nRuleName = $PSCmdlet.MyInvocation.InvocationName\nSeverity = \"Warning\"\n}<\/code><\/p>\n<p style=\"padding-left: 150px\"><code>}<\/code><\/p>\n<p style=\"padding-left: 120px\"><code>}<\/code><\/p>\n<p style=\"padding-left: 120px\"><code>catch {<\/code><\/p>\n<p style=\"padding-left: 150px\"><code>$PSCmdlet.ThrowTerminatingError( $_ )<\/code><\/p>\n<p style=\"padding-left: 120px\"><code>}<\/code><\/p>\n<p style=\"padding-left: 90px\"><code>}<\/code><\/p>\n<p style=\"padding-left: 60px\"><code>}<\/code><\/p>\n<p>Now, I save MyRule.psm1, and I can include it when I run <code>Invoke-ScriptAnalyzer<\/code>.<\/p>\n<p style=\"padding-left: 60px\"><code>Invoke-ScriptAnalyzer -Path .\\MyScript.ps1 -CustomRulePath .\\MyRule.psm1<\/code><\/p>\n<p>And I get back a violation that looks just like you\u2019d think it should.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/2-HSG-020317.png\"><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/2-HSG-020317.png\" alt=\"Example of a violation\" width=\"590\" height=\"41\" class=\"alignnone size-full wp-image-81996\" \/><\/a><\/p>\n<p>But wait. Doesn\u2019t MyScript.ps1 violate some of the standard rules too? Where are those violations?<\/p>\n<p>Well, if we want to include the standard rules when we\u2019re using custom rules, we just need to add one parameter to our <code>Invoke-ScriptAnalyzer<\/code> command.<\/p>\n<p style=\"padding-left: 60px\"><code>Invoke-ScriptAnalyzer -Path .\\MyScript.ps1 -CustomRulePath .\\MyRule.psm1 -IncludeDefaultRules<\/code><\/p>\n<p>That looks better!<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/3-HSG-020317.png\"><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/3-HSG-020317.png\" alt=\"3-hsg-020317\" width=\"620\" height=\"115\" class=\"alignnone size-full wp-image-82006\" \/><\/a><\/p>\n<p>That\u2019s it! This concludes my four-part deep dive on PSScriptAnalyzer. Hopefully, you\u2019ve learned something about PSSA and have seen that even though this was a deep dive, the rabbit hole goes much deeper.<\/p>\n<p>Happy scripting!<\/p>\n<p>Thomas! That was a great set, and now you\u2019ve got my brain churning! I\u2019ll be sure to have my PC wrapped this weekend playing with all these cool new ideas! Thanks!<\/p>\n<p>I invite you to follow the Scripting Guys on <a target=\"_blank\" href=\"http:\/\/bit.ly\/scriptingguystwitter\">Twitter<\/a> and <a target=\"_blank\" href=\"http:\/\/bit.ly\/scriptingguysfacebook\">Facebook<\/a>. If you have any questions, send email to them at <a target=\"_blank\" href=\"mailto:scripter@microsoft.com\">scripter@microsoft.com<\/a>, or post your questions on the <a target=\"_blank\" href=\"http:\/\/bit.ly\/scriptingforum\">Official Scripting Guys Forum<\/a>. See you tomorrow.<\/p>\n<p>Until then, always remember that with Great PowerShell comes Great Responsibility.<\/p>\n<p><strong>Sean Kearney\n<\/strong>Honorary Scripting Guy\nCloud and Datacenter Management MVP<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Summary: Thomas Rayner, Microsoft Cloud and Datacenter Management MVP, shows how to write a custom PSScriptAnalyzer rule. Hello! I\u2019m Thomas Rayner, a Cloud and Datacenter Management Microsoft MVP, filling in for The Scripting Guy this week. You can find me on Twitter (@MrThomasRayner), or posting on my blog, workingsysadmin.com. This week, I\u2019m presenting a four-part [&hellip;]<\/p>\n","protected":false},"author":596,"featured_media":87096,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[568,685,641],"tags":[56,652,45],"class_list":["post-81986","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-hey-scripting-guy","category-scripting-techniques","category-windows-powershell","tag-guest-blogger","tag-thomas-rayner","tag-windows-powershell"],"acf":[],"blog_post_summary":"<p>Summary: Thomas Rayner, Microsoft Cloud and Datacenter Management MVP, shows how to write a custom PSScriptAnalyzer rule. Hello! I\u2019m Thomas Rayner, a Cloud and Datacenter Management Microsoft MVP, filling in for The Scripting Guy this week. You can find me on Twitter (@MrThomasRayner), or posting on my blog, workingsysadmin.com. This week, I\u2019m presenting a four-part [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/81986","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/users\/596"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/comments?post=81986"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/81986\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/media\/87096"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/media?parent=81986"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/categories?post=81986"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/tags?post=81986"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}