diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..ae004a34e --- /dev/null +++ b/.gitignore @@ -0,0 +1,208 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studo 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +##Our project binplace location +PSScriptAnalyzer/ \ No newline at end of file diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 1602860be..4bec47113 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,4 +1,4 @@ -## Unreleased (May.7, 2015) +## Released v1.0.1 (May.8, 2015) ###Features: - Integrated with waffle.io for Project Management. - Added documentation for writing script rules. @@ -12,11 +12,12 @@ - PSProvideVerboseMessage only throws warnings in non-advanced functions. - Fix the issue in importing customized rule +- Fix Function Member Ast cast error -##Relesed on Apr.24, 2015 +##Released v1.0.0 on Apr.24, 2015 ###Features: - Finalized three levels of Severity - Error/Warning/Information. diff --git a/Engine/Commands/GetScriptAnalyzerRuleCommand.cs b/Engine/Commands/GetScriptAnalyzerRuleCommand.cs index 33ec3f1a6..b749bc6e9 100644 --- a/Engine/Commands/GetScriptAnalyzerRuleCommand.cs +++ b/Engine/Commands/GetScriptAnalyzerRuleCommand.cs @@ -142,7 +142,7 @@ protected override void ProcessRecord() { if (severity != null) { - var ruleSeverity = severity.Select(item => Enum.Parse(typeof (RuleSeverity), item)); + var ruleSeverity = severity.Select(item => Enum.Parse(typeof (RuleSeverity), item, true)); rules = rules.Where(item => ruleSeverity.Contains(item.GetSeverity())).ToList(); } diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index 1baefa989..2c1e2f3f4 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -581,7 +581,7 @@ private void AnalyzeFile(string filePath) if (severity != null) { - var diagSeverity = severity.Select(item => Enum.Parse(typeof(DiagnosticSeverity), item)); + var diagSeverity = severity.Select(item => Enum.Parse(typeof(DiagnosticSeverity), item, true)); diagnostics = diagnostics.Where(item => diagSeverity.Contains(item.Severity)).ToList(); } diff --git a/Engine/PSScriptAnalyzer.psd1 b/Engine/PSScriptAnalyzer.psd1 index 2c95981f9..bfa38cf53 100644 --- a/Engine/PSScriptAnalyzer.psd1 +++ b/Engine/PSScriptAnalyzer.psd1 @@ -11,7 +11,7 @@ Author = 'Microsoft Corporation' RootModule = 'Microsoft.Windows.Powershell.ScriptAnalyzer.dll' # Version number of this module. -ModuleVersion = '1.0' +ModuleVersion = '1.0.1' # ID used to uniquely identify this module GUID = '324fc715-36bf-4aee-8e58-72e9b4a08ad9' diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index de3e0c23e..715575a6e 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -231,6 +231,12 @@ public List GetExternalRule(string[] moduleNames) string script = string.Format(CultureInfo.CurrentCulture, "Get-Module -Name '{0}' -ListAvailable", moduleName); shortModuleName = posh.AddScript(script).Invoke().First().Name; + // Invokes Update-Help for this module + // Required since when invoking Get-Help later on, the cmdlet prompts for Update-Help interactively + // By invoking Update-Help first, Get-Help will not prompt for downloading help later + script = string.Format(CultureInfo.CurrentCulture, "Update-Help -Module '{0}' -Force", shortModuleName); + posh.AddScript(script).Invoke(); + // Invokes Get-Command and Get-Help for each functions in the module. script = string.Format(CultureInfo.CurrentCulture, "Get-Command -Module '{0}'", shortModuleName); var psobjects = posh.AddScript(script).Invoke(); diff --git a/Engine/SpecialVars.cs b/Engine/SpecialVars.cs index 2c3e506b5..f616234cc 100644 --- a/Engine/SpecialVars.cs +++ b/Engine/SpecialVars.cs @@ -91,6 +91,7 @@ static SpecialVars() internal const string WhatIfPreference = "WhatIfPreference"; internal const string WarningPreference = "WarningPreference"; internal const string ConfirmPreference = "ConfirmPreference"; + internal const string ProgressPreference = "ProgressPreference"; internal static readonly string[] PreferenceVariables = new string[] { @@ -99,7 +100,8 @@ static SpecialVars() ErrorActionPreference, WhatIfPreference, WarningPreference, - ConfirmPreference, + ConfirmPreference, + ProgressPreference }; internal static readonly Type[] PreferenceVariableTypes = new Type[] @@ -109,7 +111,8 @@ static SpecialVars() /* ErrorPreference */ typeof(ActionPreference), /* WhatIfPreference */ typeof(SwitchParameter), /* WarningPreference */ typeof(ActionPreference), - /* ConfirmPreference */ typeof(ConfirmImpact), + /* ConfirmPreference */ typeof(ConfirmImpact), + /* ProgressPreference */ typeof(Enum), }; internal enum AutomaticVariable diff --git a/Engine/VariableAnalysis.cs b/Engine/VariableAnalysis.cs index 352ab40ef..5445df4bc 100644 --- a/Engine/VariableAnalysis.cs +++ b/Engine/VariableAnalysis.cs @@ -339,7 +339,8 @@ public bool IsUninitialized(VariableExpressionAst varTarget) } return analysis.DefinedBlock == null - && !SpecialVars.InitializedVariables.Contains(analysis.Name, StringComparer.OrdinalIgnoreCase) + && !(SpecialVars.InitializedVariables.Contains(analysis.Name, StringComparer.OrdinalIgnoreCase) || + SpecialVars.InitializedVariables.Contains(analysis.RealName, StringComparer.OrdinalIgnoreCase)) && !IsGlobalOrEnvironment(varTarget); } diff --git a/PowerShellBestPractices.md b/PowerShellBestPractices.md new file mode 100644 index 000000000..996e95fae --- /dev/null +++ b/PowerShellBestPractices.md @@ -0,0 +1,135 @@ +#PowerShell Best Practices + +The following guidelines come from a combined effort from both the PowerShell team and the community. We will use this guideline to define rules for PSScriptAnalyzer. Please feel free to propose additional guidelines and rules for PSScriptAnalyzer. +**Note: The hyperlink next to each guidelines will redirect to documentation page for the rule that is already implemented. + +##Cmdlet Design Rules +###Severity: Error +###Severity: Warning + - Use Only Approved Verbs [UseApprovedVerbs](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/UseApprovedVerbs.md) + - Cmdlets Names: Characters that cannot be Used [AvoidReservedCharInCmdlet](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidReservedCharInCmdlet.md) + - Parameter Names that cannot be Used [AvoidReservedParams](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidReservedParams.md) + - Support Confirmation Requests [UseShouldProcessCorrectly](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/UseShouldProcessCorrectly.md) and [UseShouldProcessForStateChangingFunctions](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/UseShouldProcessForStateChangingFunctions.md) + - Nouns should be singular [UseSingularNouns](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/UseSingularNouns.md) + - Module Manifest Fields [MissingModuleManifestField](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/MissingModuleManifestField.md) + - Version + - Author + - Description + - LicenseUri (for PowerShell Gallery) + - Must call ShouldProcess when ShouldProcess attribute is present and vice versa.[UseShouldProcessCorrectly](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/UseShouldProcessCorrectly.md) + - Switch parameters should not default to true  [AvoidDefaultTrueValueSwtichParameter](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidDefaultTrueValueSwitchParameter.md) + +###Severity: Information + +###Severity: TBD + - Support Force Parameter for Interactive Session + - If your cmdlet is used interactively, always provide a Force parameter to override the interactive actions, such as prompts or reading lines of input). This is important because it allows your cmdlet to be used in non-interactive scripts and hosts. The following methods can be implemented by an interactive host. + - Document Output Objects + - Module must be loadable + - No syntax errors + - Unresolved dependencies are an error + - Derive from the Cmdlet or PSCmdlet Classes + - Specify the Cmdlet Attribute + - Override an Input Processing Method + - Specify the OutputType Attribute + - Write Single Records to the Pipeline + - Make Cmdlets Case-Insensitive and Case-Preserving + +##Script Functions +###Severity: Error + +###Severity: Warning + - Avoid using alias [AvoidAlias](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidAlias.md) + - Avoid using deprecated WMI cmdlets [AvoidUsingWMICmdlet](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidUsingWMICmdlet.md) + - Empty catch block should not be used [AvoidEmptyCatchBlock](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidEmptyCatchBlock.md) + - Invoke existing cmdlet with correct parameters [UseCmdletCorrectly](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/UseCmdletCorrectly.md) + - Cmdlets should have ShouldProcess/ShouldContinue and Force param if certain system-modding verbs are present (Update, Set, Remove, New)[UseShouldProcessForStateChangingFunctions](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/UseShouldProcessForStateChangingFunctions.md) + - Positional parameters should be avoided [AvoidUsingPositionalParameters](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidUsingPositionalParameters.md) + - Non-global variables must be initialized. Those that are supposed to be global and not initialized must have “global:” (includes for loop initializations)[AvoidUninitializedVariable](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidUninitializedVariable.md) + - Global variables should be avoided. [AvoidGlobalVars](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidGlobalVars.md) + - Declared variables must be used in more than just their assignment. [UseDeclaredVarsMoreThanAssignments](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/UseDeclaredVarsMoreThanAssignments.md) + - No trap statments should be used [AvoidTrapStatement](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidTrapStatement.md) + - No Invoke-Expression [AvoidUsingInvokeExpression](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidUsingInvokeExpression.md) + +###Severity: Information + +###Severity: TBD + - Clear-Host should not be used + - File paths should not be used (UNC) + - Error Handling + - Use -ErrorAction Stop when calling cmdlets + - Use $ErrorActionPreference = 'Stop'/' Continue' when calling non-cmdlets + - Avoid using flags to handle errors + - Avoid using $? + - Avoid testing for a null variable as an error condition + - Copy $Error[0] to your own variable + - Avoid using pipelines in scripts + - If a return type is declared, the cmdlet must return that type. If a type is returned, a return type must be declared. + + + +##Scripting Style +###Severity: Error + +###Severity: Warning + - Don't use write-host unless writing to the host is all you want to do [AvoidUsingWriteHost](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidUsingWriteHost.md) + +###Severity: Information + - Write comment-based help [ProvideCommentHelp](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/ProvideCommentHelp.md) + - Use write-verbose to give information to someone running your script [ProvideVerboseMessage](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/ProvideVerboseMessage.md) +###Severity: TBD + - Provide usage Examples + - Use the Notes section for detail on how the tool work + - Should have help on every exported command (including parameter documentation + - Document the version of PowerShell that script was written for + - Indent your code + - Avoid backticks + + +##Script Security +###Severity: Error + - Password should be secure string [AvoidUsingPlainTextForPassword](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidUsingPlainTextForPassword.md)- Should never have both -Username and -Password parameters (should take credentials)[UsePSCredentialType](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/UsePSCredentialType.md) + - -ComputerName hardcoded should not be used (information disclosure)[AvoidUsingComputerNameHardcoded](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidUsingComputerNameHardcoded.md) + - ConvertTo-SecureString with plaintext should not be used (information disclosure) [AvoidUsingConvertToSecureStringWithPlainText](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidUsingConvertToSecureStringWithPlainText.md) + +###Severity: Warning +- Password = 'string' should not be used. (information disclosure) [AvoidUsingUsernameAndPasswordParams](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidUsingUsernameAndPasswordParams.md) +- Internal URLs should not be used (information disclosure)[AvoidUsingFilePath](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidUsingFilePath.md) + +###Severity: Information + +###Severity: TBD + - APIKey and Credentials variables that are initialized (information disclosure) + + +##DSC Related Rules +###Severity: Error + - Use standard DSC methods [UseStandardDSCFunctionsInResource](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/UseStandardDSC FunctionsInResource.md) + - Use identical mandatory parameters for all DSC methods [UseIdenticalMandatoryParametersDSC](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/UseIdenticalMandatoryParametersDSC.md) + - Use identical parameters for Set and Test DSC methods [UseIdenticalParametersDSC](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/UseIdenticalParametersDSC.md) + +###Severity: Warning + +###Severity: Information + - All of the following three rule are grouped by: [ReturnCorrectTypeDSCFunctions](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/ReturnCorrectTypeDSCFunctions.md) + - Avoid return any object from a Set-TargetResource function + - Returning a Boolean object from a Test-TargetResource function + - Returning an object from a Get-TargetResource function + - DSC resources should have DSC tests [DSCTestsPresent](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/DscTestsPresent.md) + - DSC resources should have DSC examples [DSCExamplesPresent](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/DscExamplesPresent.md) + +###Severity: TBD + - For PowerShell V4: Resource module contains .psd1 file and schema.mof for every resource + - MOF has description for each element [IssueOpened](https://github.com/PowerShell/PSScriptAnalyzer/issues/131) + - Resource module must contain .psd1 file (always) and schema.mof (for non-class resource). [IssueOpened](https://github.com/PowerShell/PSScriptAnalyzer/issues/116) + - Use ShouldProcess for a Set DSC method + - Resource module contains DscResources folder which contains the resources [IssueOpened](https://github.com/PowerShell/PSScriptAnalyzer/issues/130) + + + +###Reference: +* Cmdlet Development Guidelines from MSDN site (Cmdlet Development Guidelines) + +* The Community Book of PowerShell Practices (Compiled by Don Jones and Matt Penny and the Windows PowerShell Community) + +* [PowerShell DSC Resource Design and Testing Checklist](http://blogs.msdn.com/b/powershell/archive/2014/11/18/powershell-dsc-resource-design-and-testing-checklist.aspx) diff --git a/README.md b/README.md index b2a3b6996..25d4aea1a 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,19 @@ To confirm installation: run ```Get-ScriptAnalyzerRule``` in the PowerShell cons Building the Code ================= -Use Visual Studio to build "ScriptAnalyzer.sln". Use ~/PSScriptAnalyzer/ folder to load PSScriptAnalyzer.psd1 +Use Visual Studio to build "PSScriptAnalyzer.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** + +Build Status +============== + +| |Master Branch | +|---------|:------:|:------:|:-------:|:-------:| +|**Debug Version**|[![Build status](https://ci.appveyor.com/api/projects/status/h5mot3vqtvxw5d7l/branch/master?svg=true)](https://ci.appveyor.com/project/PowerShell/psscriptanalyzer/branch/master) | + + Running Tests ============= @@ -59,7 +68,16 @@ 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) +[![Stories in progress](https://badge.waffle.io/PowerShell/PSScriptAnalyzer.png?label=In%20Progress&title=In%20Progress)](https://waffle.io/PowerShell/PSScriptAnalyzer) + +[![Stories in ready](https://badge.waffle.io/PowerShell/PSScriptAnalyzer.png?label=ready&title=Ready)](https://waffle.io/PowerShell/PSScriptAnalyzer) + +[![Stories in backlog](https://badge.waffle.io/PowerShell/PSScriptAnalyzer.png?label=BackLog&title=BackLog)](https://waffle.io/PowerShell/PSScriptAnalyzer) + +Throughput Graph + +[![Throughput Graph](https://graphs.waffle.io/powershell/psscriptanalyzer/throughput.svg)](https://waffle.io/powershell/psscriptanalyzer/metrics) + Contributing to ScriptAnalyzer diff --git a/RuleDocumentation/AvoidUsingConvertToSecureStringWithPlainText.md b/RuleDocumentation/AvoidUsingConvertToSecureStringWithPlainText.md new file mode 100644 index 000000000..38bad47d7 --- /dev/null +++ b/RuleDocumentation/AvoidUsingConvertToSecureStringWithPlainText.md @@ -0,0 +1,30 @@ +#AvoidUsingConvertToSecureStringWithPlainText +**Severity Level: Error** + + +##Description + +Information in the script should be protected properly. Using ConvertTo-SecureString with plain text will expose secure information. + +##How to Fix + +To fix a violation of this rule, please use a standard encrypted variable to do the conversion. + +##Example + +Wrong: + +``` +$notsecure = convertto-securestring "abc" -asplaintext -force + +New-Object System.Management.Automation.PSCredential -ArgumentList "username", (ConvertTo-SecureString "notsecure" -AsPlainText -Force) + +``` + +Correct: + +``` +$secure = read-host -assecurestring +$encrypted = convertfrom-securestring -securestring $secure +convertto-securestring -string $encrypted +``` diff --git a/RuleDocumentation/AvoidUsingPositionalParameters.md b/RuleDocumentation/AvoidUsingPositionalParameters.md index 9fa5f0bbe..ecc71053c 100644 --- a/RuleDocumentation/AvoidUsingPositionalParameters.md +++ b/RuleDocumentation/AvoidUsingPositionalParameters.md @@ -1,5 +1,5 @@ #AvoidUsingPositionalParameters -**Severity Level: Info** +**Severity Level: Warning** ##Description diff --git a/RuleDocumentation/ProvideVerboseMessage.md b/RuleDocumentation/ProvideVerboseMessage.md index 99d8e73ca..343c046eb 100644 --- a/RuleDocumentation/ProvideVerboseMessage.md +++ b/RuleDocumentation/ProvideVerboseMessage.md @@ -1,5 +1,5 @@ #ProvideVerboseMessage -**Severity Level: Warning** +**Severity Level: Information** ##Description diff --git a/RuleDocumentation/ReturnCorrectTypeDSCFunctions.md b/RuleDocumentation/ReturnCorrectTypeDSCFunctions.md index 0ea962afd..a530b7c85 100644 --- a/RuleDocumentation/ReturnCorrectTypeDSCFunctions.md +++ b/RuleDocumentation/ReturnCorrectTypeDSCFunctions.md @@ -1,5 +1,5 @@ #ReturnCorrectTypeDSCFunctions -**Severity Level: Error** +**Severity Level: Information** ##Description diff --git a/RuleDocumentation/UseCmdletCorrectly.md b/RuleDocumentation/UseCmdletCorrectly.md index 8899080b5..6bc94d9de 100644 --- a/RuleDocumentation/UseCmdletCorrectly.md +++ b/RuleDocumentation/UseCmdletCorrectly.md @@ -1,5 +1,5 @@ #UseCmdletCorrectly -**Severity Level: Error** +**Severity Level: Warning** ##Description diff --git a/RuleDocumentation/UseDeclaredVarsMoreThanAssignments.md b/RuleDocumentation/UseDeclaredVarsMoreThanAssignments.md index 82d76b644..e80d276f4 100644 --- a/RuleDocumentation/UseDeclaredVarsMoreThanAssignments.md +++ b/RuleDocumentation/UseDeclaredVarsMoreThanAssignments.md @@ -1,5 +1,5 @@ #UseDeclaredVarsMoreThanAssignments -**Severity Level: Info** +**Severity Level: Warning** ##Description diff --git a/RuleDocumentation/UseShouldProcessCorrectly.md b/RuleDocumentation/UseShouldProcessCorrectly.md index 40ab429b6..336f22a02 100644 --- a/RuleDocumentation/UseShouldProcessCorrectly.md +++ b/RuleDocumentation/UseShouldProcessCorrectly.md @@ -1,5 +1,5 @@ #UseShouldProcessCorrectly -**Severity Level: Error** +**Severity Level: Warning** ##Description diff --git a/Rules/AvoidDefaultTrueValueSwitchParameter.cs b/Rules/AvoidDefaultTrueValueSwitchParameter.cs index de4a44637..2572a1fb9 100644 --- a/Rules/AvoidDefaultTrueValueSwitchParameter.cs +++ b/Rules/AvoidDefaultTrueValueSwitchParameter.cs @@ -39,7 +39,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) // Iterrates all ParamAsts and check if any are switch. foreach (ParameterAst paramAst in paramAsts) { - if (paramAst.Attributes.Any(attr => String.Equals(attr.TypeName.FullName, "switch", StringComparison.OrdinalIgnoreCase)) + if (paramAst.Attributes.Any(attr => string.Equals(attr.TypeName.GetReflectionType().FullName, "system.management.automation.switchparameter", StringComparison.OrdinalIgnoreCase)) && paramAst.DefaultValue != null && String.Equals(paramAst.DefaultValue.Extent.Text, "$true", StringComparison.OrdinalIgnoreCase)) { yield return new DiagnosticRecord( diff --git a/Rules/MissingModuleManifestField.cs b/Rules/MissingModuleManifestField.cs index 8caa7e3ff..012c18069 100644 --- a/Rules/MissingModuleManifestField.cs +++ b/Rules/MissingModuleManifestField.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Management.Automation.Language; using System.Management.Automation; using Microsoft.Windows.Powershell.ScriptAnalyzer.Generic; @@ -44,6 +45,10 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) { ps.AddCommand("Test-ModuleManifest"); ps.AddParameter("Path", fileName); + + // Suppress warnings emitted during the execution of Test-ModuleManifest + // ModuleManifest rule must catch any violations (warnings/errors) and generate DiagnosticRecord(s) + ps.AddParameter("WarningAction", ActionPreference.SilentlyContinue); ps.Invoke(); } catch { } diff --git a/Rules/UseIdenticalMandatoryParametersDSC.cs b/Rules/UseIdenticalMandatoryParametersDSC.cs index 93309e30d..b2aaa3c16 100644 --- a/Rules/UseIdenticalMandatoryParametersDSC.cs +++ b/Rules/UseIdenticalMandatoryParametersDSC.cs @@ -98,7 +98,7 @@ public IEnumerable AnalyzeDSCResource(Ast ast, string fileName { List functionsNotContainingParam = expectedTargetResourceFunctionNames.Except(mandatoryParameters[paramName]).ToList(); yield return new DiagnosticRecord(string.Format(CultureInfo.InvariantCulture, Strings.UseIdenticalMandatoryParametersDSCError, paramName, string.Join(", ", functionsNotContainingParam.ToArray())), - ast.Extent, GetName(), DiagnosticSeverity.Information, fileName); + ast.Extent, GetName(), DiagnosticSeverity.Error, fileName); } } @@ -159,7 +159,7 @@ public SourceType GetSourceType() /// public RuleSeverity GetSeverity() { - return RuleSeverity.Information; + return RuleSeverity.Error; } /// diff --git a/Rules/UseIdenticalParametersDSC.cs b/Rules/UseIdenticalParametersDSC.cs index ff33d03c7..8318c6155 100644 --- a/Rules/UseIdenticalParametersDSC.cs +++ b/Rules/UseIdenticalParametersDSC.cs @@ -67,7 +67,7 @@ public IEnumerable AnalyzeDSCResource(Ast ast, string fileName || !CompareParamAsts(paramAst, paramNames[paramAst.Name.VariablePath.UserPath])) { yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.UseIdenticalParametersDSCError), - paramAst.Extent, GetName(), DiagnosticSeverity.Information, fileName); + paramAst.Extent, GetName(), DiagnosticSeverity.Error, fileName); } } } @@ -166,7 +166,7 @@ public SourceType GetSourceType() /// public RuleSeverity GetSeverity() { - return RuleSeverity.Warning; + return RuleSeverity.Error; } /// diff --git a/Rules/UseOutputTypeCorrectly.cs b/Rules/UseOutputTypeCorrectly.cs index f79ccea15..5d04de225 100644 --- a/Rules/UseOutputTypeCorrectly.cs +++ b/Rules/UseOutputTypeCorrectly.cs @@ -110,6 +110,7 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun || String.Equals(typeof(Unreached).FullName, typeName, StringComparison.OrdinalIgnoreCase) || String.Equals(typeof(Undetermined).FullName, typeName, StringComparison.OrdinalIgnoreCase) || String.Equals(typeof(object).FullName, typeName, StringComparison.OrdinalIgnoreCase) + || String.Equals(typeof(void).FullName, typeName, StringComparison.OrdinalIgnoreCase) || outputTypes.Contains(typeName, StringComparer.OrdinalIgnoreCase)) { continue; diff --git a/Rules/UseSingularNouns.cs b/Rules/UseSingularNouns.cs index d71cce214..503e7162b 100644 --- a/Rules/UseSingularNouns.cs +++ b/Rules/UseSingularNouns.cs @@ -49,7 +49,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) { String noun = funcNamePieces[1]; var ps = System.Data.Entity.Design.PluralizationServices.PluralizationService.CreateService(CultureInfo.GetCultureInfo("en-us")); - if (ps.IsPlural(noun)) + if (!ps.IsSingular(noun)) { yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.UseSingularNounsError, funcAst.Name), funcAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); diff --git a/Rules/UseStandardDSCFunctionsInResource.cs b/Rules/UseStandardDSCFunctionsInResource.cs index 60e729cde..67e9728df 100644 --- a/Rules/UseStandardDSCFunctionsInResource.cs +++ b/Rules/UseStandardDSCFunctionsInResource.cs @@ -54,7 +54,7 @@ public IEnumerable AnalyzeDSCResource(Ast ast, string fileName if (!targetResourceFunctionNamesInAst.Contains(expectedTargetResourceFunctionName, StringComparer.CurrentCultureIgnoreCase)) { yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.UseStandardDSCFunctionsInResourceError, expectedTargetResourceFunctionName), - ast.Extent, GetName(), DiagnosticSeverity.Information, fileName); + ast.Extent, GetName(), DiagnosticSeverity.Error, fileName); } } } @@ -85,7 +85,7 @@ item is TypeDefinitionAst if (!functions.Any(function => String.Equals(resourceFunctionName, (function as FunctionMemberAst).Name))) { yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.UseStandardDSCFunctionsInClassError, resourceFunctionName), - dscClass.Extent, GetName(), DiagnosticSeverity.Information, fileName); + dscClass.Extent, GetName(), DiagnosticSeverity.Error, fileName); } } } diff --git a/CustomizedRuleDocumentation.md b/ScriptRuleDocumentation.md similarity index 100% rename from CustomizedRuleDocumentation.md rename to ScriptRuleDocumentation.md diff --git a/Tests/Engine/CustomizedRule.tests.ps1 b/Tests/Engine/CustomizedRule.tests.ps1 index 2c5777bfd..f7fac7213 100644 --- a/Tests/Engine/CustomizedRule.tests.ps1 +++ b/Tests/Engine/CustomizedRule.tests.ps1 @@ -22,6 +22,27 @@ Describe "Test importing customized rules with null return results" { } Describe "Test importing correct customized rules" { + + Context "Test Get-Help functionality in ScriptRule parsing logic" { + It "ScriptRule help section must be correctly processed when Get-Help is called for the first time" { + + # Force Get-Help to prompt for interactive input to download help using Update-Help + # By removing this registry key we force to turn on Get-Help interactivity logic during ScriptRule parsing + $null,"Wow6432Node" | ForEach-Object { + try + { + Remove-ItemProperty -Name "DisablePromptToUpdateHelp" -Path "HKLM:\SOFTWARE\$($_)\Microsoft\PowerShell" -ErrorAction Stop + } catch { + #Ignore for cases when tests are running in non-elevated more or registry key does not exist or not accessible + } + } + + $customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -CustomizedRulePath $directory\samplerule\samplerule.psm1 | Where-Object {$_.Message -eq $message} + $customizedRulePath.Count | Should Be 1 + } + + } + 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} diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index 46f4ad5e9..e299b45e2 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -113,23 +113,28 @@ Describe "Test RuleExtension" { Describe "TestSeverity" { It "filters rules based on the specified rule severity" { $rules = Get-ScriptAnalyzerRule -Severity Error - $rules.Count | Should be 4 + $rules.Count | Should be 6 } It "filters rules based on multiple severity inputs"{ $rules = Get-ScriptAnalyzerRule -Severity Error,Information - $rules.Count | Should be 8 + $rules.Count | Should be 12 + } + + It "takes lower case inputs" { + $rules = Get-ScriptAnalyzerRule -Severity error + $rules.Count | Should be 6 } } Describe "TestWildCard" { It "filters rules based on the -Name wild card input" { $rules = Get-ScriptAnalyzerRule -Name PSDSC* - $rules.Count | Should be 4 + $rules.Count | Should be 6 } It "filters rules based on wild card input and severity"{ $rules = Get-ScriptAnalyzerRule -Name PSDSC* -Severity Information - $rules.Count | Should be 2 + $rules.Count | Should be 3 } } \ No newline at end of file diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 index 9cc6ccdbd..f5303a2f4 100644 --- a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 +++ b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 @@ -141,7 +141,7 @@ Describe "Test IncludeRule" { it "includes 2 wildcardrules" { $includeWildcard = Invoke-ScriptAnalyzer $directory\..\Rules\BadCmdlet.ps1 -IncludeRule $avoidRules, $useRules - $includeWildcard.Count | Should be 7 + $includeWildcard.Count | Should be 9 } } } @@ -169,6 +169,11 @@ Describe "Test Severity" { $errors = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -Severity Information, Warning $errors.Count | Should Be 2 } + + It "works with lowercase argument"{ + $errors = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -Severity information, warning + $errors.Count | Should Be 2 + } } Context "When used incorrectly" { diff --git a/Tests/Rules/AvoidDefaultTrueValueSwitchParameter.ps1 b/Tests/Rules/AvoidDefaultTrueValueSwitchParameter.ps1 index 49881156c..a9190ed48 100644 --- a/Tests/Rules/AvoidDefaultTrueValueSwitchParameter.ps1 +++ b/Tests/Rules/AvoidDefaultTrueValueSwitchParameter.ps1 @@ -13,7 +13,11 @@ # Param2 help description [switch] - $switch=$true + $switch=$true, + + # Param3 help description + [System.Management.Automation.SwitchParameter] + $switch2 = $true ) Begin diff --git a/Tests/Rules/AvoidDefaultTrueValueSwitchParameter.tests.ps1 b/Tests/Rules/AvoidDefaultTrueValueSwitchParameter.tests.ps1 index 469dc6a66..a3fda1d5d 100644 --- a/Tests/Rules/AvoidDefaultTrueValueSwitchParameter.tests.ps1 +++ b/Tests/Rules/AvoidDefaultTrueValueSwitchParameter.tests.ps1 @@ -7,8 +7,8 @@ $noViolations = Invoke-ScriptAnalyzer $directory\AvoidDefaultTrueValueSwitchPara Describe "AvoidDefaultTrueValueSwitchParameter" { Context "When there are violations" { - It "has 1 avoid using switch parameter default to true violation" { - $violations.Count | Should Be 1 + It "has 2 avoid using switch parameter default to true violation" { + $violations.Count | Should Be 2 } It "has the correct description message" { diff --git a/Tests/Rules/AvoidGlobalOrUnitializedVarsNoViolations.ps1 b/Tests/Rules/AvoidGlobalOrUnitializedVarsNoViolations.ps1 index b5206cd68..432dd1a91 100644 --- a/Tests/Rules/AvoidGlobalOrUnitializedVarsNoViolations.ps1 +++ b/Tests/Rules/AvoidGlobalOrUnitializedVarsNoViolations.ps1 @@ -1,30 +1,41 @@ -function Test { - $initialized = "Initialized" - $noglobal = "local" - $env:ShouldNotRaiseError -} - -$a = 3; - -if ($true) { - $a = 4; - $c = 3; -} else { - $b = 5; - $c = 4; -} - -$b = 6; -$a; -$b; - -stop-process 12,23 -ErrorVariable ev -ErrorAction SilentlyContinue -if($null -ne $ev) -{ - Write-host $ev[0] - # no error should be raised here - Invoke-Command {$b} -} - -get-process notepad | tee-object -variable proc -$proc[0] \ No newline at end of file +function Test { + $initialized = "Initialized" + $noglobal = "local" + $env:ShouldNotRaiseError +} + +$a = 3; + +if ($true) { + $a = 4; + $c = 3; +} else { + $b = 5; + $c = 4; +} + +$b = 6; +$a; +$b; + +stop-process 12,23 -ErrorVariable ev -ErrorAction SilentlyContinue +if($null -ne $ev) +{ + Write-host $ev[0] + # no error should be raised here + Invoke-Command {$b} +} + +get-process notepad | tee-object -variable proc +$proc[0] + +function Test-PreferenceVariable +{ + + if (-not $PSBoundParameters.ContainsKey('Verbose')) { + $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') -as + [System.Management.Automation.ActionPreference] + } + + $VerbosePreference + } \ No newline at end of file diff --git a/Tests/Rules/AvoidUsingUninitializedVariable.Tests.ps1 b/Tests/Rules/AvoidUsingUninitializedVariable.Tests.ps1 new file mode 100644 index 000000000..3fb75b360 --- /dev/null +++ b/Tests/Rules/AvoidUsingUninitializedVariable.Tests.ps1 @@ -0,0 +1,24 @@ +Import-Module PSScriptAnalyzer +$AvoidUninitializedVariable = "PSAvoidUninitializedVariable" +$violationMessage = "Variable 'MyProgressPreference' is not initialized. Non-global variables must be initialized. To fix a violation of this rule, please initialize non-global variables." +$directory = Split-Path -Parent $MyInvocation.MyCommand.Path +$violations = Invoke-ScriptAnalyzer $directory\AvoidUsingUninitializedVariable.ps1 -IncludeRule $AvoidUninitializedVariable +$noViolations = Invoke-ScriptAnalyzer $directory\AvoidUsingUninitializedVariableNoViolations.ps1 -IncludeRule $AvoidUninitializedVariable + +Describe "AvoidUsingUninitializedVariable" { + Context "Script uses uninitialized variables - Violation" { + It "Have 3 rule violations" { + $violations.Count | Should Be 3 + } + + It "has the correct description message for UninitializedVariable rule violation" { + $violations[0].Message | Should Be $violationMessage + } + } + + Context "Script uses initialized variables - No violation" { + It "results in no rule violations" { + $noViolations.Count | Should Be 0 + } + } +} \ No newline at end of file diff --git a/Tests/Rules/AvoidUsingUninitializedVariable.ps1 b/Tests/Rules/AvoidUsingUninitializedVariable.ps1 new file mode 100644 index 000000000..980a01d70 --- /dev/null +++ b/Tests/Rules/AvoidUsingUninitializedVariable.ps1 @@ -0,0 +1,10 @@ +# Script has uninitialized variables +# Must result in AvoidUsingUninitializedVariablerule violations along with other violations + +function Test-MyPreference +{ + Write-Verbose $MyProgressPreference + Write-Verbose $MyVerbosePreference +} + +Write-Verbose $MyProgressPreference \ No newline at end of file diff --git a/Tests/Rules/AvoidUsingUninitializedVariableNoViolations.ps1 b/Tests/Rules/AvoidUsingUninitializedVariableNoViolations.ps1 new file mode 100644 index 000000000..046126997 --- /dev/null +++ b/Tests/Rules/AvoidUsingUninitializedVariableNoViolations.ps1 @@ -0,0 +1,11 @@ +# Script uses built-in preference variables +# Must not result in AvoidUsingUninitializedVariablerule violations +# However there are other violations in this script - Write-Verbose is not using positional parameters + +function Test-Preference +{ + Write-Verbose $ProgressPreference + Write-Verbose $VerbosePreference +} + +Write-Verbose $ProgressPreference \ No newline at end of file diff --git a/Tests/Rules/GoodCmdlet.ps1 b/Tests/Rules/GoodCmdlet.ps1 index b10b830a4..24106a3f5 100644 --- a/Tests/Rules/GoodCmdlet.ps1 +++ b/Tests/Rules/GoodCmdlet.ps1 @@ -91,6 +91,7 @@ function Get-File { if ($pscmdlet.ShouldContinue("Yes", "No")) { } + [System.Void] $Param3 } } diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..28eb6cfa6 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,50 @@ +# WS2012R2 image with April WMF5.0 +os: unstable + +# clone directory +clone_folder: c:\projects\psscriptanalyzer + +# Install Pester +install: + - cinst -y pester + +# Build PSScriptAnalyzer using msbuild +build: true + +# branches to build +branches: + # whitelist + only: + - master + - bugfixes + - development + +# Run Pester tests and store the results +test_script: + - SET PATH=c:\Program Files\WindowsPowerShell\Modules\;%PATH%; + - ps: | + copy "C:\projects\psscriptanalyzer\PSScriptAnalyzer" "$Env:ProgramFiles\WindowsPowerShell\Modules\" -Recurse -Force + $engineTestResultsFile = ".\EngineTestResults.xml" + $ruleTestResultsFile = ".\RuleTestResults.xml" + $engineTestResults = Invoke-Pester -Script "C:\projects\psscriptanalyzer\Tests\Engine" -OutputFormat NUnitXml -OutputFile $engineTestResultsFile -PassThru + (New-Object 'System.Net.WebClient').UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $engineTestResultsFile)) + if ($engineTestResults.FailedCount -gt 0) { + throw "$($engineTestResults.FailedCount) tests failed." + } + $ruleTestResults = Invoke-Pester -Script "C:\projects\psscriptanalyzer\Tests\Rules" -OutputFormat NUnitXml -OutputFile $ruleTestResultsFile -PassThru + (New-Object 'System.Net.WebClient').UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $ruleTestResultsFile)) + if ($ruleTestResults.FailedCount -gt 0) { + throw "$($ruleTestResults.FailedCount) tests failed." + } + +# Upload the project along with TestResults as a zip archive +on_finish: + - ps: | + $stagingDirectory = (Resolve-Path ..).Path + $zipFile = Join-Path $stagingDirectory "$(Split-Path $pwd -Leaf).zip" + Add-Type -assemblyname System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::CreateFromDirectory($pwd, $zipFile) + @( + # You can add other artifacts here + (ls $zipFile) + ) | % { Push-AppveyorArtifact $_.FullName } diff --git a/build.cmd b/build.cmd new file mode 100644 index 000000000..e51842115 --- /dev/null +++ b/build.cmd @@ -0,0 +1,15 @@ +@echo off +setlocal +if "%VS120COMNTOOLS%"=="" GOTO NOTOOLS +call "%VS120COMNTOOLS%\VsDevCmd.bat" +msbuild .\PSScriptAnalyzer.sln /p:Configuration=Debug /l:FileLogger,Microsoft.Build.Engine;logfile=PSScriptAnalyzer_Build.log;append=true +if NOT [%ERRORLEVEL%]==[0] pause + +GOTO END + +:NOTOOLS +echo The Visual Studio 2013 tools are not installed +pause + +:END +endlocal \ No newline at end of file