diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index d89413948..ccf5d5992 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -31,14 +31,19 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands /// /// InvokeScriptAnalyzerCommand: Cmdlet to statically check PowerShell scripts. /// - [Cmdlet(VerbsLifecycle.Invoke, "ScriptAnalyzer", HelpUri = "http://go.microsoft.com/fwlink/?LinkId=525914")] + [Cmdlet(VerbsLifecycle.Invoke, + "ScriptAnalyzer", + DefaultParameterSetName="File", + HelpUri = "http://go.microsoft.com/fwlink/?LinkId=525914")] public class InvokeScriptAnalyzerCommand : PSCmdlet, IOutputWriter { #region Parameters /// /// Path: The path to the file or folder to invoke PSScriptAnalyzer on. /// - [Parameter(Position = 0, Mandatory = true)] + [Parameter(Position = 0, + ParameterSetName = "File", + Mandatory = true)] [ValidateNotNull] [Alias("PSPath")] public string Path @@ -48,6 +53,20 @@ public string Path } private string path; + /// + /// ScriptDefinition: a script definition in the form of a string to run rules on. + /// + [Parameter(Position = 0, + ParameterSetName = "ScriptDefinition", + Mandatory = true)] + [ValidateNotNull] + public string ScriptDefinition + { + get { return scriptDefinition; } + set { scriptDefinition = value; } + } + private string scriptDefinition; + /// /// CustomRulePath: The path to the file containing custom rules to run. /// @@ -160,11 +179,18 @@ protected override void BeginProcessing() /// protected override void ProcessRecord() { - // throws Item Not Found Exception - Collection paths = this.SessionState.Path.GetResolvedPSPathFromPSPath(path); - foreach (PathInfo p in paths) + if (String.Equals(this.ParameterSetName, "File", StringComparison.OrdinalIgnoreCase)) { - ProcessPath(this.SessionState.Path.GetUnresolvedProviderPathFromPSPath(p.Path)); + // throws Item Not Found Exception + Collection paths = this.SessionState.Path.GetResolvedPSPathFromPSPath(path); + foreach (PathInfo p in paths) + { + ProcessPathOrScriptDefinition(this.SessionState.Path.GetUnresolvedProviderPathFromPSPath(p.Path)); + } + } + else if (String.Equals(this.ParameterSetName, "ScriptDefinition", StringComparison.OrdinalIgnoreCase)) + { + ProcessPathOrScriptDefinition(scriptDefinition); } } @@ -172,10 +198,18 @@ protected override void ProcessRecord() #region Methods - private void ProcessPath(string path) + private void ProcessPathOrScriptDefinition(string pathOrScriptDefinition) { - IEnumerable diagnosticsList = - ScriptAnalyzer.Instance.AnalyzePath(path, this.recurse); + IEnumerable diagnosticsList = Enumerable.Empty(); + + if (String.Equals(this.ParameterSetName, "File", StringComparison.OrdinalIgnoreCase)) + { + diagnosticsList = ScriptAnalyzer.Instance.AnalyzePath(pathOrScriptDefinition); + } + else if (String.Equals(this.ParameterSetName, "ScriptDefinition", StringComparison.OrdinalIgnoreCase)) + { + diagnosticsList = ScriptAnalyzer.Instance.AnalyzeScriptDefinition(pathOrScriptDefinition); + } //Output through loggers foreach (ILogger logger in ScriptAnalyzer.Instance.Loggers) diff --git a/Engine/Generic/DiagnosticRecord.cs b/Engine/Generic/DiagnosticRecord.cs index d62a5217c..5425d9d9a 100644 --- a/Engine/Generic/DiagnosticRecord.cs +++ b/Engine/Generic/DiagnosticRecord.cs @@ -70,7 +70,16 @@ public string ScriptName { get { return scriptName; } //Trim down to the leaf element of the filePath and pass it to Diagnostic Record - set { scriptName = System.IO.Path.GetFileName(value); } + set { + if (!string.IsNullOrWhiteSpace(value)) + { + scriptName = System.IO.Path.GetFileName(value); + } + else + { + scriptName = string.Empty; + } + } } /// diff --git a/Engine/Generic/RuleSuppression.cs b/Engine/Generic/RuleSuppression.cs index a90f1b88c..5ce24af67 100644 --- a/Engine/Generic/RuleSuppression.cs +++ b/Engine/Generic/RuleSuppression.cs @@ -294,8 +294,15 @@ public RuleSuppression(AttributeAst attrAst, int start, int end) if (!String.IsNullOrWhiteSpace(Error)) { - Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormat, StartAttributeLine, - System.IO.Path.GetFileName(attrAst.Extent.File), Error); + if (String.IsNullOrWhiteSpace(attrAst.Extent.File)) + { + Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormatScriptDefinition, StartAttributeLine, Error); + } + else + { + Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormat, StartAttributeLine, + System.IO.Path.GetFileName(attrAst.Extent.File), Error); + } } } @@ -372,8 +379,17 @@ public static List GetSuppressions(IEnumerable at { if (targetAsts.Count() == 0) { - ruleSupp.Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormat, ruleSupp.StartAttributeLine, - System.IO.Path.GetFileName(scopeAst.Extent.File), String.Format(Strings.TargetCannotBeFoundError, ruleSupp.Target, ruleSupp.Scope)); + if (String.IsNullOrWhiteSpace(scopeAst.Extent.File)) + { + ruleSupp.Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormatScriptDefinition, ruleSupp.StartAttributeLine, + String.Format(Strings.TargetCannotBeFoundError, ruleSupp.Target, ruleSupp.Scope)); + } + else + { + ruleSupp.Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormat, ruleSupp.StartAttributeLine, + System.IO.Path.GetFileName(scopeAst.Extent.File), String.Format(Strings.TargetCannotBeFoundError, ruleSupp.Target, ruleSupp.Scope)); + } + result.Add(ruleSupp); continue; } diff --git a/Engine/Helper.cs b/Engine/Helper.cs index 7297bc4b0..bc2fef74c 100644 --- a/Engine/Helper.cs +++ b/Engine/Helper.cs @@ -956,8 +956,18 @@ public Tuple, List> SuppressRule(string // If we cannot found any error but the rulesuppression has a rulesuppressionid then it must be used wrongly if (!String.IsNullOrWhiteSpace(ruleSuppression.RuleSuppressionID) && suppressionCount == 0) { - ruleSuppression.Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormat, ruleSuppression.StartAttributeLine, - System.IO.Path.GetFileName(diagnostics.First().Extent.File), String.Format(Strings.RuleSuppressionIDError, ruleSuppression.RuleSuppressionID)); + // checks whether are given a string or a file path + if (String.IsNullOrWhiteSpace(diagnostics.First().Extent.File)) + { + ruleSuppression.Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormatScriptDefinition, ruleSuppression.StartAttributeLine, + String.Format(Strings.RuleSuppressionIDError, ruleSuppression.RuleSuppressionID)); + } + else + { + ruleSuppression.Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormat, ruleSuppression.StartAttributeLine, + System.IO.Path.GetFileName(diagnostics.First().Extent.File), String.Format(Strings.RuleSuppressionIDError, ruleSuppression.RuleSuppressionID)); + } + this.outputWriter.WriteError(new ErrorRecord(new ArgumentException(ruleSuppression.Error), ruleSuppression.Error, ErrorCategory.InvalidArgument, ruleSuppression)); } } diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index d82ebd1d7..054db6758 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -883,7 +883,7 @@ public Dictionary> CheckRuleExtension(string[] path, PathIn } #endregion - + /// /// Analyzes a script file or a directory containing script files. @@ -924,6 +924,49 @@ public IEnumerable AnalyzePath(string path, bool searchRecursi } } + /// + /// Analyzes a script definition in the form of a string input + /// + /// The script to be analyzed + /// + public IEnumerable AnalyzeScriptDefinition(string scriptDefinition) + { + ScriptBlockAst scriptAst = null; + Token[] scriptTokens = null; + ParseError[] errors = null; + + this.outputWriter.WriteVerbose(string.Format(CultureInfo.CurrentCulture, Strings.VerboseScriptDefinitionMessage)); + + try + { + scriptAst = Parser.ParseInput(scriptDefinition, out scriptTokens, out errors); + } + catch (Exception e) + { + this.outputWriter.WriteWarning(e.ToString()); + return null; + } + + if (errors != null && errors.Length > 0) + { + foreach (ParseError error in errors) + { + string parseErrorMessage = String.Format(CultureInfo.CurrentCulture, Strings.ParseErrorFormatForScriptDefinition, error.Message.TrimEnd('.'), error.Extent.StartLineNumber, error.Extent.StartColumnNumber); + this.outputWriter.WriteError(new ErrorRecord(new ParseException(parseErrorMessage), parseErrorMessage, ErrorCategory.ParserError, error.ErrorId)); + } + } + + if (errors != null && errors.Length > 10) + { + string manyParseErrorMessage = String.Format(CultureInfo.CurrentCulture, Strings.ParserErrorMessageForScriptDefinition); + this.outputWriter.WriteError(new ErrorRecord(new ParseException(manyParseErrorMessage), manyParseErrorMessage, ErrorCategory.ParserError, scriptDefinition)); + + return new List(); + } + + return this.AnalyzeSyntaxTree(scriptAst, scriptTokens, String.Empty); + } + private void BuildScriptPathList( string path, bool searchRecursively, @@ -1038,7 +1081,9 @@ private IEnumerable AnalyzeFile(string filePath) /// /// The ScriptBlockAst from the parsed script. /// The tokens found in the script. - /// The path to the file that was parsed. + /// The path to the file that was parsed. + /// If AnalyzeSyntaxTree is called from an ast that we get from ParseInput, then this field will be String.Empty + /// /// An enumeration of DiagnosticRecords that were found by rules. public IEnumerable AnalyzeSyntaxTree( ScriptBlockAst scriptAst, @@ -1052,8 +1097,12 @@ public IEnumerable AnalyzeSyntaxTree( // Use a List of KVP rather than dictionary, since for a script containing inline functions with same signature, keys clash List> cmdInfoTable = new List>(); + bool filePathIsNullOrWhiteSpace = String.IsNullOrWhiteSpace(filePath); + filePath = filePathIsNullOrWhiteSpace ? String.Empty : filePath; - bool helpFile = (scriptAst == null) && Helper.Instance.IsHelpFile(filePath); + // check whether the script we are analyzing is a help file or not. + // this step is not applicable for scriptdefinition, whose filepath is null + bool helpFile = (scriptAst == null) && (!filePathIsNullOrWhiteSpace) && Helper.Instance.IsHelpFile(filePath); if (!helpFile) { @@ -1083,7 +1132,7 @@ public IEnumerable AnalyzeSyntaxTree( #region Run ScriptRules //Trim down to the leaf element of the filePath and pass it to Diagnostic Record - string fileName = System.IO.Path.GetFileName(filePath); + string fileName = filePathIsNullOrWhiteSpace ? String.Empty : System.IO.Path.GetFileName(filePath); if (this.ScriptRules != null) { @@ -1285,7 +1334,7 @@ public IEnumerable AnalyzeSyntaxTree( } // Check if the supplied artifact is indeed part of the DSC resource - if (Helper.Instance.IsDscResourceModule(filePath)) + if (!filePathIsNullOrWhiteSpace && Helper.Instance.IsDscResourceModule(filePath)) { // Run all DSC Rules foreach (IDSCResourceRule dscResourceRule in this.DSCResourceRules) diff --git a/Engine/Strings.Designer.cs b/Engine/Strings.Designer.cs index cc892c6c8..619490de5 100644 --- a/Engine/Strings.Designer.cs +++ b/Engine/Strings.Designer.cs @@ -177,6 +177,15 @@ internal static string NullRuleNameError { } } + /// + /// Looks up a localized string similar to Parse error in script definition: {0} at line {1} column {2}.. + /// + internal static string ParseErrorFormatForScriptDefinition { + get { + return ResourceManager.GetString("ParseErrorFormatForScriptDefinition", resourceCulture); + } + } + /// /// Looks up a localized string similar to Parse error in file {0}: {1} at line {2} column {3}.. /// @@ -195,6 +204,15 @@ internal static string ParserErrorMessage { } } + /// + /// Looks up a localized string similar to There are too many parser errors in the script definition. Please correct them before running ScriptAnalyzer.. + /// + internal static string ParserErrorMessageForScriptDefinition { + get { + return ResourceManager.GetString("ParserErrorMessageForScriptDefinition", resourceCulture); + } + } + /// /// Looks up a localized string similar to RULE_ERROR. /// @@ -222,6 +240,15 @@ internal static string RuleSuppressionErrorFormat { } } + /// + /// Looks up a localized string similar to Suppression Message Attribute error at line {0} in script definition : {1}. + /// + internal static string RuleSuppressionErrorFormatScriptDefinition { + get { + return ResourceManager.GetString("RuleSuppressionErrorFormatScriptDefinition", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot find any DiagnosticRecord with the Rule Suppression ID {0}.. /// @@ -285,6 +312,15 @@ internal static string VerboseRunningMessage { } } + /// + /// Looks up a localized string similar to Analyzing Script Definition.. + /// + internal static string VerboseScriptDefinitionMessage { + get { + return ResourceManager.GetString("VerboseScriptDefinitionMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} is not a valid key in the profile hashtable: line {0} column {1} in file {2}. /// diff --git a/Engine/Strings.resx b/Engine/Strings.resx index 87daf264d..9de1cef68 100644 --- a/Engine/Strings.resx +++ b/Engine/Strings.resx @@ -204,4 +204,16 @@ Profile file '{0}' is invalid because it does not contain a hashtable. + + Parse error in script definition: {0} at line {1} column {2}. + + + There are too many parser errors in the script definition. Please correct them before running ScriptAnalyzer. + + + Suppression Message Attribute error at line {0} in script definition : {1} + + + Analyzing Script Definition. + \ No newline at end of file diff --git a/PSScriptAnalyzer.sln b/PSScriptAnalyzer.sln index 45cae24a7..a9dcdc36e 100644 --- a/PSScriptAnalyzer.sln +++ b/PSScriptAnalyzer.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2013 -VisualStudioVersion = 12.0.21005.1 +VisualStudioVersion = 12.0.31101.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScriptAnalyzerEngine", "Engine\ScriptAnalyzerEngine.csproj", "{F4BDE3D0-3EEF-4157-8A3E-722DF7ADEF60}" EndProject diff --git a/Rules/AvoidDefaultTrueValueSwitchParameter.cs b/Rules/AvoidDefaultTrueValueSwitchParameter.cs index 932da65e6..ee5c60a44 100644 --- a/Rules/AvoidDefaultTrueValueSwitchParameter.cs +++ b/Rules/AvoidDefaultTrueValueSwitchParameter.cs @@ -42,9 +42,18 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) if (paramAst.Attributes.Any(attr => attr.TypeName.GetReflectionType() == typeof(System.Management.Automation.SwitchParameter)) && paramAst.DefaultValue != null && String.Equals(paramAst.DefaultValue.Extent.Text, "$true", StringComparison.OrdinalIgnoreCase)) { - yield return new DiagnosticRecord( - String.Format(CultureInfo.CurrentCulture, Strings.AvoidDefaultValueSwitchParameterError, System.IO.Path.GetFileName(fileName)), - paramAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); + if (String.IsNullOrWhiteSpace(fileName)) + { + yield return new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, Strings.AvoidDefaultValueSwitchParameterErrorScriptDefinition), + paramAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); + } + else + { + yield return new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, Strings.AvoidDefaultValueSwitchParameterError, System.IO.Path.GetFileName(fileName)), + paramAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); + } } } } diff --git a/Rules/AvoidShouldContinueWithoutForce.cs b/Rules/AvoidShouldContinueWithoutForce.cs index 0222200e9..42996c990 100644 --- a/Rules/AvoidShouldContinueWithoutForce.cs +++ b/Rules/AvoidShouldContinueWithoutForce.cs @@ -68,9 +68,18 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) if (String.Equals(typeAst.VariablePath.UserPath, "pscmdlet", StringComparison.OrdinalIgnoreCase) && (String.Equals(imeAst.Member.Extent.Text, "shouldcontinue", StringComparison.OrdinalIgnoreCase))) { - yield return new DiagnosticRecord( - String.Format(CultureInfo.CurrentCulture, Strings.AvoidShouldContinueWithoutForceError, funcAst.Name, System.IO.Path.GetFileName(fileName)), - imeAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); + if (String.IsNullOrWhiteSpace(fileName)) + { + yield return new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, Strings.AvoidShouldContinueWithoutForceErrorScriptDefinition, funcAst.Name), + imeAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); + } + else + { + yield return new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, Strings.AvoidShouldContinueWithoutForceError, funcAst.Name, + System.IO.Path.GetFileName(fileName)), imeAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); + } } } } diff --git a/Rules/AvoidUsingConvertToSecureStringWithPlainText.cs b/Rules/AvoidUsingConvertToSecureStringWithPlainText.cs index d9105235a..0105680c8 100644 --- a/Rules/AvoidUsingConvertToSecureStringWithPlainText.cs +++ b/Rules/AvoidUsingConvertToSecureStringWithPlainText.cs @@ -60,9 +60,16 @@ public override bool ParameterCondition(CommandAst CmdAst, CommandElementAst CeA /// /// /// - public override string GetError(string FileName, CommandAst CmdAst) + public override string GetError(string fileName, CommandAst cmdAst) { - return String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingConvertToSecureStringWithPlainTextError, System.IO.Path.GetFileName(FileName)); + if (String.IsNullOrWhiteSpace(fileName)) + { + return String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingConvertToSecureStringWithPlainTextErrorScriptDefinition); + } + else + { + return String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingConvertToSecureStringWithPlainTextError, System.IO.Path.GetFileName(fileName)); + } } /// diff --git a/Rules/AvoidUsingWMICmdlet.cs b/Rules/AvoidUsingWMICmdlet.cs index d5104a298..4ac3b4bea 100644 --- a/Rules/AvoidUsingWMICmdlet.cs +++ b/Rules/AvoidUsingWMICmdlet.cs @@ -50,8 +50,16 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) || String.Equals(cmdAst.GetCommandName(), "set-wmiinstance", StringComparison.OrdinalIgnoreCase)) ) { - yield return new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWMICmdletError, System.IO.Path.GetFileName(fileName)), - cmdAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); + if (String.IsNullOrWhiteSpace(fileName)) + { + yield return new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWMICmdletErrorScriptDefinition), + cmdAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); + } + else + { + yield return new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWMICmdletError, + System.IO.Path.GetFileName(fileName)), cmdAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName); + } } } } diff --git a/Rules/AvoidUsingWriteHost.cs b/Rules/AvoidUsingWriteHost.cs index e2339cb75..bd5cd3a6a 100644 --- a/Rules/AvoidUsingWriteHost.cs +++ b/Rules/AvoidUsingWriteHost.cs @@ -78,8 +78,16 @@ public override AstVisitAction VisitCommand(CommandAst cmdAst) if (cmdAst.GetCommandName() != null && String.Equals(cmdAst.GetCommandName(), "write-host", StringComparison.OrdinalIgnoreCase)) { - records.Add(new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWriteHostError, System.IO.Path.GetFileName(fileName)), - cmdAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName)); + if (String.IsNullOrWhiteSpace(fileName)) + { + records.Add(new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWriteHostErrorScriptDefinition), + cmdAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName)); + } + else + { + records.Add(new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWriteHostError, + System.IO.Path.GetFileName(fileName)), cmdAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName)); + } } return AstVisitAction.Continue; @@ -102,7 +110,8 @@ public override AstVisitAction VisitInvokeMemberExpression(InvokeMemberExpressio 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), + records.Add(new DiagnosticRecord(String.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingConsoleWriteError, + String.IsNullOrWhiteSpace(fileName) ? Strings.ScriptDefinitionName : System.IO.Path.GetFileName(fileName), imeAst.Member.Extent.Text), imeAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName)); } diff --git a/Rules/DscExamplesPresent.cs b/Rules/DscExamplesPresent.cs index e0f9b53a1..a659c9cb6 100644 --- a/Rules/DscExamplesPresent.cs +++ b/Rules/DscExamplesPresent.cs @@ -39,6 +39,12 @@ public class DscExamplesPresent : IDSCResourceRule /// The results of the analysis public IEnumerable AnalyzeDSCResource(Ast ast, string fileName) { + // we are given a script definition, do not analyze + if (String.IsNullOrWhiteSpace(fileName)) + { + yield break; + } + String fileNameOnly = Path.GetFileName(fileName); String resourceName = Path.GetFileNameWithoutExtension(fileNameOnly); String examplesQuery = String.Format("*{0}*", resourceName); @@ -72,6 +78,12 @@ public IEnumerable AnalyzeDSCResource(Ast ast, string fileName /// public IEnumerable AnalyzeDSCClass(Ast ast, string fileName) { + // we are given a script definition, do not analyze + if (String.IsNullOrWhiteSpace(fileName)) + { + yield break; + } + String resourceName = null; IEnumerable dscClasses = ast.FindAll(item => diff --git a/Rules/DscTestsPresent.cs b/Rules/DscTestsPresent.cs index 9a25ece36..7b355cb3b 100644 --- a/Rules/DscTestsPresent.cs +++ b/Rules/DscTestsPresent.cs @@ -39,6 +39,12 @@ public class DscTestsPresent : IDSCResourceRule /// The results of the analysis public IEnumerable AnalyzeDSCResource(Ast ast, string fileName) { + // we are given a script definition, do not analyze + if (String.IsNullOrWhiteSpace(fileName)) + { + yield break; + } + String fileNameOnly = Path.GetFileName(fileName); String resourceName = Path.GetFileNameWithoutExtension(fileNameOnly); String testsQuery = String.Format("*{0}*", resourceName); @@ -72,6 +78,12 @@ public IEnumerable AnalyzeDSCResource(Ast ast, string fileName /// public IEnumerable AnalyzeDSCClass(Ast ast, string fileName) { + // we are given a script definition, do not analyze + if (String.IsNullOrWhiteSpace(fileName)) + { + yield break; + } + String resourceName = null; IEnumerable dscClasses = ast.FindAll(item => diff --git a/Rules/ProvideDefaultParameterValue.cs b/Rules/ProvideDefaultParameterValue.cs index 3d5c24023..c67c14133 100644 --- a/Rules/ProvideDefaultParameterValue.cs +++ b/Rules/ProvideDefaultParameterValue.cs @@ -37,7 +37,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) IEnumerable functionAsts = ast.FindAll(testAst => testAst is FunctionDefinitionAst, true); // Checks whether this is a dsc resource file (we don't raise this rule for get, set and test-target resource - bool isDscResourceFile = Helper.Instance.IsDscResourceModule(fileName); + bool isDscResourceFile = !String.IsNullOrWhiteSpace(fileName) && Helper.Instance.IsDscResourceModule(fileName); List targetResourcesFunctions = new List(new string[] { "get-targetresource", "set-targetresource", "test-targetresource" }); diff --git a/Rules/ScriptAnalyzerBuiltinRules.csproj b/Rules/ScriptAnalyzerBuiltinRules.csproj index 9328f21d1..6f8058d4b 100644 --- a/Rules/ScriptAnalyzerBuiltinRules.csproj +++ b/Rules/ScriptAnalyzerBuiltinRules.csproj @@ -70,17 +70,17 @@ + + True + True + Strings.resx + - - True - True - Strings.resx - @@ -94,6 +94,12 @@ + + + {f4bde3d0-3eef-4157-8a3e-722df7adef60} + ScriptAnalyzerEngine + + Designer @@ -101,12 +107,6 @@ Strings.Designer.cs - - - {f4bde3d0-3eef-4157-8a3e-722df7adef60} - ScriptAnalyzerEngine - - diff --git a/Rules/Strings.Designer.cs b/Rules/Strings.Designer.cs index 99e681f14..9128ebedd 100644 --- a/Rules/Strings.Designer.cs +++ b/Rules/Strings.Designer.cs @@ -123,6 +123,15 @@ internal static string AvoidDefaultValueSwitchParameterError { } } + /// + /// Looks up a localized string similar to Script definition has a switch parameter default to true.. + /// + internal static string AvoidDefaultValueSwitchParameterErrorScriptDefinition { + get { + return ResourceManager.GetString("AvoidDefaultValueSwitchParameterErrorScriptDefinition", resourceCulture); + } + } + /// /// Looks up a localized string similar to AvoidDefaultValueSwitchParameter. /// @@ -240,6 +249,15 @@ internal static string AvoidShouldContinueWithoutForceError { } } + /// + /// Looks up a localized string similar to Function '{0}' in script definition uses ShouldContinue but does not have a boolean force parameter. The force parameter will allow users of the script to bypass ShouldContinue prompt. + /// + internal static string AvoidShouldContinueWithoutForceErrorScriptDefinition { + get { + return ResourceManager.GetString("AvoidShouldContinueWithoutForceErrorScriptDefinition", resourceCulture); + } + } + /// /// Looks up a localized string similar to AvoidShouldContinueWithoutForce. /// @@ -501,6 +519,15 @@ internal static string AvoidUsingConvertToSecureStringWithPlainTextError { } } + /// + /// Looks up a localized string similar to Script definition uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead.. + /// + internal static string AvoidUsingConvertToSecureStringWithPlainTextErrorScriptDefinition { + get { + return ResourceManager.GetString("AvoidUsingConvertToSecureStringWithPlainTextErrorScriptDefinition", resourceCulture); + } + } + /// /// Looks up a localized string similar to AvoidUsingConvertToSecureStringWithPlainText. /// @@ -771,6 +798,15 @@ internal static string AvoidUsingWMICmdletError { } } + /// + /// Looks up a localized string similar to Script definition 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 AvoidUsingWMICmdletErrorScriptDefinition { + get { + return ResourceManager.GetString("AvoidUsingWMICmdletErrorScriptDefinition", resourceCulture); + } + } + /// /// Looks up a localized string similar to AvoidUsingWMICmdlet. /// @@ -807,6 +843,15 @@ internal static string AvoidUsingWriteHostError { } } + /// + /// Looks up a localized string similar to Script definition uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.. + /// + internal static string AvoidUsingWriteHostErrorScriptDefinition { + get { + return ResourceManager.GetString("AvoidUsingWriteHostErrorScriptDefinition", resourceCulture); + } + } + /// /// Looks up a localized string similar to AvoidUsingWriteHost. /// @@ -1338,6 +1383,15 @@ internal static string ReturnCorrectTypesForSetTargetResourceFunctionsDSCError { } } + /// + /// Looks up a localized string similar to ScriptDefinition. + /// + internal static string ScriptDefinitionName { + get { + return ResourceManager.GetString("ScriptDefinitionName", resourceCulture); + } + } + /// /// Looks up a localized string similar to www.sharepoint.com. /// diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 4657658df..2679b9b34 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -756,4 +756,22 @@ UseBOMForUnicodeEncodedFile + + Script definition has a switch parameter default to true. + + + Function '{0}' in script definition uses ShouldContinue but does not have a boolean force parameter. The force parameter will allow users of the script to bypass ShouldContinue prompt + + + Script definition uses ConvertTo-SecureString with plaintext. This will expose secure information. Encrypted standard strings should be used instead. + + + Script definition 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. + + + Script definition uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information. + + + ScriptDefinition + \ No newline at end of file diff --git a/Rules/UseBOMForUnicodeEncodedFile.cs b/Rules/UseBOMForUnicodeEncodedFile.cs index 7ea1d79eb..eeeddf90c 100644 --- a/Rules/UseBOMForUnicodeEncodedFile.cs +++ b/Rules/UseBOMForUnicodeEncodedFile.cs @@ -33,6 +33,12 @@ public class UseBOMForUnicodeEncodedFile : IScriptRule /// public IEnumerable AnalyzeScript(Ast ast, string fileName) { + // we are given a script definition, do not analyze + if (String.IsNullOrWhiteSpace(fileName)) + { + yield break; + } + byte[] byteStream = File.ReadAllBytes(fileName); if (null == GetByteStreamEncoding(byteStream)) diff --git a/Rules/UseUTF8EncodingForHelpFile.cs b/Rules/UseUTF8EncodingForHelpFile.cs index 7f301abc8..05989d546 100644 --- a/Rules/UseUTF8EncodingForHelpFile.cs +++ b/Rules/UseUTF8EncodingForHelpFile.cs @@ -30,6 +30,13 @@ public class UseUTF8EncodingForHelpFile : IScriptRule /// public IEnumerable AnalyzeScript(Ast ast, string fileName) { + // we are given a script definition, do not analyze + // this rule is not applicable for that + if (String.IsNullOrWhiteSpace(fileName)) + { + yield break; + } + if (!String.IsNullOrWhiteSpace(fileName) && Helper.Instance.IsHelpFile(fileName)) { using (var reader = new System.IO.StreamReader(fileName, true)) diff --git a/Tests/Engine/GlobalSuppression.test.ps1 b/Tests/Engine/GlobalSuppression.test.ps1 index 11b030756..e5aeb87cd 100644 --- a/Tests/Engine/GlobalSuppression.test.ps1 +++ b/Tests/Engine/GlobalSuppression.test.ps1 @@ -7,19 +7,25 @@ if (!(Get-Module PSScriptAnalyzer) -and !$testingLibraryUsage) } $directory = Split-Path -Parent $MyInvocation.MyCommand.Path -$violations = Invoke-ScriptAnalyzer $directory\GlobalSuppression.ps1 -$suppression = Invoke-ScriptAnalyzer $directory\GlobalSuppression.ps1 -Profile $directory\Profile.ps1 +$violations = Invoke-ScriptAnalyzer "$directory\GlobalSuppression.ps1" +$violationsUsingScriptDefinition = Invoke-ScriptAnalyzer -ScriptDefinition (Get-Content -Raw "$directory\GlobalSuppression.ps1") +$suppression = Invoke-ScriptAnalyzer "$directory\GlobalSuppression.ps1" -Profile "$directory\Profile.ps1" +$suppressionUsingScriptDefinition = Invoke-ScriptAnalyzer -ScriptDefinition (Get-Content -Raw "$directory\GlobalSuppression.ps1") -Profile "$directory\Profile.ps1" Describe "GlobalSuppression" { Context "Exclude Rule" { It "Raises 1 violation for uninitialized variable and 1 for cmdlet alias" { $withoutProfile = $violations | Where-Object { $_.RuleName -eq "PSAvoidUsingCmdletAliases" -or $_.RuleName -eq "PSAvoidUninitializedVariable" } - $withoutProfile.Count | Should Be 2 + $withoutProfile.Count | Should Be 1 + $withoutProfile = $violationsUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSAvoidUsingCmdletAliases" -or $_.RuleName -eq "PSAvoidUninitializedVariable" } + $withoutProfile.Count | Should Be 1 } It "Does not raise any violations for uninitialized variable and cmdlet alias with profile" { $withProfile = $suppression | Where-Object { $_.RuleName -eq "PSAvoidUsingCmdletAliases" -or $_.RuleName -eq "PSAvoidUninitializedVariable" } $withProfile.Count | Should be 0 + $withProfile = $suppressionUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSAvoidUsingCmdletAliases" -or $_.RuleName -eq "PSAvoidUninitializedVariable" } + $withProfile.Count | Should be 0 } } @@ -27,11 +33,15 @@ Describe "GlobalSuppression" { It "Raises 1 violation for computername hard-coded" { $withoutProfile = $violations | Where-Object { $_.RuleName -eq "PSAvoidUsingComputerNameHardcoded" } $withoutProfile.Count | Should Be 1 + $withoutProfile = $violationsUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSAvoidUsingComputerNameHardcoded" } + $withoutProfile.Count | Should Be 1 } It "Does not raise any violations for computername hard-coded" { $withProfile = $suppression | Where-Object { $_.RuleName -eq "PSAvoidUsingComputerNameHardcoded" } $withProfile.Count | Should be 0 + $withProfile = $suppressionUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSAvoidUsingComputerNameHardcoded" } + $withProfile.Count | Should be 0 } } @@ -39,11 +49,15 @@ Describe "GlobalSuppression" { It "Raises 1 violation for internal url without profile" { $withoutProfile = $violations | Where-Object { $_.RuleName -eq "PSAvoidUsingInternalURLs" } $withoutProfile.Count | Should Be 1 + $withoutProfile = $violationsUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSAvoidUsingInternalURLs" } + $withoutProfile.Count | Should Be 1 } It "Does not raise any violations for internal urls with profile" { $withProfile = $suppression | Where-Object { $_.RuleName -eq "PSAvoidUsingInternalURLs" } $withProfile.Count | Should be 0 + $withProfile = $suppressionUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSAvoidUsingInternalURLs" } + $withProfile.Count | Should be 0 } } } \ No newline at end of file diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 index 467a71723..c85152b70 100644 --- a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 +++ b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 @@ -25,6 +25,16 @@ Describe "Test available parameters" { } } + Context "Path parameter" { + It "has a ScriptDefinition parameter" { + $params.ContainsKey("ScriptDefinition") | Should Be $true + } + + It "accepts string" { + $params["ScriptDefinition"].ParameterType.FullName | Should Be "System.String" + } + } + Context "CustomizedRulePath parameters" { It "has a CustomizedRulePath parameter" { $params.ContainsKey("CustomizedRulePath") | Should Be $true @@ -54,6 +64,46 @@ Describe "Test available parameters" { $params["Severity"].ParameterType.FullName | Should Be "System.String[]" } } + + Context "It has 2 parameter sets: File and ScriptDefinition" { + It "Has 2 parameter sets" { + $sa.ParameterSets.Count | Should Be 2 + } + + It "Has File parameter set" { + $hasFile = $false + foreach ($paramSet in $sa.ParameterSets) { + if ($paramSet.Name -eq "File") { + $hasFile = $true + break + } + } + + $hasFile | Should Be $true + } + + It "Has ScriptDefinition parameter set" { + $hasFile = $false + foreach ($paramSet in $sa.ParameterSets) { + if ($paramSet.Name -eq "ScriptDefinition") { + $hasFile = $true + break + } + } + + $hasFile | Should Be $true + } + + } +} + +Describe "Test ScriptDefinition" { + Context "When given a script definition" { + It "Does not run rules on script with more than 10 parser errors" { + $moreThanTenErrors = Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue -ScriptDefinition (Get-Content -Raw "$directory\CSharp.ps1") + $moreThanTenErrors.Count | Should Be 0 + } + } } Describe "Test Path" { diff --git a/Tests/Engine/LibraryUsage.tests.ps1 b/Tests/Engine/LibraryUsage.tests.ps1 index 37137c832..5e3c9694b 100644 --- a/Tests/Engine/LibraryUsage.tests.ps1 +++ b/Tests/Engine/LibraryUsage.tests.ps1 @@ -6,10 +6,15 @@ $directory = Split-Path -Parent $MyInvocation.MyCommand.Path # wraps the usage of ScriptAnalyzer as a .NET library function Invoke-ScriptAnalyzer { param ( - [parameter(Mandatory = $true, Position = 0)] + [CmdletBinding(DefaultParameterSetName="File")] + + [parameter(Mandatory = $true, Position = 0, ParameterSetName="File")] [Alias("PSPath")] [string] $Path, + [parameter(Mandatory = $true, ParameterSetName="ScriptDefinition")] + [string] $ScriptDefinition, + [Parameter(Mandatory = $false)] [string[]] $CustomizedRulePath = $null, @@ -41,7 +46,12 @@ function Invoke-ScriptAnalyzer { $SuppressedOnly.IsPresent ); - return $scriptAnalyzer.AnalyzePath($Path, $Recurse.IsPresent); + if ($PSCmdlet.ParameterSetName -eq "File") { + return $scriptAnalyzer.AnalyzePath($Path, $Recurse.IsPresent); + } + else { + return $scriptAnalyzer.AnalyzeScriptDefinition($ScriptDefinition); + } } # Define an implementation of the IOutputWriter interface diff --git a/Tests/Engine/RuleSuppression.tests.ps1 b/Tests/Engine/RuleSuppression.tests.ps1 index 7902648d7..35a4f15c7 100644 --- a/Tests/Engine/RuleSuppression.tests.ps1 +++ b/Tests/Engine/RuleSuppression.tests.ps1 @@ -7,13 +7,16 @@ if (!(Get-Module PSScriptAnalyzer) -and !$testingLibraryUsage) } $directory = Split-Path -Parent $MyInvocation.MyCommand.Path -$violations = Invoke-ScriptAnalyzer $directory\RuleSuppression.ps1 +$violationsUsingScriptDefinition = Invoke-ScriptAnalyzer -ScriptDefinition (Get-Content -Raw "$directory\RuleSuppression.ps1") +$violations = Invoke-ScriptAnalyzer "$directory\RuleSuppression.ps1" Describe "RuleSuppressionWithoutScope" { Context "Function" { It "Does not raise violations" { $suppression = $violations | Where-Object { $_.RuleName -eq "PSProvideCommentHelp" } $suppression.Count | Should Be 0 + $suppression = $violationsUsingScriptDefinition | Where-Object { $_.RuleName -eq "PSProvideCommentHelp" } + $suppression.Count | Should Be 0 } } @@ -21,6 +24,8 @@ Describe "RuleSuppressionWithoutScope" { It "Does not raise violations" { $suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidUsingInvokeExpression" } $suppression.Count | Should Be 0 + $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidUsingInvokeExpression" } + $suppression.Count | Should Be 0 } } @@ -28,6 +33,8 @@ Describe "RuleSuppressionWithoutScope" { It "Does not raise violations" { $suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidUsingCmdletAliases" } $suppression.Count | Should Be 0 + $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidUsingCmdletAliases" } + $suppression.Count | Should Be 0 } } @@ -35,6 +42,8 @@ Describe "RuleSuppressionWithoutScope" { It "Does not raise violations" { $suppression = $violations | Where-Object {$_.RuleName -eq "PSProvideCommentHelp" } $suppression.Count | Should Be 0 + $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSProvideCommentHelp" } + $suppression.Count | Should Be 0 } } @@ -42,6 +51,8 @@ Describe "RuleSuppressionWithoutScope" { It "Only suppress violations for that ID" { $suppression = $violations | Where-Object {$_.RuleName -eq "PSProvideDefaultParameterValue" } $suppression.Count | Should Be 1 + $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSProvideDefaultParameterValue" } + $suppression.Count | Should Be 1 } } } @@ -51,6 +62,8 @@ Describe "RuleSuppressionWithScope" { It "Does not raise violations" { $suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidUsingPositionalParameters" } $suppression.Count | Should Be 0 + $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidUsingPositionalParameters" } + $suppression.Count | Should Be 0 } } @@ -58,6 +71,8 @@ Describe "RuleSuppressionWithScope" { It "Does not raise violations" { $suppression = $violations | Where-Object {$_.RuleName -eq "PSAvoidUsingConvertToSecureStringWithPlainText" } $suppression.Count | Should Be 0 + $suppression = $violationsUsingScriptDefinition | Where-Object {$_.RuleName -eq "PSAvoidUsingConvertToSecureStringWithPlainText" } + $suppression.Count | Should Be 0 } } } \ No newline at end of file