Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Engine/Commands/GetScriptAnalyzerRuleCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ protected override void BeginProcessing()

string[] rulePaths = Helper.ProcessCustomRulePaths(customRulePath,
this.SessionState, recurseCustomRulePath);
ScriptAnalyzer.Instance.Initialize(this, rulePaths, null, null, null, null == rulePaths);
ScriptAnalyzer.Instance.Initialize(this, rulePaths,
includeDefaultRules: rulePaths == null, terminateOnMissingRules: false);
}

/// <summary>
Expand Down
104 changes: 63 additions & 41 deletions Engine/EditableText.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Windows.PowerShell.ScriptAnalyzer.Extensions;
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
{
Expand All @@ -14,7 +15,9 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
/// </summary>
public class EditableText
{
private TextLines lines { get; set; }
private static readonly Regex LineEnding = new Regex(@"(\r\n|\n|\r)");
private readonly TextLines lines;
private readonly TextLines mixedLineEndings;

/// <summary>
/// Return the number of lines in the text.
Expand All @@ -24,15 +27,39 @@ public class EditableText
/// <summary>
/// The text that is available for editing.
/// </summary>
public string Text { get { return String.Join(NewLine, lines); } }
public string Text
{
get
{
if (mixedLineEndings == null)
{
return string.Join(NewLine, lines);
}

var text = new StringBuilder();
using (var endings = mixedLineEndings.GetEnumerator())
{
foreach (string line in lines)
{
text.Append(line);
if (endings.MoveNext())
{
text.Append(endings.Current);
}
}
}

return text.ToString();
}
}

/// <summary>
/// The lines in the Text.
/// </summary>
public ReadOnlyCollection<string> Lines => lines.ReadOnly();

/// <summary>
/// The new line character in the Text.
/// The line ending used when inserting a new line.
/// </summary>
public string NewLine { get; private set; }

Expand All @@ -47,9 +74,15 @@ public EditableText(string text)
throw new ArgumentNullException(nameof(text));
}

string[] lines;
NewLine = GetNewLineCharacters(text, out lines);
this.lines = new TextLines(lines);
// The captured delimiters occupy the odd indexes between lines.
string[] parts = LineEnding.Split(text);
lines = new TextLines(parts.Where((part, index) => index % 2 == 0));
NewLine = parts.Length > 1 ? parts[1] : Environment.NewLine;
var endings = parts.Where((part, index) => index % 2 == 1);
if (endings.Any(ending => ending != NewLine))
{
mixedLineEndings = new TextLines(endings);
}
}

/// <summary>
Expand All @@ -76,9 +109,26 @@ public EditableText ApplyEdit(TextEdit textEdit)
lines[textEdit.EndLineNumber - 1].Length - textEdit.EndColumnNumber + 1);

// Remove the old lines
for (int i = 0; i <= textEdit.EndLineNumber - textEdit.StartLineNumber; i++)
int firstLineIndex = textEdit.StartLineNumber - 1;
int linesToRemove = textEdit.EndLineNumber - textEdit.StartLineNumber + 1;
for (int i = 0; i < linesToRemove; i++)
{
lines.RemoveAt(textEdit.StartLineNumber - 1);
lines.RemoveAt(firstLineIndex);
}

// Keep the separator after the last replaced line.
if (mixedLineEndings != null)
{
for (int i = 1; i < linesToRemove; i++)
{
mixedLineEndings.RemoveAt(firstLineIndex);
}

if (editLines.Length == 0 && lines.Count > 0)
{
// No replacement line remains to carry the separator.
mixedLineEndings.RemoveAt(Math.Min(firstLineIndex, lines.Count - 1));
}
}

// Build and insert the new lines
Expand All @@ -102,6 +152,10 @@ public EditableText ApplyEdit(TextEdit textEdit)
}

lines.Insert(currentLineNumber - 1, finalLine);
if (changeIndex < editLines.Length - 1)
{
mixedLineEndings?.Insert(currentLineNumber - 1, NewLine);
}
currentLineNumber++;
}

Expand Down Expand Up @@ -156,37 +210,5 @@ private void ValidateTextEditExtent(TextEdit textEdit)
Strings.EditableTextRangeIsNotContained));
}
}

private static string GetNewLineCharacters(string text, out string[] lines)
{
int numNewLineChars = GetNumNewLineCharacters(text, out lines);
if (lines.Length == 1)
{
return Environment.NewLine;
}

return text.Substring(lines[0].Length, numNewLineChars);
}

