diff --git a/Engine/Commands/GetScriptAnalyzerRuleCommand.cs b/Engine/Commands/GetScriptAnalyzerRuleCommand.cs index a914e1f59..f9c1934bc 100644 --- a/Engine/Commands/GetScriptAnalyzerRuleCommand.cs +++ b/Engine/Commands/GetScriptAnalyzerRuleCommand.cs @@ -33,12 +33,25 @@ public class GetScriptAnalyzerRuleCommand : PSCmdlet, IOutputWriter [Parameter(Mandatory = false)] [ValidateNotNullOrEmpty] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] - public string[] CustomizedRulePath + [Alias("CustomizedRulePath")] + public string CustomRulePath { - get { return customizedRulePath; } - set { customizedRulePath = value; } + get { return customRulePath; } + set { customRulePath = value; } } - private string[] customizedRulePath; + private string customRulePath; + + /// + /// RecurseCustomRulePath: Find rules within subfolders under the path + /// + [Parameter(Mandatory = false)] + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + public SwitchParameter RecurseCustomRulePath + { + get { return recurseCustomRulePath; } + set { recurseCustomRulePath = value; } + } + private bool recurseCustomRulePath; /// /// Name: The name of a specific rule to list. @@ -76,7 +89,9 @@ public string[] Severity /// protected override void BeginProcessing() { - ScriptAnalyzer.Instance.Initialize(this, customizedRulePath); + string[] rulePaths = Helper.ProcessCustomRulePaths(customRulePath, + this.SessionState, recurseCustomRulePath); + ScriptAnalyzer.Instance.Initialize(this, rulePaths); } /// diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index ccf5d5992..afbfc46b7 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -73,12 +73,25 @@ public string ScriptDefinition [Parameter(Mandatory = false)] [ValidateNotNull] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] - public string[] CustomizedRulePath + [Alias("CustomizedRulePath")] + public string CustomRulePath { - get { return customizedRulePath; } - set { customizedRulePath = value; } + get { return customRulePath; } + set { customRulePath = value; } } - private string[] customizedRulePath; + private string customRulePath; + + /// + /// RecurseCustomRulePath: Find rules within subfolders under the path + /// + [Parameter(Mandatory = false)] + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + public SwitchParameter RecurseCustomRulePath + { + get { return recurseCustomRulePath; } + set { recurseCustomRulePath = value; } + } + private bool recurseCustomRulePath; /// /// ExcludeRule: Array of names of rules to be disabled. @@ -164,9 +177,12 @@ public string Profile /// protected override void BeginProcessing() { + string[] rulePaths = Helper.ProcessCustomRulePaths(customRulePath, + this.SessionState, recurseCustomRulePath); + ScriptAnalyzer.Instance.Initialize( this, - customizedRulePath, + rulePaths, this.includeRule, this.excludeRule, this.severity, diff --git a/Engine/Helper.cs b/Engine/Helper.cs index bc2fef74c..ef10cefc5 100644 --- a/Engine/Helper.cs +++ b/Engine/Helper.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Management.Automation; @@ -983,9 +984,45 @@ public Tuple, List> SuppressRule(string return result; } + public static string[] ProcessCustomRulePaths(string rulePath, SessionState sessionState, bool recurse = false) + { + //if directory is given, list all the psd1 files + List outPaths = new List(); + if (rulePath == null) + { + return null; + } + try + { + Collection pathInfo = sessionState.Path.GetResolvedPSPathFromPSPath(rulePath); + foreach (PathInfo pinfo in pathInfo) + { + string path = pinfo.Path; + if (Directory.Exists(path)) + { + path = path.TrimEnd('\\'); + if (recurse) + { + outPaths.AddRange(Directory.GetDirectories(pinfo.Path, "*", SearchOption.AllDirectories)); + } + } + outPaths.Add(path); + } + return outPaths.ToArray(); + } + catch (Exception ex) + { + // need to do this as the path validation takes place later in the hierarchy. + outPaths.Add(rulePath); + return outPaths.ToArray(); + } + } + + #endregion } + internal class TupleComparer : IComparer> { public int Compare(Tuple t1, Tuple t2) diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index 054db6758..b385fe4ae 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -108,7 +108,7 @@ internal void Initialize( { throw new ArgumentNullException("cmdlet"); } - + this.Initialize( cmdlet, cmdlet.SessionState.Path, @@ -188,7 +188,7 @@ private void Initialize( if (!String.IsNullOrWhiteSpace(profile)) { try - { + { profile = path.GetResolvedPSPathFromPSPath(profile).First().Path; } catch @@ -784,7 +784,7 @@ public Dictionary> CheckRuleExtension(string[] path, PathIn // We have to identify the childPath is really a directory or just a module name. // You can also consider following two commands. // Get-ScriptAnalyzerRule -RuleExtension "ContosoAnalyzerRules" - // Get-ScriptAnalyzerRule -RuleExtension "%USERPROFILE%\WindowsPowerShell\Modules\ContosoAnalyzerRules" + // Get-ScriptAnalyzerRule -RuleExtension "%USERPROFILE%\WindowsPowerShell\Modules\ContosoAnalyzerRules" if (Path.GetDirectoryName(childPath) == string.Empty) { resolvedPath = childPath; @@ -797,14 +797,14 @@ public Dictionary> CheckRuleExtension(string[] path, PathIn using (System.Management.Automation.PowerShell posh = System.Management.Automation.PowerShell.Create()) - { + { posh.AddCommand("Get-Module").AddParameter("Name", resolvedPath).AddParameter("ListAvailable"); PSModuleInfo moduleInfo = posh.Invoke().First(); // Adds original path, otherwise path.Except(validModPaths) will fail. // It's possible that user can provide something like this: // "..\..\..\ScriptAnalyzer.UnitTest\modules\CommunityAnalyzerRules\CommunityAnalyzerRules.psd1" - if (moduleInfo.ExportedFunctions.Count > 0) validModPaths.Add(childPath); + if (moduleInfo.ExportedFunctions.Count > 0) validModPaths.Add(resolvedPath); } } catch diff --git a/Tests/Engine/CustomizedRule.tests.ps1 b/Tests/Engine/CustomizedRule.tests.ps1 index cf87ed818..2ef337c88 100644 --- a/Tests/Engine/CustomizedRule.tests.ps1 +++ b/Tests/Engine/CustomizedRule.tests.ps1 @@ -51,18 +51,83 @@ Describe "Test importing correct customized rules" { } Context "Test Get-ScriptAnalyzer with customized rules" { - It "will show the customized rule" { + It "will show the custom rule" { $customizedRulePath = Get-ScriptAnalyzerRule -CustomizedRulePath $directory\samplerule\samplerule.psm1 | Where-Object {$_.RuleName -eq $measure} $customizedRulePath.Count | Should Be 1 } - + + It "will show the custom rule when given a rule folder path" { + $customizedRulePath = Get-ScriptAnalyzerRule -CustomizedRulePath $directory\samplerule | Where-Object {$_.RuleName -eq $measure} + $customizedRulePath.Count | Should Be 1 + } + + if (!$testingLibraryUsage) + { + It "will show the custom rule when given a rule folder path with trailing backslash" { + $customizedRulePath = Get-ScriptAnalyzerRule -CustomizedRulePath $directory\samplerule\ | Where-Object {$_.RuleName -eq $measure} + $customizedRulePath.Count | Should Be 1 + } + + It "will show the custom rules when given a glob" { + $customizedRulePath = Get-ScriptAnalyzerRule -CustomizedRulePath $directory\samplerule\samplerule* | Where-Object {$_.RuleName -match $measure} + $customizedRulePath.Count | Should be 4 + } + + It "will show the custom rules when given recurse switch" { + $customizedRulePath = Get-ScriptAnalyzerRule -RecurseCustomRulePath -CustomizedRulePath $directory\samplerule | Where-Object {$_.RuleName -eq $measure} + $customizedRulePath.Count | Should be 3 + } + + it "will show the custom rules when given glob with recurse switch" { + $customizedRulePath = Get-ScriptAnalyzerRule -RecurseCustomRulePath -CustomizedRulePath $directory\samplerule\samplerule* | Where-Object {$_.RuleName -eq $measure} + $customizedRulePath.Count | Should be 5 + } + + it "will show the custom rules when given glob with recurse switch" { + $customizedRulePath = Get-ScriptAnalyzerRule -RecurseCustomRulePath -CustomizedRulePath $directory\samplerule* | Where-Object {$_.RuleName -eq $measure} + $customizedRulePath.Count | Should be 3 + } + } } Context "Test Invoke-ScriptAnalyzer with customized rules" { - It "will show the customized rule in the results" { + It "will show the custom rule in the results" { $customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -CustomizedRulePath $directory\samplerule\samplerule.psm1 | Where-Object {$_.Message -eq $message} $customizedRulePath.Count | Should Be 1 } + + It "will show the custom rule in the results when given a rule folder path" { + $customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -CustomizedRulePath $directory\samplerule | Where-Object {$_.Message -eq $message} + $customizedRulePath.Count | Should Be 1 + } + + if (!$testingLibraryUsage) + { + It "will show the custom rule in the results when given a rule folder path with trailing backslash" { + $customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -CustomizedRulePath $directory\samplerule\ | Where-Object {$_.Message -eq $message} + $customizedRulePath.Count | Should Be 1 + } + + It "will show the custom rules when given a glob" { + $customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -CustomizedRulePath $directory\samplerule\samplerule* | Where-Object {$_.Message -eq $message} + $customizedRulePath.Count | Should be 3 + } + + It "will show the custom rules when given recurse switch" { + $customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -RecurseCustomRulePath -CustomizedRulePath $directory\samplerule | Where-Object {$_.Message -eq $message} + $customizedRulePath.Count | Should be 3 + } + + it "will show the custom rules when given glob with recurse switch" { + $customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -RecurseCustomRulePath -CustomizedRulePath $directory\samplerule\samplerule* | Where-Object {$_.Message -eq $message} + $customizedRulePath.Count | Should be 4 + } + + it "will show the custom rules when given glob with recurse switch" { + $customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -RecurseCustomRulePath -CustomizedRulePath $directory\samplerule* | Where-Object {$_.Message -eq $message} + $customizedRulePath.Count | Should be 3 + } + } } +} -} \ No newline at end of file diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index 63895955c..32624e216 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -19,12 +19,16 @@ Describe "Test available parameters" { Context "RuleExtension parameters" { It "has a RuleExtension parameter" { - $params.ContainsKey("CustomizedRulePath") | Should Be $true + $params.ContainsKey("CustomRulePath") | Should Be $true } It "accepts string array" { - $params["CustomizedRulePath"].ParameterType.FullName | Should Be "System.String[]" + $params["CustomRulePath"].ParameterType.FullName | Should Be "System.String" } + + It "takes CustomizedRulePath parameter as an alias of CustomRulePath paramter" { + $params.CustomRulePath.Aliases.Contains("CustomizedRulePath") | Should be $true + } } } diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 index c85152b70..ab1d5dc7b 100644 --- a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 +++ b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 @@ -30,19 +30,30 @@ Describe "Test available parameters" { $params.ContainsKey("ScriptDefinition") | Should Be $true } - It "accepts string" { + It "accepts string" { $params["ScriptDefinition"].ParameterType.FullName | Should Be "System.String" } } - Context "CustomizedRulePath parameters" { - It "has a CustomizedRulePath parameter" { - $params.ContainsKey("CustomizedRulePath") | Should Be $true + Context "CustomRulePath parameters" { + It "has a CustomRulePath parameter" { + $params.ContainsKey("CustomRulePath") | Should Be $true } - It "accepts string array" { - $params["CustomizedRulePath"].ParameterType.FullName | Should Be "System.String[]" + It "accepts a string" { + if ($testingLibraryUsage) + { + $params["CustomRulePath"].ParameterType.FullName | Should Be "System.String[]" + } + else + { + $params["CustomRulePath"].ParameterType.FullName | Should Be "System.String" + } } + + It "has a CustomizedRulePath alias"{ + $params.CustomRulePath.Aliases.Contains("CustomizedRulePath") | Should be $true + } } Context "IncludeRule parameters" { diff --git a/Tests/Engine/LibraryUsage.tests.ps1 b/Tests/Engine/LibraryUsage.tests.ps1 index 5e3c9694b..49a1c6bdc 100644 --- a/Tests/Engine/LibraryUsage.tests.ps1 +++ b/Tests/Engine/LibraryUsage.tests.ps1 @@ -16,7 +16,11 @@ function Invoke-ScriptAnalyzer { [string] $ScriptDefinition, [Parameter(Mandatory = $false)] - [string[]] $CustomizedRulePath = $null, + [Alias("CustomizedRulePath")] + [string[]] $CustomRulePath = $null, + + [Parameter(Mandatory = $false)] + [switch] $RecurseCustomRulePath, [Parameter(Mandatory=$false)] [string[]] $ExcludeRule = $null, @@ -32,18 +36,28 @@ function Invoke-ScriptAnalyzer { [switch] $Recurse, [Parameter(Mandatory = $false)] - [switch] $SuppressedOnly - ) + [switch] $SuppressedOnly, - $scriptAnalyzer = New-Object "Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer" + [Parameter(Mandatory = $false)] + [string] $Profile = $null + ) + # There is an inconsistency between this implementation and c# implementation of the cmdlet. + # The CustomRulePath parameter here is of "string[]" type whereas in the c# implementation it is of "string" type. + # If we set the CustomRulePath parameter here to "string[]", then the library usage test fails when run as an administrator. + # We want to note that the library usage test doesn't fail when run as a non-admin user. + # The following is the error statement when the test runs as an administrator. + # Assert failed on "Initialize" with "7" argument(s): "Test failed due to terminating error: The module was expected to contain an assembly manifest. (Exception from HRESULT: 0x80131018)" + + $scriptAnalyzer = New-Object "Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer"; $scriptAnalyzer.Initialize( $runspace, $testOutputWriter, - $CustomizedRulePath, + $CustomRulePath, $IncludeRule, $ExcludeRule, $Severity, - $SuppressedOnly.IsPresent + $SuppressedOnly.IsPresent, + $Profile ); if ($PSCmdlet.ParameterSetName -eq "File") { diff --git a/Tests/Engine/samplerule/samplerule1.psm1 b/Tests/Engine/samplerule/samplerule1.psm1 new file mode 100644 index 000000000..cc94c6b1b --- /dev/null +++ b/Tests/Engine/samplerule/samplerule1.psm1 @@ -0,0 +1,47 @@ +#Requires -Version 3.0 + +<# +.SYNOPSIS + Uses #Requires -RunAsAdministrator instead of your own methods. +.DESCRIPTION + The #Requires statement prevents a script from running unless the Windows PowerShell version, modules, snap-ins, and module and snap-in version prerequisites are met. + From Windows PowerShell 4.0, the #Requires statement let script developers require that sessions be run with elevated user rights (run as Administrator). + Script developers does not need to write their own methods any more. + To fix a violation of this rule, please consider to use #Requires -RunAsAdministrator instead of your own methods. +.EXAMPLE + Measure-RequiresRunAsAdministrator -ScriptBlockAst $ScriptBlockAst +.INPUTS + [System.Management.Automation.Language.ScriptBlockAst] +.OUTPUTS + [OutputType([PSCustomObject[])] +.NOTES + None +#> +function Measure-RequiresRunAsAdministrator +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $testAst + ) + + + $results = @() + + $result = [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{"Message" = "this is help"; + "Extent" = $ast.Extent; + "RuleName" = $PSCmdlet.MyInvocation.InvocationName; + "Severity" = "Warning"} + + $results += $result + + + return $results + + +} +Export-ModuleMember -Function Measure* \ No newline at end of file diff --git a/Tests/Engine/samplerule/samplerule2/samplerule2.psm1 b/Tests/Engine/samplerule/samplerule2/samplerule2.psm1 new file mode 100644 index 000000000..cc94c6b1b --- /dev/null +++ b/Tests/Engine/samplerule/samplerule2/samplerule2.psm1 @@ -0,0 +1,47 @@ +#Requires -Version 3.0 + +<# +.SYNOPSIS + Uses #Requires -RunAsAdministrator instead of your own methods. +.DESCRIPTION + The #Requires statement prevents a script from running unless the Windows PowerShell version, modules, snap-ins, and module and snap-in version prerequisites are met. + From Windows PowerShell 4.0, the #Requires statement let script developers require that sessions be run with elevated user rights (run as Administrator). + Script developers does not need to write their own methods any more. + To fix a violation of this rule, please consider to use #Requires -RunAsAdministrator instead of your own methods. +.EXAMPLE + Measure-RequiresRunAsAdministrator -ScriptBlockAst $ScriptBlockAst +.INPUTS + [System.Management.Automation.Language.ScriptBlockAst] +.OUTPUTS + [OutputType([PSCustomObject[])] +.NOTES + None +#> +function Measure-RequiresRunAsAdministrator +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $testAst + ) + + + $results = @() + + $result = [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{"Message" = "this is help"; + "Extent" = $ast.Extent; + "RuleName" = $PSCmdlet.MyInvocation.InvocationName; + "Severity" = "Warning"} + + $results += $result + + + return $results + + +} +Export-ModuleMember -Function Measure* \ No newline at end of file diff --git a/Tests/Engine/samplerule/samplerule2/samplerule3/samplerule3.psm1 b/Tests/Engine/samplerule/samplerule2/samplerule3/samplerule3.psm1 new file mode 100644 index 000000000..cc94c6b1b --- /dev/null +++ b/Tests/Engine/samplerule/samplerule2/samplerule3/samplerule3.psm1 @@ -0,0 +1,47 @@ +#Requires -Version 3.0 + +<# +.SYNOPSIS + Uses #Requires -RunAsAdministrator instead of your own methods. +.DESCRIPTION + The #Requires statement prevents a script from running unless the Windows PowerShell version, modules, snap-ins, and module and snap-in version prerequisites are met. + From Windows PowerShell 4.0, the #Requires statement let script developers require that sessions be run with elevated user rights (run as Administrator). + Script developers does not need to write their own methods any more. + To fix a violation of this rule, please consider to use #Requires -RunAsAdministrator instead of your own methods. +.EXAMPLE + Measure-RequiresRunAsAdministrator -ScriptBlockAst $ScriptBlockAst +.INPUTS + [System.Management.Automation.Language.ScriptBlockAst] +.OUTPUTS + [OutputType([PSCustomObject[])] +.NOTES + None +#> +function Measure-RequiresRunAsAdministrator +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $testAst + ) + + + $results = @() + + $result = [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{"Message" = "this is help"; + "Extent" = $ast.Extent; + "RuleName" = $PSCmdlet.MyInvocation.InvocationName; + "Severity" = "Warning"} + + $results += $result + + + return $results + + +} +Export-ModuleMember -Function Measure* \ No newline at end of file