diff --git a/CustomizedRuleDocumentation.md b/CustomizedRuleDocumentation.md new file mode 100644 index 000000000..51e58f5a1 --- /dev/null +++ b/CustomizedRuleDocumentation.md @@ -0,0 +1,158 @@ +##Documentation for Customized Rules in PowerShell Scripts +PSScriptAnalyzer uses MEF(Managed Extensibility Framework) to import all rules defined in the assembly. It can also consume rules written in PowerShell scripts. When calling Invoke-ScriptAnalyzer, users can pass customized rules using parameter -CustomizedRulePath to apply rule checkings on the scripts. + +This documentation serves as a basic guideline on how to define customized rules. + +###Basics +- Functions should have comment-based help. Make sure .DESCRIPTION field is there, as it will be consumed as rule description for the customized rule. +``` +<# +.SYNOPSIS + Name of your rule. +.DESCRIPTION + This would be the description of your rule. Please refer to Rule Documentation for consistent rule messages. +.EXAMPLE +.INPUTS +.OUTPUTS +.NOTES +#> +``` + +- Output type should be DiagnosticRecord: +``` +[OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] +``` + +- Make sure each function takes either a Token or an Ast as a parameter +``` +Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $testAst + ) +``` + +- DiagnosticRecord should have four properties: Message, Extent, RuleName and Severity +``` +$result = [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{"Message" = "This is a sample rule"; + "Extent" = $ast.Extent; + "RuleName" = $PSCmdlet.MyInvocation.InvocationName; + "Severity" = "Warning"} +``` + +- Make sure you export the function(s) at the end of the script using Export-ModuleMember +``` +Export-ModuleMember -Function (FunctionName) +``` + +###Example +``` +<# +.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 + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] +.NOTES + None +#> +function Measure-RequiresRunAsAdministrator +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + + Process + { + $results = @() + try + { + #region Define predicates to find ASTs. + # Finds specific method, IsInRole. + [ScriptBlock]$predicate1 = { + param ([System.Management.Automation.Language.Ast]$Ast) + [bool]$returnValue = $false + if ($Ast -is [System.Management.Automation.Language.MemberExpressionAst]) + { + [System.Management.Automation.Language.MemberExpressionAst]$meAst = $ast; + if ($meAst.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) + { + [System.Management.Automation.Language.StringConstantExpressionAst]$sceAst = $meAst.Member; + if ($sceAst.Value -eq "isinrole") + { + $returnValue = $true; + } + } + } + return $returnValue + } + + # Finds specific value, [system.security.principal.windowsbuiltinrole]::administrator. + [ScriptBlock]$predicate2 = { + param ([System.Management.Automation.Language.Ast]$Ast) + [bool]$returnValue = $false + if ($ast -is [System.Management.Automation.Language.AssignmentStatementAst]) + { + [System.Management.Automation.Language.AssignmentStatementAst]$asAst = $Ast; + if ($asAst.Right.ToString().ToLower() -eq "[system.security.principal.windowsbuiltinrole]::administrator") + { + $returnValue = $true + } + } + return $returnValue + } + #endregion + #region Finds ASTs that match the predicates. + + [System.Management.Automation.Language.Ast[]]$methodAst = $ScriptBlockAst.FindAll($predicate1, $true) + [System.Management.Automation.Language.Ast[]]$assignmentAst = $ScriptBlockAst.FindAll($predicate2, $true) + if ($null -ne $ScriptBlockAst.ScriptRequirements) + { + if ((!$ScriptBlockAst.ScriptRequirements.IsElevationRequired) -and + ($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) + { + $result = [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord]@{"Message" = $Messages.MeasureRequiresRunAsAdministrator; + "Extent" = $assignmentAst.Extent; + "RuleName" = $PSCmdlet.MyInvocation.InvocationName; + "Severity" = "Information"} + $results += $result + } + } + else + { + if (($methodAst.Count -ne 0) -and ($assignmentAst.Count -ne 0)) + { + $result = [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord]@{"Message" = $Messages.MeasureRequiresRunAsAdministrator; + "Extent" = $assignmentAst.Extent; + "RuleName" = $PSCmdlet.MyInvocation.InvocationName; + "Severity" = "Information"} + $results += $result + } + } + return $results + #endregion + } + catch + { + $PSCmdlet.ThrowTerminatingError($PSItem) + } + } +} +``` +More examples can be found in *Tests\Engine\CommunityRules* diff --git a/Engine/Generic/ExternalRule.cs b/Engine/Generic/ExternalRule.cs index 934bbc831..c73792e28 100644 --- a/Engine/Generic/ExternalRule.cs +++ b/Engine/Generic/ExternalRule.cs @@ -28,7 +28,7 @@ internal class ExternalRule : IExternalRule string param = string.Empty; string srcName = string.Empty; string modPath = string.Empty; - + string paramType = string.Empty; public string GetName() { @@ -55,6 +55,11 @@ public SourceType GetSourceType() return SourceType.Module; } + public string GetParameterType() + { + return this.paramType; + } + //Set the community rule level as warning as the current implementation does not require user to specify rule severity when defining their functions in PS scripts public RuleSeverity GetSeverity() { @@ -80,7 +85,7 @@ public ExternalRule() } - public ExternalRule(string name, string commonName, string desc, string param, string srcName, string modPath) + public ExternalRule(string name, string commonName, string desc, string param, string paramType, string srcName, string modPath) { this.name = name; this.commonName = commonName; @@ -88,6 +93,7 @@ public ExternalRule(string name, string commonName, string desc, string param, s this.param = param; this.srcName = srcName; this.modPath = modPath; + this.paramType = paramType; } #endregion diff --git a/Engine/Generic/RuleInfo.cs b/Engine/Generic/RuleInfo.cs index b4c6e4369..666c84afa 100644 --- a/Engine/Generic/RuleInfo.cs +++ b/Engine/Generic/RuleInfo.cs @@ -35,7 +35,7 @@ public class RuleInfo /// Name: The name of the rule. /// [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - public string Name + public string RuleName { get { return name; } private set { name = value; } @@ -102,7 +102,7 @@ public RuleSeverity Severity /// Source name of the rule. public RuleInfo(string name, string commonName, string description, SourceType sourceType, string sourceName, RuleSeverity severity) { - Name = name; + RuleName = name; CommonName = commonName; Description = description; SourceType = sourceType; diff --git a/Engine/PSScriptAnalyzer.psd1 b/Engine/PSScriptAnalyzer.psd1 index 4e82b6f12..2c95981f9 100644 --- a/Engine/PSScriptAnalyzer.psd1 +++ b/Engine/PSScriptAnalyzer.psd1 @@ -3,6 +3,9 @@ # @{ + +# Author of this module +Author = 'Microsoft Corporation' # Script module or binary module file associated with this manifest. RootModule = 'Microsoft.Windows.Powershell.ScriptAnalyzer.dll' @@ -14,10 +17,10 @@ ModuleVersion = '1.0' GUID = '324fc715-36bf-4aee-8e58-72e9b4a08ad9' # Company or vendor of this module -CompanyName = 'Microsoft' +CompanyName = 'Microsoft Corporation' # Copyright statement for this module -Copyright = '(c) 2015. All rights reserved.' +Copyright = '(c) Microsoft Corporation 2015. All rights reserved.' # Description of the functionality provided by this module Description = 'PSScriptAnalyzer provides script analysis and checks for potential code defects in the scripts by applying a group of builtin or customized rules on the scripts being analyzed.' diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index bfd11f969..de3e0c23e 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -10,6 +10,7 @@ // THE SOFTWARE. // +using System.Text.RegularExpressions; using Microsoft.Windows.Powershell.ScriptAnalyzer.Commands; using Microsoft.Windows.Powershell.ScriptAnalyzer.Generic; using System; @@ -141,7 +142,7 @@ public void Initilaize(Dictionary> result) paths = result.ContainsKey("ValidDllPaths") ? result["ValidDllPaths"] : result["ValidPaths"]; foreach (string path in paths) { - if (Path.GetExtension(path).ToLower(CultureInfo.CurrentCulture) == ".dll") + if (String.Equals(Path.GetExtension(path),".dll",StringComparison.OrdinalIgnoreCase)) { catalog.Catalogs.Add(new AssemblyCatalog(path)); } @@ -188,8 +189,18 @@ public IEnumerable GetRule(string[] moduleNames, string[] ruleNames) if (ruleNames != null) { - results = rules.Where(item => - ruleNames.Contains(item.GetName(), StringComparer.OrdinalIgnoreCase)); + //Check wild card input for -Name parameter and create regex match patterns + List regexList = new List(); + foreach (string ruleName in ruleNames) + { + Regex includeRegex = new Regex(String.Format("^{0}$", Regex.Escape(ruleName).Replace(@"\*", ".*")), RegexOptions.IgnoreCase); + regexList.Add(includeRegex); + } + + results = from rule in rules + from regex in regexList + where regex.IsMatch(rule.GetName()) + select rule; } else { @@ -230,8 +241,8 @@ public List GetExternalRule(string[] moduleNames) FunctionInfo funcInfo = (FunctionInfo)psobject.ImmediateBaseObject; ParameterMetadata param = funcInfo.Parameters.Values - .First(item => item.Name.ToLower(CultureInfo.CurrentCulture).EndsWith("ast", StringComparison.CurrentCulture) || - item.Name.ToLower(CultureInfo.CurrentCulture).EndsWith("token", StringComparison.CurrentCulture)); + .First(item => item.Name.EndsWith("ast", StringComparison.OrdinalIgnoreCase) || + item.Name.EndsWith("token", StringComparison.OrdinalIgnoreCase)); //Only add functions that are defined as rules. if (param != null) @@ -240,7 +251,7 @@ public List GetExternalRule(string[] moduleNames) string desc =posh.AddScript(script).Invoke()[0].ImmediateBaseObject.ToString() .Replace("\r\n", " ").Trim(); - rules.Add(new ExternalRule(funcInfo.Name, funcInfo.Name, desc, param.Name, + rules.Add(new ExternalRule(funcInfo.Name, funcInfo.Name, desc, param.Name,param.ParameterType.FullName, funcInfo.ModuleName, funcInfo.Module.Path)); } } @@ -278,13 +289,13 @@ public IEnumerable GetExternalRecord(Ast ast, Token[] token, E // Groups rules by AstType and Tokens. Dictionary> astRuleGroups = rules - .Where(item => item.GetParameter().EndsWith("ast", true, CultureInfo.CurrentCulture)) - .GroupBy(item => item.GetParameter()) + .Where(item => item.GetParameter().EndsWith("ast", StringComparison.OrdinalIgnoreCase)) + .GroupBy(item => item.GetParameterType()) .ToDictionary(item => item.Key, item => item.ToList()); Dictionary> tokenRuleGroups = rules - .Where(item => item.GetParameter().EndsWith("token", true, CultureInfo.CurrentCulture)) - .GroupBy(item => item.GetParameter()) + .Where(item => item.GetParameter().EndsWith("token", StringComparison.OrdinalIgnoreCase)) + .GroupBy(item => item.GetParameterType()) .ToDictionary(item => item.Key, item => item.ToList()); using (rsp) @@ -326,7 +337,7 @@ public IEnumerable GetExternalRecord(Ast ast, Token[] token, E { // Find all AstTypes that appeared in rule groups. IEnumerable childAsts = ast.FindAll(new Func((testAst) => - (testAst.GetType().Name.ToLower(CultureInfo.CurrentCulture) == astRuleGroup.Key.ToLower(CultureInfo.CurrentCulture))), false); + (astRuleGroup.Key.IndexOf(testAst.GetType().FullName,StringComparison.OrdinalIgnoreCase) != -1)), false); foreach (Ast childAst in childAsts) { @@ -354,49 +365,63 @@ public IEnumerable GetExternalRecord(Ast ast, Token[] token, E } #endregion - #region Collects the results from commands. - - for (int i = 0; i < powerShellCommands.Count; i++) + List diagnostics = new List(); + try { - // EndInvoke will wait for each command to finish, so we will be getting the commands - // in the same order that they have been invoked withy BeginInvoke. - PSDataCollection psobjects = powerShellCommands[i].EndInvoke(powerShellCommandResults[i]); - - foreach (var psobject in psobjects) + for (int i = 0; i < powerShellCommands.Count; i++) { - DiagnosticSeverity severity; - IScriptExtent extent; - string message = string.Empty; - string ruleName = string.Empty; - - // Because error stream is merged to output stream, - // we need to handle the error records. - if (psobject.ImmediateBaseObject is ErrorRecord) - { - ErrorRecord record = (ErrorRecord)psobject.ImmediateBaseObject; - command.WriteError(record); - continue; - } + // EndInvoke will wait for each command to finish, so we will be getting the commands + // in the same order that they have been invoked withy BeginInvoke. + PSDataCollection psobjects = powerShellCommands[i].EndInvoke(powerShellCommandResults[i]); - // DiagnosticRecord may not be correctly returned from external rule. - try - { - Enum.TryParse(psobject.Properties["Severity"].Value.ToString().ToUpper(), out severity); - message = psobject.Properties["Message"].Value.ToString(); - extent = (IScriptExtent)psobject.Properties["Extent"].Value; - ruleName = psobject.Properties["RuleName"].Value.ToString(); - } - catch (Exception ex) + foreach (var psobject in psobjects) { - command.WriteError(new ErrorRecord(ex, ex.HResult.ToString("X"), ErrorCategory.NotSpecified, this)); - continue; + DiagnosticSeverity severity; + IScriptExtent extent; + string message = string.Empty; + string ruleName = string.Empty; + + if (psobject != null && psobject.ImmediateBaseObject != null) + { + // Because error stream is merged to output stream, + // we need to handle the error records. + if (psobject.ImmediateBaseObject is ErrorRecord) + { + ErrorRecord record = (ErrorRecord)psobject.ImmediateBaseObject; + command.WriteError(record); + continue; + } + + // DiagnosticRecord may not be correctly returned from external rule. + try + { + Enum.TryParse(psobject.Properties["Severity"].Value.ToString().ToUpper(), out severity); + message = psobject.Properties["Message"].Value.ToString(); + extent = (IScriptExtent)psobject.Properties["Extent"].Value; + ruleName = psobject.Properties["RuleName"].Value.ToString(); + } + catch (Exception ex) + { + command.WriteError(new ErrorRecord(ex, ex.HResult.ToString("X"), ErrorCategory.NotSpecified, this)); + continue; + } + + if (!string.IsNullOrEmpty(message)) + { + diagnostics.Add(new DiagnosticRecord(message, extent, ruleName, severity, null)); + } + } } - - if (!string.IsNullOrEmpty(message)) yield return new DiagnosticRecord(message, extent, ruleName, severity, null); } } + //Catch exception where customized defined rules have exceptins when doing invoke + catch(Exception ex) + { + command.WriteError(new ErrorRecord(ex, ex.HResult.ToString("X"), ErrorCategory.NotSpecified, this)); + } + return diagnostics; #endregion } } @@ -467,7 +492,7 @@ public Dictionary> CheckRuleExtension(string[] path, PSCmdl cmdlet.WriteDebug(string.Format(CultureInfo.CurrentCulture, Strings.CheckAssemblyFile, resolvedPath)); - if (Path.GetExtension(resolvedPath).ToLower(CultureInfo.CurrentCulture) == ".dll") + if (String.Equals(Path.GetExtension(resolvedPath),".dll", StringComparison.OrdinalIgnoreCase)) { if (!File.Exists(resolvedPath)) { diff --git a/Engine/ScriptAnalyzer.format.ps1xml b/Engine/ScriptAnalyzer.format.ps1xml index f8de0fcde..9839d5eac 100644 --- a/Engine/ScriptAnalyzer.format.ps1xml +++ b/Engine/ScriptAnalyzer.format.ps1xml @@ -10,7 +10,7 @@ 35 - + 12 @@ -18,7 +18,7 @@ 10 - + 5 @@ -66,7 +66,7 @@ - 10 + 12 @@ -116,7 +116,7 @@ 35 - + 15 @@ -136,7 +136,7 @@ - Name + RuleName Severity diff --git a/Engine/SpecialVars.cs b/Engine/SpecialVars.cs index 298c0ba16..2c3e506b5 100644 --- a/Engine/SpecialVars.cs +++ b/Engine/SpecialVars.cs @@ -38,6 +38,7 @@ internal class SpecialVars internal const string MyInvocation = "MyInvocation"; internal const string PSScriptRoot = "PSScriptRoot"; internal const string PSCommandPath = "PSCommandPath"; + internal const string ExecutionContext = "ExecutionContext"; internal static readonly string[] InitializedVariables; @@ -66,7 +67,8 @@ static SpecialVars() PSBoundParameters, MyInvocation, PSScriptRoot, - PSCommandPath, + PSCommandPath, + ExecutionContext }; internal static readonly Type[] AutomaticVariableTypes = new Type[] { @@ -79,6 +81,7 @@ static SpecialVars() /* MyInvocation */ typeof(InvocationInfo), /* PSScriptRoot */ typeof(string), /* PSCommandPath */ typeof(string), + /* ExecutionContext */ typeof(EngineIntrinsics), }; diff --git a/ScriptAnalyzer.sln b/PSScriptAnalyzer.sln similarity index 100% rename from ScriptAnalyzer.sln rename to PSScriptAnalyzer.sln diff --git a/README.md b/README.md index 97fc1146e..b2a3b6996 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -Introduction + +Introduction ============ PSScriptAnalyzer is a static code checker for Windows PowerShell modules and scripts. PSScriptAnalyzer checks the quality of Windows PowerShell code by running a set of rules. The rules are based on PowerShell best practices identified by PowerShell Team and the community. It generates DiagnosticResults (errors and warnings) to inform users about potential code defects and suggests possible solutions for improvements. @@ -16,7 +17,9 @@ Invoke-ScriptAnalyzer [-Path] [-CustomizedRulePath ] [-Exclud Requirements ============ -WS2012R2 / Windows 8.1 / Windows OS running PowerShell v5.0 which can be obtained using [Windows Management Framework 5.0 Preview February 2015](http://go.microsoft.com/fwlink/?LinkId=398175). +WS2012R2 / Windows 8.1 / Windows OS running **PowerShell v5.0** and **Windows Management Framework 5.0 Preview** + +Download the latest WMF package from [Windows Management Framework 5.0 Preview](http://go.microsoft.com/fwlink/?LinkId=398175). Installation ============ @@ -27,6 +30,7 @@ Installation ```powershell Import-Module PSScriptAnalyzer ``` +If you have previous version of PSScriptAnalyzer installed on your machine, you may need to override old binaries by copying content of [``~/ProjectRoot/PSScriptAnalyzer``] to PSModulePath. To confirm installation: run ```Get-ScriptAnalyzerRule``` in the PowerShell console to obtain the built-in rules @@ -35,6 +39,8 @@ Building the Code Use Visual Studio to build "ScriptAnalyzer.sln". Use ~/PSScriptAnalyzer/ folder to load PSScriptAnalyzer.psd1 +**Note: If there are any build errors, please refer to Requirements section and make sure all dependencies are properly installed** + Running Tests ============= @@ -48,6 +54,13 @@ Pester-based ScriptAnalyzer Tests are located in ```/PSScriptAnalyzer/Te .\*.ps1 (Example - .\ AvoidConvertToSecureStringWithPlainText.ps1) *You can also run all tests under \Engine or \Rules by calling Invoke-Pester in the Engine/Rules directory. +Project Management Dashboard +============================== + +You can track issues, pull requests, backlog items here: + +[![Stories in Ready](https://badge.waffle.io/PowerShell/PSScriptAnalyzer.png?label=ready&title=Ready)](https://waffle.io/PowerShell/PSScriptAnalyzer) + Contributing to ScriptAnalyzer ============================== diff --git a/RuleDocumentation/AvoidUninitializedVariable.md b/RuleDocumentation/AvoidUninitializedVariable.md index 85807bff9..68ee59164 100644 --- a/RuleDocumentation/AvoidUninitializedVariable.md +++ b/RuleDocumentation/AvoidUninitializedVariable.md @@ -17,8 +17,8 @@ Wrong: function NotGlobal { $localVars = "Localization?" - $unitialized - Write-Output $unitialized + $uninitialized + Write-Output $uninitialized } @@ -27,4 +27,4 @@ Correct: function NotGlobal { $localVars = "Localization?" Write-Output $localVars - } \ No newline at end of file + } diff --git a/RuleDocumentation/AvoidUsingWMICmdlet.md b/RuleDocumentation/AvoidUsingWMICmdlet.md new file mode 100644 index 000000000..01d6cca01 --- /dev/null +++ b/RuleDocumentation/AvoidUsingWMICmdlet.md @@ -0,0 +1,26 @@ +#AvoidUsingWMICmdlet +**Severity Level: Warning** + + +##Description + +Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance + +For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems. + +##How to Fix + +Use corresponding CIM cmdlets such as Get-CIMInstance, Remove-CIMInstance, Invoke-CIMMethod, Register-CimIndicationEvent, Set-CimInstance + +##Example + +Wrong: +``` +Get-WmiObject -Query 'Select * from Win32_Process where name LIKE "myprocess%"' | Remove-WmiObject +Invoke-WmiMethod –Class Win32_Process –Name "Create" –ArgumentList @{ CommandLine = "notepad.exe" } +``` +Correct: +``` +Get-CimInstance -Query 'Select * from Win32_Process where name LIKE "myprocess%"' | Remove-CIMInstance +Invoke-CimMethod –ClassName Win32_Process –MethodName "Create" –Arguments @{ CommandLine = "notepad.exe" } +``` diff --git a/RuleDocumentation/DscTestsPresent.md b/RuleDocumentation/DscTestsPresent.md new file mode 100644 index 000000000..0ed054337 --- /dev/null +++ b/RuleDocumentation/DscTestsPresent.md @@ -0,0 +1,54 @@ +#DscTestsPresent +**Severity Level: Information** + + +##Description + +Checks that DSC tests for given resource are present. + +##How to Fix + +To fix a violation of this rule, please make sure Tests directory is present: +* For non-class based resources it should exist at the same folder level as DSCResources folder. +* For class based resources it should be present at the same folder level as resource psm1 file. + +Tests folder should contain test script for given resource - file name should contain resource's name. + +##Example + +### Non-class based resource + +Let's assume we have non-class based resource with a following file structure: + +* xAzure + * DSCResources + * MSFT_xAzureSubscription + * MSFT_xAzureSubscription.psm1 + * MSFT_xAzureSubscription.schema.mof + +In this case, to fix this warning, we should add tests in a following way: + +* xAzure + * DSCResources + * MSFT_xAzureSubscription + * MSFT_xAzureSubscription.psm1 + * MSFT_xAzureSubscription.schema.mof + * Tests + * MSFT_xAzureSubscription_Tests.ps1 + +### Class based resource + +Let's assume we have class based resource with a following file structure: + +* MyDscResource + * MyDscResource.psm1 + * MyDscresource.psd1 + +In this case, to fix this warning, we should add tests in a following way: + +* MyDscResource + * MyDscResource.psm1 + * MyDscresource.psd1 + * Tests + * MyDscResource_Tests.ps1 + diff --git a/Rules/AvoidUnitializedVariable.cs b/Rules/AvoidUninitializedVariable.cs similarity index 100% rename from Rules/AvoidUnitializedVariable.cs rename to Rules/AvoidUninitializedVariable.cs diff --git a/Rules/AvoidUsingWMIObjectCmdlet.cs b/Rules/AvoidUsingWMICmdlet.cs similarity index 78% rename from Rules/AvoidUsingWMIObjectCmdlet.cs rename to Rules/AvoidUsingWMICmdlet.cs index 6c71f1646..079f0ec14 100644 --- a/Rules/AvoidUsingWMIObjectCmdlet.cs +++ b/Rules/AvoidUsingWMICmdlet.cs @@ -28,19 +28,19 @@ namespace Microsoft.Windows.Powershell.ScriptAnalyzer.BuiltinRules { /// - /// AvoidUsingWMIObjectCmdlet: Verify that Get-WMIObject, Remove-WMIObject are not used + /// AvoidUsingWMICmdlet: Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance /// [Export(typeof(IScriptRule))] - public class AvoidUsingWMIObjectCmdlet : IScriptRule + public class AvoidUsingWMICmdlet : IScriptRule { /// - /// AnalyzeScript: Verify that Get-WMIObject, Remove-WMIObject are not used + /// AnalyzeScript: Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance /// public IEnumerable AnalyzeScript(Ast ast, string fileName) { if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); - // Rule is applicable only when PowerShell Version is < 3.0, since Get-CIMInstance was introduced in 3.0 + // Rule is applicable only when PowerShell Version is < 3.0, since CIM cmdlet was introduced in 3.0 int majorPSVersion = GetPSMajorVersion(ast); if (!(3 > majorPSVersion && 0 < majorPSVersion)) { @@ -50,9 +50,15 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) // Iterate all CommandAsts and check the command name foreach (CommandAst cmdAst in commandAsts) { - if (cmdAst.GetCommandName() != null && (String.Equals(cmdAst.GetCommandName(), "get-wmiobject", StringComparison.OrdinalIgnoreCase) || String.Equals(cmdAst.GetCommandName(), "remove-wmiobject", StringComparison.OrdinalIgnoreCase))) + if (cmdAst.GetCommandName() != null && + (String.Equals(cmdAst.GetCommandName(), "get-wmiobject", StringComparison.OrdinalIgnoreCase) + || String.Equals(cmdAst.GetCommandName(), "remove-wmiobject", StringComparison.OrdinalIgnoreCase) + || String.Equals(cmdAst.GetCommandName(), "invoke-wmimethod", StringComparison.OrdinalIgnoreCase) + || String.Equals(cmdAst.GetCommandName(), "register-wmievent", StringComparison.OrdinalIgnoreCase) + || String.Equals(cmdAst.GetCommandName(), "set-wmiinstance", StringComparison.OrdinalIgnoreCase)) + ) { - yield return new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWMIObjectCmdletError, System.IO.Path.GetFileName(fileName)), + yield return new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWMICmdletError, System.IO.Path.GetFileName(fileName)), cmdAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); } } @@ -87,7 +93,7 @@ private int GetPSMajorVersion(Ast ast) /// The name of this rule public string GetName() { - return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.AvoidUsingWMIObjectCmdletName); + return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.AvoidUsingWMICmdletName); } /// @@ -96,7 +102,7 @@ public string GetName() /// The common name of this rule public string GetCommonName() { - return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWMIObjectCmdletCommonName); + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWMICmdletCommonName); } /// @@ -105,7 +111,7 @@ public string GetCommonName() /// The description of this rule public string GetDescription() { - return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWMIObjectCmdletDescription); + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWMICmdletDescription); } /// diff --git a/Rules/AvoidUsingWriteHost.cs b/Rules/AvoidUsingWriteHost.cs index 597d7cf4a..fcc06a38b 100644 --- a/Rules/AvoidUsingWriteHost.cs +++ b/Rules/AvoidUsingWriteHost.cs @@ -23,8 +23,11 @@ namespace Microsoft.Windows.Powershell.ScriptAnalyzer.BuiltinRules /// AvoidUsingWriteHost: Check that Write-Host or Console.Write are not used /// [Export(typeof(IScriptRule))] - public class AvoidUsingWriteHost : IScriptRule + public class AvoidUsingWriteHost : AstVisitor, IScriptRule { + List records; + string fileName; + /// /// AnalyzeScript: check that Write-Host or Console.Write are not used. /// @@ -32,34 +35,78 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) { if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage); - // Finds all CommandAsts. - IEnumerable commandAsts = ast.FindAll(testAst => testAst is CommandAst, true); + records = new List(); + this.fileName = fileName; + + ast.Visit(this); + + return records; + } + + + /// + /// Visit function and skips any function that starts with show + /// + /// + /// + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst funcAst) + { + if (funcAst == null || funcAst.Name == null) + { + return AstVisitAction.SkipChildren; + } + + if (funcAst.Name.StartsWith("show", StringComparison.OrdinalIgnoreCase)) + { + return AstVisitAction.SkipChildren; + } + + return AstVisitAction.Continue; + } + + /// + /// Checks that write-host command is not used + /// + /// + /// + public override AstVisitAction VisitCommand(CommandAst cmdAst) + { + if (cmdAst == null) + { + return AstVisitAction.SkipChildren; + } - // Iterrates all CommandAsts and check the command name. - foreach (CommandAst cmdAst in commandAsts) + if (cmdAst.GetCommandName() != null && String.Equals(cmdAst.GetCommandName(), "write-host", StringComparison.OrdinalIgnoreCase)) { - if (cmdAst.GetCommandName() != null && String.Equals(cmdAst.GetCommandName(), "write-host", StringComparison.OrdinalIgnoreCase)) - { - yield return new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWriteHostError, System.IO.Path.GetFileName(fileName)), - cmdAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); - } + records.Add(new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWriteHostError, System.IO.Path.GetFileName(fileName)), + cmdAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName)); } - // Finds all InvokeMemberExpressionAst - IEnumerable invokeAsts = ast.FindAll(testAst => testAst is InvokeMemberExpressionAst, true); + return AstVisitAction.Continue; + } - foreach (InvokeMemberExpressionAst invokeAst in invokeAsts) + public override AstVisitAction VisitInvokeMemberExpression(InvokeMemberExpressionAst imeAst) + { + if (imeAst == null) { - TypeExpressionAst typeAst = invokeAst.Expression as TypeExpressionAst; - if (typeAst == null || typeAst.TypeName == null || typeAst.TypeName.FullName == null) continue; - - if (typeAst.TypeName.FullName.EndsWith("console", StringComparison.OrdinalIgnoreCase) - && !String.IsNullOrWhiteSpace(invokeAst.Member.Extent.Text) && invokeAst.Member.Extent.Text.StartsWith("Write", StringComparison.OrdinalIgnoreCase)) - { - yield return new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingConsoleWriteError, System.IO.Path.GetFileName(fileName), invokeAst.Member.Extent.Text), - invokeAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); - } + return AstVisitAction.SkipChildren; } + + TypeExpressionAst typeAst = imeAst.Expression as TypeExpressionAst; + + if (typeAst == null || typeAst.TypeName == null || typeAst.TypeName.FullName == null) + { + return AstVisitAction.SkipChildren; + } + + if (typeAst.TypeName.FullName.EndsWith("console", StringComparison.OrdinalIgnoreCase) + && !String.IsNullOrWhiteSpace(imeAst.Member.Extent.Text) && imeAst.Member.Extent.Text.StartsWith("Write", StringComparison.OrdinalIgnoreCase)) + { + records.Add(new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingConsoleWriteError, System.IO.Path.GetFileName(fileName), imeAst.Member.Extent.Text), + imeAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName)); + } + + return AstVisitAction.Continue; } /// diff --git a/Rules/DscTestsPresent.cs b/Rules/DscTestsPresent.cs new file mode 100644 index 000000000..15fbbda23 --- /dev/null +++ b/Rules/DscTestsPresent.cs @@ -0,0 +1,164 @@ +// +// Copyright (c) Microsoft Corporation. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; +using Microsoft.Windows.Powershell.ScriptAnalyzer.Generic; +using System.ComponentModel.Composition; +using System.Globalization; +using System.IO; +using System.Management.Automation; + +namespace Microsoft.Windows.Powershell.ScriptAnalyzer.BuiltinRules +{ + /// + /// DscTestsPresent: Checks that DSC tests for given resource are present. + /// Rule expects directory Tests to be present: + /// For non-class based resources it should exist at the same folder level as DSCResources folder. + /// For class based resources it should be present at the same folder level as resource psm1 file. + /// Tests folder should contain test script for given resource - file name should contain resource's name. + /// + [Export(typeof(IDSCResourceRule))] + public class DscTestsPresent : IDSCResourceRule + { + /// + /// AnalyzeDSCResource: Analyzes given DSC Resource + /// + /// The script's ast + /// The name of the script file being analyzed + /// The results of the analysis + public IEnumerable AnalyzeDSCResource(Ast ast, string fileName) + { + String fileNameOnly = Path.GetFileName(fileName); + String resourceName = Path.GetFileNameWithoutExtension(fileNameOnly); + String testsQuery = String.Format("*{0}*", resourceName); + Boolean testsPresent = false; + String expectedTestsPath = Path.Combine(new String[] { fileName, "..", "..", "..", "Tests" }); + + // Verify tests are present + if (Directory.Exists(expectedTestsPath)) + { + DirectoryInfo testsFolder = new DirectoryInfo(expectedTestsPath); + FileInfo[] testFiles = testsFolder.GetFiles(testsQuery); + if (testFiles.Length != 0) + { + testsPresent = true; + } + } + + // Return error if no tests present + if (!testsPresent) + { + yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.DscTestsPresentNoTestsError, resourceName), + ast.Extent, GetName(), DiagnosticSeverity.Information, fileName); + } + } + + /// + /// AnalyzeDSCClass: Analyzes given DSC class + /// + /// + /// + /// + public IEnumerable AnalyzeDSCClass(Ast ast, string fileName) + { + String resourceName = null; + + IEnumerable dscClasses = ast.FindAll(item => + item is TypeDefinitionAst + && ((item as TypeDefinitionAst).IsClass) + && (item as TypeDefinitionAst).Attributes.Any(attr => String.Equals("DSCResource", attr.TypeName.FullName, StringComparison.OrdinalIgnoreCase)), true); + + foreach (TypeDefinitionAst dscClass in dscClasses) + { + resourceName = dscClass.Name; + + String testsQuery = String.Format("*{0}*", resourceName); + Boolean testsPresent = false; + String expectedTestsPath = Path.Combine(new String[] { fileName, "..", "Tests" }); + + // Verify tests are present + if (Directory.Exists(expectedTestsPath)) + { + DirectoryInfo testsFolder = new DirectoryInfo(expectedTestsPath); + FileInfo[] testFiles = testsFolder.GetFiles(testsQuery); + if (testFiles.Length != 0) + { + testsPresent = true; + } + } + + // Return error if no tests present + if (!testsPresent) + { + yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.DscTestsPresentNoTestsError, resourceName), + dscClass.Extent, GetName(), DiagnosticSeverity.Information, fileName); + } + } + } + + /// + /// GetName: Retrieves the name of this rule. + /// + /// The name of this rule + public string GetName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.DscTestsPresent); + } + + /// + /// GetCommonName: Retrieves the Common name of this rule. + /// + /// The common name of this rule + public string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.DscTestsPresentCommonName); + } + + /// + /// GetDescription: Retrieves the description of this rule. + /// + /// The description of this rule + public string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.DscTestsPresentDescription); + } + + /// + /// GetSourceType: Retrieves the type of the rule: builtin, managed or module. + /// + public SourceType GetSourceType() + { + return SourceType.Builtin; + } + + /// + /// GetSeverity: Retrieves the severity of the rule: error, warning or information. + /// + /// + public RuleSeverity GetSeverity() + { + return RuleSeverity.Information; + } + + /// + /// GetSourceName: Retrieves the module/assembly name the rule is from. + /// + public string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.DSCSourceName); + } + } + +} \ No newline at end of file diff --git a/Rules/ScriptAnalyzerBuiltinRules.csproj b/Rules/ScriptAnalyzerBuiltinRules.csproj index a11c83100..7401ac269 100644 --- a/Rules/ScriptAnalyzerBuiltinRules.csproj +++ b/Rules/ScriptAnalyzerBuiltinRules.csproj @@ -59,15 +59,16 @@ - + - + + diff --git a/Rules/Strings.Designer.cs b/Rules/Strings.Designer.cs index ef44c1fcd..641440213 100644 --- a/Rules/Strings.Designer.cs +++ b/Rules/Strings.Designer.cs @@ -718,38 +718,38 @@ internal static string AvoidUsingPositionalParametersName { } /// - /// Looks up a localized string similar to Avoid Using Get-WMIObject, Remove-WMIObject. + /// Looks up a localized string similar to Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance. /// - internal static string AvoidUsingWMIObjectCmdletCommonName { + internal static string AvoidUsingWMICmdletCommonName { get { - return ResourceManager.GetString("AvoidUsingWMIObjectCmdletCommonName", resourceCulture); + return ResourceManager.GetString("AvoidUsingWMICmdletCommonName", resourceCulture); } } /// - /// Looks up a localized string similar to Depricated. Starting in Windows PowerShell 3.0, these cmdlets have been superseded by CimInstance cmdlets.. + /// Looks up a localized string similar to Depricated. Starting in Windows PowerShell 3.0, these cmdlets have been superseded by CIM cmdlets.. /// - internal static string AvoidUsingWMIObjectCmdletDescription { + internal static string AvoidUsingWMICmdletDescription { get { - return ResourceManager.GetString("AvoidUsingWMIObjectCmdletDescription", resourceCulture); + return ResourceManager.GetString("AvoidUsingWMICmdletDescription", resourceCulture); } } /// - /// Looks up a localized string similar to File '{0}' uses WMIObject cmdlet. For PowerShell 3.0 and above, this is not recommended because the cmdlet is based on a non-standard DCOM protocol. Use CIMInstance cmdlet instead. This is CIM and WS-Man standards compliant and works in a heterogeneous environment.. + /// Looks up a localized string similar to File '{0}' uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems.. /// - internal static string AvoidUsingWMIObjectCmdletError { + internal static string AvoidUsingWMICmdletError { get { - return ResourceManager.GetString("AvoidUsingWMIObjectCmdletError", resourceCulture); + return ResourceManager.GetString("AvoidUsingWMICmdletError", resourceCulture); } } /// - /// Looks up a localized string similar to AvoidUsingWMIObjectCmdlet. + /// Looks up a localized string similar to AvoidUsingWMICmdlet. /// - internal static string AvoidUsingWMIObjectCmdletName { + internal static string AvoidUsingWMICmdletName { get { - return ResourceManager.GetString("AvoidUsingWMIObjectCmdletName", resourceCulture); + return ResourceManager.GetString("AvoidUsingWMICmdletName", resourceCulture); } } @@ -870,6 +870,42 @@ internal static string DSCSourceName { } } + /// + /// Looks up a localized string similar to DscTestsPresent. + /// + internal static string DscTestsPresent { + get { + return ResourceManager.GetString("DscTestsPresent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dsc tests are present. + /// + internal static string DscTestsPresentCommonName { + get { + return ResourceManager.GetString("DscTestsPresentCommonName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Every DSC resource module should contain folder "Tests" with tests for every resource. Test scripts should have resource name they are testing in the file name.. + /// + internal static string DscTestsPresentDescription { + get { + return ResourceManager.GetString("DscTestsPresentDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No tests found for resource '{0}'. + /// + internal static string DscTestsPresentNoTestsError { + get { + return ResourceManager.GetString("DscTestsPresentNoTestsError", resourceCulture); + } + } + /// /// Looks up a localized string similar to Module Manifest Fields. /// diff --git a/Rules/Strings.resx b/Rules/Strings.resx index c57821d3e..b26e4d6fa 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -666,17 +666,17 @@ UseShouldProcessForStateChangingFunctions - - Avoid Using Get-WMIObject, Remove-WMIObject + + Avoid Using Get-WMIObject, Remove-WMIObject, Invoke-WmiMethod, Register-WmiEvent, Set-WmiInstance - - Depricated. Starting in Windows PowerShell 3.0, these cmdlets have been superseded by CimInstance cmdlets. + + Depricated. Starting in Windows PowerShell 3.0, these cmdlets have been superseded by CIM cmdlets. - - File '{0}' uses WMIObject cmdlet. For PowerShell 3.0 and above, this is not recommended because the cmdlet is based on a non-standard DCOM protocol. Use CIMInstance cmdlet instead. This is CIM and WS-Man standards compliant and works in a heterogeneous environment. + + File '{0}' uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems. - - AvoidUsingWMIObjectCmdlet + + AvoidUsingWMICmdlet Use OutputType Correctly @@ -690,6 +690,18 @@ UseOutputTypeCorrectly + + DscTestsPresent + + + Dsc tests are present + + + Every DSC resource module should contain folder "Tests" with tests for every resource. Test scripts should have resource name they are testing in the file name. + + + No tests found for resource '{0}' + DscExamplesPresent diff --git a/Tests/Engine/CustomizedRule.tests.ps1 b/Tests/Engine/CustomizedRule.tests.ps1 new file mode 100644 index 000000000..2c5777bfd --- /dev/null +++ b/Tests/Engine/CustomizedRule.tests.ps1 @@ -0,0 +1,40 @@ +Import-Module PSScriptAnalyzer +$directory = Split-Path -Parent $MyInvocation.MyCommand.Path +$message = "this is help" +$measure = "Measure-RequiresRunAsAdministrator" + +Describe "Test importing customized rules with null return results" { + Context "Test Get-ScriptAnalyzer with customized rules" { + It "will not terminate the engine" { + $customizedRulePath = Get-ScriptAnalyzerRule -CustomizedRulePath $directory\samplerule\SampleRulesWithErrors.psm1 | Where-Object {$_.RuleName -eq $measure} + $customizedRulePath.Count | Should Be 1 + } + + } + + Context "Test Invoke-ScriptAnalyzer with customized rules" { + It "will not terminate the engine" { + $customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -CustomizedRulePath $directory\samplerule\SampleRulesWithErrors.psm1 | Where-Object {$_.RuleName -eq $measure} + $customizedRulePath.Count | Should Be 0 + } + } + +} + +Describe "Test importing correct customized rules" { + Context "Test Get-ScriptAnalyzer with customized rules" { + It "will show the customized rule" { + $customizedRulePath = Get-ScriptAnalyzerRule -CustomizedRulePath $directory\samplerule\samplerule.psm1 | Where-Object {$_.RuleName -eq $measure} + $customizedRulePath.Count | Should Be 1 + } + + } + + Context "Test Invoke-ScriptAnalyzer with customized rules" { + It "will show the customized 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 + } + } + +} \ No newline at end of file diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index 8c32e9b4b..46f4ad5e9 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -8,7 +8,7 @@ $dscIdentical = "PSDSCUseIdenticalParametersForDSC" Describe "Test available parameters" { $params = $sa.Parameters Context "Name parameter" { - It "has a Name parameter" { + It "has a RuleName parameter" { $params.ContainsKey("Name") | Should Be $true } @@ -34,20 +34,20 @@ Describe "Test Name parameters" { It "works with 1 name" { $rule = Get-ScriptAnalyzerRule -Name $singularNouns $rule.Count | Should Be 1 - $rule[0].Name | Should Be $singularNouns + $rule[0].RuleName | Should Be $singularNouns } It "works for DSC Rule" { $rule = Get-ScriptAnalyzerRule -Name $dscIdentical $rule.Count | Should Be 1 - $rule[0].Name | Should Be $dscIdentical + $rule[0].RuleName | Should Be $dscIdentical } It "works with 3 names" { $rules = Get-ScriptAnalyzerRule -Name $approvedVerbs, $singularNouns $rules.Count | Should Be 2 - ($rules | Where-Object {$_.Name -eq $singularNouns}).Count | Should Be 1 - ($rules | Where-Object {$_.Name -eq $approvedVerbs}).Count | Should Be 1 + ($rules | Where-Object {$_.RuleName -eq $singularNouns}).Count | Should Be 1 + ($rules | Where-Object {$_.RuleName -eq $approvedVerbs}).Count | Should Be 1 } } @@ -60,7 +60,7 @@ Describe "Test Name parameters" { It "1 incorrect and 1 correct" { $rule = Get-ScriptAnalyzerRule -Name $singularNouns, "This is a wrong name" $rule.Count | Should Be 1 - $rule[0].Name | Should Be $singularNouns + $rule[0].RuleName | Should Be $singularNouns } } } @@ -88,15 +88,15 @@ Describe "Test RuleExtension" { It "with Name of a built-in rules" { $ruleExtension = Get-ScriptAnalyzerRule -CustomizedRulePath $directory\CommunityAnalyzerRules\CommunityAnalyzerRules.psm1 -Name $singularNouns $ruleExtension.Count | Should Be 1 - $ruleExtension[0].Name | Should Be $singularNouns + $ruleExtension[0].RuleName | Should Be $singularNouns } It "with Names of built-in, DSC and non-built-in rules" { $ruleExtension = Get-ScriptAnalyzerRule -CustomizedRulePath $directory\CommunityAnalyzerRules\CommunityAnalyzerRules.psm1 -Name $singularNouns, $measureRequired, $dscIdentical $ruleExtension.Count | Should be 3 - ($ruleExtension | Where-Object {$_.Name -eq $measureRequired}).Count | Should Be 1 - ($ruleExtension | Where-Object {$_.Name -eq $singularNouns}).Count | Should Be 1 - ($ruleExtension | Where-Object {$_.Name -eq $dscIdentical}).Count | Should Be 1 + ($ruleExtension | Where-Object {$_.RuleName -eq $measureRequired}).Count | Should Be 1 + ($ruleExtension | Where-Object {$_.RuleName -eq $singularNouns}).Count | Should Be 1 + ($ruleExtension | Where-Object {$_.RuleName -eq $dscIdentical}).Count | Should Be 1 } } @@ -104,7 +104,7 @@ Describe "Test RuleExtension" { It "file cannot be found" { $wrongFile = Get-ScriptAnalyzerRule -CustomizedRulePath "This is a wrong rule" 3>&1 ($wrongFile | Select-Object -First 1) | Should Match "Cannot find rule extension 'This is a wrong rule'." - ($wrongFile | Where-Object {$_.Name -eq $singularNouns}).Count | Should Be 1 + ($wrongFile | Where-Object {$_.RuleName -eq $singularNouns}).Count | Should Be 1 } } @@ -120,4 +120,16 @@ Describe "TestSeverity" { $rules = Get-ScriptAnalyzerRule -Severity Error,Information $rules.Count | Should be 8 } +} + +Describe "TestWildCard" { + It "filters rules based on the -Name wild card input" { + $rules = Get-ScriptAnalyzerRule -Name PSDSC* + $rules.Count | Should be 4 + } + + It "filters rules based on wild card input and severity"{ + $rules = Get-ScriptAnalyzerRule -Name PSDSC* -Severity Information + $rules.Count | Should be 2 + } } \ No newline at end of file diff --git a/Tests/Engine/samplerule/SampleRulesWithErrors.psm1 b/Tests/Engine/samplerule/SampleRulesWithErrors.psm1 new file mode 100644 index 000000000..3fe13d9b8 --- /dev/null +++ b/Tests/Engine/samplerule/SampleRulesWithErrors.psm1 @@ -0,0 +1,40 @@ +#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] + $ScriptBlockAst + ) + + + $results = @() + + $results += $null + return $results + + +} +Export-ModuleMember -Function Measure* \ No newline at end of file diff --git a/Tests/Engine/samplerule/samplerule.psm1 b/Tests/Engine/samplerule/samplerule.psm1 new file mode 100644 index 000000000..cc94c6b1b --- /dev/null +++ b/Tests/Engine/samplerule/samplerule.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/Rules/AvoidUsingWMICmdlet.ps1 b/Tests/Rules/AvoidUsingWMICmdlet.ps1 new file mode 100644 index 000000000..81c03f64d --- /dev/null +++ b/Tests/Rules/AvoidUsingWMICmdlet.ps1 @@ -0,0 +1,18 @@ +#Script violates the rule because Get-CIMInstance is available on PS 3.0 and needs to use that + +#requires -version 3.0 + +function TestFunction +{ + Get-WmiObject -Class Win32_ComputerSystem + + Invoke-WMIMethod -Path Win32_Process -Name Create -ArgumentList notepad.exe + + Register-WMIEvent -Class Win32_ProcessStartTrace -SourceIdentifier "ProcessStarted" + + Set-WMIInstance -Class Win32_Environment -Argument @{Name='MyEnvVar';VariableValue='VarValue';UserName=''} +} + +TestFunction + +Remove-WmiObject -Class Win32_OperatingSystem -Verbose \ No newline at end of file diff --git a/Tests/Rules/AvoidUsingWMICmdlet.tests.ps1 b/Tests/Rules/AvoidUsingWMICmdlet.tests.ps1 new file mode 100644 index 000000000..b8c7a6e44 --- /dev/null +++ b/Tests/Rules/AvoidUsingWMICmdlet.tests.ps1 @@ -0,0 +1,24 @@ +Import-Module PSScriptAnalyzer +$WMIRuleName = "PSAvoidUsingWMICmdlet" +$violationMessage = "File 'AvoidUsingWMICmdlet.ps1' uses WMI cmdlet. For PowerShell 3.0 and above, use CIM cmdlet which perform the same tasks as the WMI cmdlets. The CIM cmdlets comply with WS-Management (WSMan) standards and with the Common Information Model (CIM) standard, which enables the cmdlets to use the same techniques to manage Windows computers and those running other operating systems." +$directory = Split-Path -Parent $MyInvocation.MyCommand.Path +$violations = Invoke-ScriptAnalyzer $directory\AvoidUsingWMICmdlet.ps1 -IncludeRule $WMIRuleName +$noViolations = Invoke-ScriptAnalyzer $directory\AvoidUsingWMICmdletNoViolations.ps1 -IncludeRule $WMIRuleName + +Describe "AvoidUsingWMICmdlet" { + Context "Script contains references to WMI cmdlets - Violation" { + It "Have 5 WMI cmdlet Violations" { + $violations.Count | Should Be 5 + } + + It "has the correct description message for WMI rule violation" { + $violations[0].Message | Should Be $violationMessage + } + } + + Context "Script contains no calls to WMI cmdlet - No violation" { + It "results in no rule violations" { + $noViolations.Count | Should Be 0 + } + } +} \ No newline at end of file diff --git a/Tests/Rules/AvoidUsingWMICmdletNoViolations.ps1 b/Tests/Rules/AvoidUsingWMICmdletNoViolations.ps1 new file mode 100644 index 000000000..51809547a --- /dev/null +++ b/Tests/Rules/AvoidUsingWMICmdletNoViolations.ps1 @@ -0,0 +1,19 @@ +# No Rule violations since this script requires PS 2.0 and Get-CIMInstance is not available for this version +# So using Get-WMIObject is OK + +#requires -Version 2.0 + +Invoke-WMIMethod -Path Win32_Process -Name Create -ArgumentList notepad.exe + +function TestFunction +{ + Get-WmiObject -Class Win32_ComputerSystem + + Register-WMIEvent -Class Win32_ProcessStartTrace -SourceIdentifier "ProcessStarted" + + Set-WMIInstance -Class Win32_Environment -Argument @{Name='MyEnvVar';VariableValue='VarValue';UserName=''} +} + +TestFunction + +Remove-WmiObject -Class Win32_OperatingSystem -Verbose \ No newline at end of file diff --git a/Tests/Rules/AvoidUsingWMIObjectCmdlet.ps1 b/Tests/Rules/AvoidUsingWMIObjectCmdlet.ps1 deleted file mode 100644 index 13f0412c9..000000000 --- a/Tests/Rules/AvoidUsingWMIObjectCmdlet.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -#Script violates the rule because Get-CIMInstance is available on PS 3.0 and needs to use that - -#requires -version 3.0 - -function TestFunction -{ - Get-WmiObject -Class Win32_ComputerSystem - -} - -TestFunction - -Remove-WmiObject -Class Win32_OperatingSystem -Verbose \ No newline at end of file diff --git a/Tests/Rules/AvoidUsingWMIObjectCmdlet.tests.ps1 b/Tests/Rules/AvoidUsingWMIObjectCmdlet.tests.ps1 deleted file mode 100644 index 0a96ad16e..000000000 --- a/Tests/Rules/AvoidUsingWMIObjectCmdlet.tests.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -Import-Module PSScriptAnalyzer -$wmiObjectRuleName = "PSAvoidUsingWMIObjectCmdlet" -$violationMessage = "File 'AvoidUsingWMIObjectCmdlet.ps1' uses WMIObject cmdlet. For PowerShell 3.0 and above, this is not recommended because the cmdlet is based on a non-standard DCOM protocol. Use CIMInstance cmdlet instead. This is CIM and WS-Man standards compliant and works in a heterogeneous environment." -$directory = Split-Path -Parent $MyInvocation.MyCommand.Path -$violations = Invoke-ScriptAnalyzer $directory\AvoidUsingWMIObjectCmdlet.ps1 | Where-Object {$_.RuleName -eq $wmiObjectRuleName} -$noViolations = Invoke-ScriptAnalyzer $directory\AvoidUsingWMIObjectCmdletNoViolations.ps1 | Where-Object {$_.RuleName -eq $wmiObjectRuleName} - -Describe "AvoidUsingWMIObjectCmdlet" { - Context "Script contains references to WMIObject cmdlets - Violation" { - It "Have 2 WMIObject cmdlet Violations" { - $violations.Count | Should Be 2 - } - - It "has the correct description message for WMIObject rule violation" { - $violations[0].Message | Should Match $violationMessage - } - } - - Context "Script contains no calls to WMIObject cmdlet - No violation" { - It "results in no rule violations" { - $noViolations.Count | Should Be 0 - } - } -} \ No newline at end of file diff --git a/Tests/Rules/AvoidUsingWMIObjectCmdletNoViolations.ps1 b/Tests/Rules/AvoidUsingWMIObjectCmdletNoViolations.ps1 deleted file mode 100644 index b5d2e6f14..000000000 --- a/Tests/Rules/AvoidUsingWMIObjectCmdletNoViolations.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -# No Rule violations since this script requires PS 2.0 and Get-CIMInstance is not available for this version -# So using Get-WMIObject is OK - -#requires -Version 2.0 - -function TestFunction -{ - Remove-WmiObject -Class Win32_ComputerSystem - -} - -TestFunction - -Get-WmiObject -Class Win32_OperatingSystem -Verbose \ No newline at end of file diff --git a/Tests/Rules/AvoidUsingWriteHost.ps1 b/Tests/Rules/AvoidUsingWriteHost.ps1 index b9ea56344..d8f73259c 100644 --- a/Tests/Rules/AvoidUsingWriteHost.ps1 +++ b/Tests/Rules/AvoidUsingWriteHost.ps1 @@ -3,4 +3,9 @@ cls Write-Host "aaa" clear [System.Console]::Write("abcdefg"); -[System.Console]::WriteLine("No console.writeline plz!"); \ No newline at end of file +[System.Console]::WriteLine("No console.writeline plz!"); + +function Test +{ + Write-Host "aaaa" +} \ No newline at end of file diff --git a/Tests/Rules/AvoidUsingWriteHost.tests.ps1 b/Tests/Rules/AvoidUsingWriteHost.tests.ps1 index f9cd926a2..2e28f5c37 100644 --- a/Tests/Rules/AvoidUsingWriteHost.tests.ps1 +++ b/Tests/Rules/AvoidUsingWriteHost.tests.ps1 @@ -8,8 +8,8 @@ $noViolations = Invoke-ScriptAnalyzer $directory\AvoidUsingWriteHostNoViolations Describe "AvoidUsingWriteHost" { Context "When there are violations" { - It "has 3 Write-Host violations" { - $violations.Count | Should Be 3 + It "has 4 Write-Host violations" { + $violations.Count | Should Be 4 } It "has the correct description message for Write-Host" { diff --git a/Tests/Rules/AvoidUsingWriteHostNoViolations.ps1 b/Tests/Rules/AvoidUsingWriteHostNoViolations.ps1 index 4dadd1faa..28b18eeae 100644 --- a/Tests/Rules/AvoidUsingWriteHostNoViolations.ps1 +++ b/Tests/Rules/AvoidUsingWriteHostNoViolations.ps1 @@ -1 +1,7 @@ -Write-Output "This is the correct way to write output" \ No newline at end of file +Write-Output "This is the correct way to write output" + +# Even if write-host is used, error should not be raised in this function +function Show-Something +{ + Write-Host "show something on screen"; +} \ No newline at end of file diff --git a/Tests/Rules/DscTestsPresent.tests.ps1 b/Tests/Rules/DscTestsPresent.tests.ps1 new file mode 100644 index 000000000..03fbeaa66 --- /dev/null +++ b/Tests/Rules/DscTestsPresent.tests.ps1 @@ -0,0 +1,70 @@ +Import-Module -Verbose PSScriptAnalyzer + +$currentPath = Split-Path -Parent $MyInvocation.MyCommand.Path +$ruleName = "PSDSCDscTestsPresent" + +Describe "DscTestsPresent rule in class based resource" { + + $testsPath = "$currentPath\DSCResources\MyDscResource\Tests" + $classResourcePath = "$currentPath\DSCResources\MyDscResource\MyDscResource.psm1" + + Context "When tests absent" { + + $violations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $classResourcePath | Where-Object {$_.RuleName -eq $ruleName} + $violationMessage = "No tests found for resource 'FileResource'" + + It "has 1 missing test violation" { + $violations.Count | Should Be 1 + } + + It "has the correct description message" { + $violations[0].Message | Should Be $violationMessage + } + } + + Context "When tests present" { + New-Item -Path $testsPath -ItemType Directory + New-Item -Path "$testsPath\FileResource_Test.psm1" -ItemType File + + $noViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $classResourcePath | Where-Object {$_.RuleName -eq $ruleName} + + It "returns no violations" { + $noViolations.Count | Should Be 0 + } + + Remove-Item -Path $testsPath -Recurse -Force + } +} + +Describe "DscTestsPresent rule in regular (non-class) based resource" { + + $testsPath = "$currentPath\Tests" + $resourcePath = "$currentPath\DSCResources\MSFT_WaitForAll\MSFT_WaitForAll.psm1" + + Context "When tests absent" { + + $violations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $resourcePath | Where-Object {$_.RuleName -eq $ruleName} + $violationMessage = "No tests found for resource 'MSFT_WaitForAll'" + + It "has 1 missing tests violation" { + $violations.Count | Should Be 1 + } + + It "has the correct description message" { + $violations[0].Message | Should Be $violationMessage + } + } + + Context "When tests present" { + New-Item -Path $testsPath -ItemType Directory + New-Item -Path "$testsPath\MSFT_WaitForAll_Test.psm1" -ItemType File + + $noViolations = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue $resourcePath | Where-Object {$_.RuleName -eq $ruleName} + + It "returns no violations" { + $noViolations.Count | Should Be 0 + } + + Remove-Item -Path $testsPath -Recurse -Force + } +} \ No newline at end of file