private static int GetNumNewLineCharacters(string text, out string[] lines)
{
lines = text.GetLines().ToArray();
if (lines.Length == 1)
{
return Environment.NewLine.Length;
}

var charsInLines = lines.Sum(line => line.Length);
var numCharDiff = text.Length - charsInLines;
int remainder = numCharDiff % (lines.Length - 1);
if (remainder != 0)
{
throw new ArgumentException(
String.Format(CultureInfo.CurrentCulture, Strings.EditableTextInvalidLineEnding),
nameof(text));
}

return numCharDiff / (lines.Length - 1);
}
}
}
28 changes: 19 additions & 9 deletions Engine/ScriptAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ internal void Initialize<TCmdlet>(
string[] excludeRuleNames = null,
string[] severity = null,
bool includeDefaultRules = false,
SuppressionPreference suppressionPreference = SuppressionPreference.Omit)
SuppressionPreference suppressionPreference = SuppressionPreference.Omit,
bool terminateOnMissingRules = true)
where TCmdlet : PSCmdlet, IOutputWriter
{
if (cmdlet == null)
Expand All @@ -139,7 +140,8 @@ internal void Initialize<TCmdlet>(
excludeRuleNames,
severity,
includeDefaultRules,
suppressionPreference);
suppressionPreference,
terminateOnMissingRules: terminateOnMissingRules);
}

/// <summary>
Expand Down Expand Up @@ -650,7 +652,8 @@ private void Initialize(
string[] severity,
bool includeDefaultRules = false,
SuppressionPreference suppressionPreference = SuppressionPreference.Omit,
string profile = null)
string profile = null,
bool terminateOnMissingRules = true)
{
if (outputWriter == null)
{
Expand Down Expand Up @@ -786,12 +789,19 @@ private void Initialize(
{
string errorMessage = string.Format(CultureInfo.CurrentCulture, Strings.RulesNotFound);

this.outputWriter.ThrowTerminatingError(
new ErrorRecord(
new Exception(errorMessage),
errorMessage,
ErrorCategory.ObjectNotFound,
this));
var error = new ErrorRecord(
new Exception(errorMessage),
errorMessage,
ErrorCategory.ObjectNotFound,
this);
if (terminateOnMissingRules)
{
this.outputWriter.ThrowTerminatingError(error);
}
else
{
this.outputWriter.WriteError(error);
}
}

#endregion
Expand Down
25 changes: 13 additions & 12 deletions Rules/AvoidUsingPlainTextForPassword.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using System.ComponentModel.Composition;
#endif
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;

namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
{
Expand All @@ -21,6 +23,12 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
#endif
public class AvoidUsingPlainTextForPassword : IScriptRule
{
private static readonly string[] PasswordIndicators = { "Password", "Passphrase", "Credential" };
// Split on separators and on the boundaries in DbCred, DBCred, and CREDSFile.
private static readonly Regex IdentifierWordBoundary = new Regex(
@"[^\p{L}]+|(?<=\p{Ll})(?=\p{Lu})|(?<=\p{Lu})(?=\p{Lu}\p{Ll})",
RegexOptions.CultureInvariant);

/// <summary>
/// AvoidUsingPlainTextForPassword: Check that parameter "password", "passphrase" and do not use plaintext.
/// </summary>
Expand All @@ -31,23 +39,16 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
// Finds all ParamAsts.
IEnumerable<Ast> paramAsts = ast.FindAll(testAst => testAst is ParameterAst, true);

List<String> passwords = new List<String>() {"Password", "Passphrase", "Cred", "Credential"};

// Iterates all ParamAsts and check if their names are on the list.
foreach (ParameterAst paramAst in paramAsts)
{
Type paramType = paramAst.StaticType;
bool hasPwd = false;
String paramName = paramAst.Name.VariablePath.ToString();

foreach (String password in passwords)
{
if (paramName.IndexOf(password, StringComparison.OrdinalIgnoreCase) != -1)
{
hasPwd = true;
break;
}
}
bool hasPwd = PasswordIndicators.Any(indicator => paramName.IndexOf(indicator, StringComparison.OrdinalIgnoreCase) >= 0)
|| (paramName.IndexOf("Cred", StringComparison.OrdinalIgnoreCase) >= 0
&& IdentifierWordBoundary.Split(paramName).Any(word =>
word.Equals("Cred", StringComparison.OrdinalIgnoreCase)
|| word.Equals("Creds", StringComparison.OrdinalIgnoreCase)));

if (hasPwd && ((!paramType.IsArray && (paramType == typeof(String) || paramType == typeof(object)))
|| (paramType.IsArray && (paramType.GetElementType() == typeof(String) || paramType.GetElementType() == typeof(object)))))
Expand Down
4 changes: 3 additions & 1 deletion Rules/UseBOMForUnicodeEncodedFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)

if (0 != byteStream.Count(o => o > 0x7F))
{
// A missing BOM is a file-level diagnostic at the start of the file.
var start = new ScriptPosition(fileName, 1, 1, null);
yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.UseBOMForUnicodeEncodedFileError, System.IO.Path.GetFileName(fileName), null),
null, GetName(), DiagnosticSeverity.Warning, fileName);
new ScriptExtent(start, start), GetName(), DiagnosticSeverity.Warning, fileName);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Rules/UseConsistentWhitespace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ private IEnumerable<DiagnosticRecord> FindInnerBraceViolations(TokenOperations t
// Ranges which represent braced member access. Tokens within these ranges should be
// excluded from formatting.
var exclusionRanges = tokenOperations.GetBracedMemberAccessRanges();
foreach (var lCurly in tokenOperations.GetTokenNodes(TokenKind.LCurly))
foreach (var lCurly in tokenOperations.GetTokenNodes(token => token.Kind == TokenKind.LCurly || token.Kind == TokenKind.AtCurly))
{
if (lCurly.Next == null
|| !(lCurly.Previous == null || IsPreviousTokenOnSameLine(lCurly))
Expand All @@ -285,7 +285,7 @@ private IEnumerable<DiagnosticRecord> FindInnerBraceViolations(TokenOperations t
GetDiagnosticSeverity(),
tokenOperations.Ast.Extent.File,
null,
GetCorrections(lCurly.Previous.Value, lCurly.Value, lCurly.Next.Value, true, false).ToList());
GetCorrections(lCurly.Previous?.Value, lCurly.Value, lCurly.Next.Value, true, false).ToList());
}
}

Expand Down
19 changes: 16 additions & 3 deletions Rules/UseProcessBlockForPipelineCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
{
if (scriptblockAst.ProcessBlock != null
|| scriptblockAst.ParamBlock?.Attributes == null
|| scriptblockAst.ParamBlock.Parameters == null)
|| scriptblockAst.ParamBlock.Parameters == null
|| ReadsPipelineInput(scriptblockAst.EndBlock))
{
continue;
}
Expand All @@ -45,8 +46,9 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)

foreach (NamedAttributeArgumentAst namedArgument in paramAttributeAst.NamedArguments)
{
if (!namedArgument.ArgumentName.Equals("valuefrompipeline", StringComparison.OrdinalIgnoreCase)
&& !namedArgument.ArgumentName.Equals("valuefrompipelinebypropertyname", StringComparison.OrdinalIgnoreCase))
if (!(namedArgument.ArgumentName.Equals("ValueFromPipeline", StringComparison.OrdinalIgnoreCase)
|| namedArgument.ArgumentName.Equals("ValueFromPipelineByPropertyName", StringComparison.OrdinalIgnoreCase))
|| !Helper.Instance.GetNamedArgumentAttributeValue(namedArgument))
{
continue;
}
Expand All @@ -65,6 +67,17 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
}
}

private static bool ReadsPipelineInput(NamedBlockAst endBlock)
{
return endBlock?.Find(
candidate => candidate is VariableExpressionAst variable
&& (variable.VariablePath.IsUnqualified || variable.VariablePath.IsLocal)
&& string.Equals(Helper.Instance.VariableNameWithoutScope(variable.VariablePath), "input", StringComparison.OrdinalIgnoreCase)
&& !(variable.Parent is AssignmentStatementAst assignment
&& assignment.Left == variable && assignment.Operator == TokenKind.Equals),
searchNestedScriptBlocks: false) != null;
}

public string GetName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.UseProcessBlockForPipelineCommandName);
Expand Down
17 changes: 11 additions & 6 deletions Tests/Documentation/RuleDocumentation.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,22 @@ Describe "Validate rule documentation files" {
$linkDefLine = @{}
$usedRefs = @{}
# Regex patterns.
# Table rule cell: | [RuleName][ref] | Severity | Default state |... | -> capture name, ref, severity, defstate.
$ruleRowRegex = '^\|\s*\[(?<name>[^\]]+)\]\[(?<ref>[^\]]+)\]\s*\|\s*(?<severity>[^|]+?)\s*\|(?<defstate>[^|]+?)\s*\|'
# A rule row can use a full or collapsed reference-style link.
$ruleRowRegex = '^\|\s*\[(?<name>[^\]]+)\]\[(?<ref>[^\]]*)\]\s*\|\s*(?<severity>[^|]+?)\s*\|(?<defstate>[^|]+?)\s*\|'
# Link definition: [ref]: target (target may include an #anchor).
$linkDefRegex = '^\[(?<ref>[^\]]+)\]:\s*(?<target>\S+)'
# Any reference-style usage anywhere: ...][ref]...
$refUsageRegex = '\]\[(?<ref>[^\]]+)\]'
# Full and collapsed reference-style links, such as [text][ref] and [ref][].
$refUsageRegex = '\[(?<label>[^\]]+)\]\[(?<ref>[^\]]*)\]'
$lines = Get-Content (Join-Path $ruleDocDirectory 'README.md')
$lineNumber = 0
foreach ($line in $lines) {
$lineNumber++
if ($line -match $ruleRowRegex) {
$ref = if ($Matches['ref']) { $Matches['ref'] } else { $Matches['name'] }
$ruleTable += [pscustomobject]@{
RowName = $Matches['name'].Trim()
RuleName = 'PS' + $Matches['name'].Trim()
Ref = $Matches['ref'].Trim()
Ref = $ref.Trim()
Severity = $Matches['severity'].Trim()
DefState = $Matches['defstate'].Trim()
Line = $lineNumber
Expand All @@ -53,7 +54,11 @@ Describe "Validate rule documentation files" {

# Collect every reference usage (table rows and prose) for orphan detection.
foreach ($match in [regex]::matches($line, $refUsageRegex)) {
$usedRefs[$match.Groups['ref'].Value] = 1
$ref = $match.Groups['ref'].Value
if (!$ref) {
$ref = $match.Groups['label'].Value
}
$usedRefs[$ref] = 1
}
}
}
Expand Down
Loading