Skip to content

New rule: AvoidOverwritingBuiltInCmdlets #1348

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Dec 9, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
05f6c3d
avoidoverwritingbuiltincmdlets first draft
thomasrayner Sep 30, 2019
8aa60a9
rough draft avoidoverwritingcmdlets working
thomasrayner Sep 30, 2019
651ed5d
Added tests, fixed typos, changed default PowerShellVersion behavior
thomasrayner Oct 1, 2019
aeea36c
updates a/p rjmholt
thomasrayner Oct 16, 2019
07825ce
remove unneeded else
thomasrayner Oct 16, 2019
4583924
avoidoverwritingbuiltincmdlets first draft
thomasrayner Sep 30, 2019
9c90ccd
rough draft avoidoverwritingcmdlets working
thomasrayner Sep 30, 2019
f5c6a9d
Added tests, fixed typos, changed default PowerShellVersion behavior
thomasrayner Oct 1, 2019
cedb19b
updates a/p rjmholt
thomasrayner Oct 16, 2019
6fb31b6
remove unneeded else
thomasrayner Oct 16, 2019
e1bfadd
changes from upstream
thomasrayner Oct 16, 2019
c55369c
updated readme - want tests to run in CI again
thomasrayner Oct 23, 2019
a67d3e9
prevent adding duplicate keys
thomasrayner Oct 23, 2019
bb6ae0a
return an empty list instead of null
thomasrayner Oct 23, 2019
debee2e
update rule count
thomasrayner Oct 23, 2019
ee3bb2e
fixing pwsh not present issue in test
thomasrayner Oct 23, 2019
e1688af
fixing a ps 4 test broke a linux test
thomasrayner Oct 23, 2019
c9bb6b6
better PS core detection
thomasrayner Oct 23, 2019
59e11d8
Add reference to UseCompatibleCmdlets doc
thomasrayner Oct 28, 2019
111495d
changes a/p Chris
thomasrayner Oct 28, 2019
da05f30
Update RuleDocumentation/AvoidOverwritingBuiltInCmdlets.md
thomasrayner Oct 29, 2019
fbd5ba0
trimmed doc and changed functiondefinitions detection to be more perf…
thomasrayner Oct 29, 2019
f2df044
retrigger-ci after fix was made in master
Nov 19, 2019
c2961eb
retrigger-ci due to sporadic test failure
Nov 20, 2019
5a8e109
Update number of expected rules due to recent merge of PR #1373
bergmeister Dec 9, 2019
bdd7dbe
Merge branch 'master' into avoidoverwritingcmdlets
bergmeister Dec 9, 2019
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
42 changes: 42 additions & 0 deletions RuleDocumentation/AvoidOverwritingBuiltInCmdlets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# AvoidOverwritingBuiltInCmdlets

**Severity Level: Warning**

## Description

This rule flags cmdlets that are available in a given Edition/Version of PowerShell on a given Operating System which are overwritten by a function declaration. It works by comparing function declarations against a set of whitelists which ship with PSScriptAnalyzer. They can be found at `/path/to/PSScriptAnalyzerModule/Settings`. These files are of the form, `PSEDITION-PSVERSION-OS.json` where `PSEDITION` can be either `Core` or `Desktop`, `OS` can be either `Windows`, `Linux` or `MacOS`, and `Version` is the PowerShell version.

## Configuration

To enable the rule to check if your script is compatible on PowerShell Core on Windows, put the following your settings file.


```PowerShell
@{
'Rules' = @{
'PSAvoidOverwritingBuiltInCmdlets' = @{
'PowerShellVersion' = @("core-6.1.0-windows")
}
}
}
```

### Parameters

#### PowerShellVersion

The parameter `PowerShellVersion` is a list that contain any of the following

- desktop-2.0-windows
- desktop-3.0-windows
- desktop-4.0-windows (taken from Windows Server 2012R2)
- desktop-5.1.14393.206-windows
- core-6.1.0-windows (taken from Windows 10 - 1803)
- core-6.1.0-linux (taken from Ubuntu 18.04)
- core-6.1.0-linux-arm (taken from Raspbian)
- core-6.1.0-macos

**Note**: The default value for `PowerShellVersion` is `"core-6.1.0-windows"` if PowerShell 6 or later is installed, and `"desktop-5.1.14393.206-windows"` if it is not.

Usually, patched versions of PowerShell have the same cmdlet data, therefore only settings of major and minor versions of PowerShell are supplied. One can also create a custom settings file as well with the [New-CommandDataFile.ps1](https://github.com/PowerShell/PSScriptAnalyzer/blob/development/Utils/New-CommandDataFile.ps1) script and use it by placing the created `JSON` into the `Settings` folder of the `PSScriptAnalyzer` module installation folder, then the `PowerShellVersion` parameter is just its file name (that can also be changed if desired).
Note that the `core-6.0.2-*` files were removed in PSScriptAnalyzer 1.18 since PowerShell 6.0 reached it's end of life.
1 change: 1 addition & 0 deletions RuleDocumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
|[AvoidGlobalFunctions](./AvoidGlobalFunctions.md) | Warning | |
|[AvoidGlobalVars](./AvoidGlobalVars.md) | Warning | |
|[AvoidInvokingEmptyMembers](./AvoidInvokingEmptyMembers.md) | Warning | |
|[AvoidOverwritingBuiltInCmdlets](./AvoidOverwritingBuiltInCmdlets.md) | Warning | |
|[AvoidNullOrEmptyHelpMessageAttribute](./AvoidNullOrEmptyHelpMessageAttribute.md) | Warning | |
|[AvoidShouldContinueWithoutForce](./AvoidShouldContinueWithoutForce.md) | Warning | |
|[AvoidUsingCmdletAliases](./AvoidUsingCmdletAliases.md) | Warning | Yes |
Expand Down
284 changes: 284 additions & 0 deletions Rules/AvoidOverwritingBuiltInCmdlets.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
#if !CORECLR
using System.ComponentModel.Composition;
#endif
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management.Automation.Language;
using System.Text.RegularExpressions;
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;

using Newtonsoft.Json.Linq;

namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
{
/// <summary>
/// AvoidOverwritingBuiltInCmdlets: Checks if a script overwrites a cmdlet that comes with PowerShell
/// </summary>
#if !CORECLR
[Export(typeof(IScriptRule))]
#endif
/// <summary>
/// A class to check if a script overwrites a cmdlet that comes with PowerShell
/// </summary>
public class AvoidOverwritingBuiltInCmdlets : ConfigurableRule
{
[ConfigurableRuleProperty(defaultValue: "")]
public string[] PowerShellVersion { get; set; }
private Dictionary<string, HashSet<string>> cmdletMap;
private bool initialized;


/// <summary>
/// Construct an object of AvoidOverwritingBuiltInCmdlets type.
/// </summary>
public AvoidOverwritingBuiltInCmdlets() : base()
{
initialized = false;
cmdletMap = new Dictionary<string, HashSet<string>>();
Enable = true; // Enable rule by default

string versionTest = string.Join("", PowerShellVersion);

if (versionTest != "core-6.1.0-windows" && versionTest != "desktop-5.1.14393.206-windows")
{
// PowerShellVersion is not already set to one of the acceptable defaults
// Try launching `pwsh -v` to see if PowerShell 6+ is installed, and use those cmdlets
// as a default. If 6+ is not installed this will throw an error, which when caught will
// allow us to use the PowerShell 5 cmdlets as a default.
try
{
var testProcess = new Process();
testProcess.StartInfo.FileName = "pwsh";
testProcess.StartInfo.Arguments = "-v";
testProcess.StartInfo.CreateNoWindow = true;
testProcess.StartInfo.UseShellExecute = false;
testProcess.Start();
PowerShellVersion = new[] {"core-6.1.0-windows"};
}
catch
{
PowerShellVersion = new[] {"desktop-5.1.14393.206-windows"};
}
}
}


/// <summary>
/// Analyzes the given ast to find the [violation]
/// </summary>
/// <param name="ast">AST to be analyzed. This should be non-null</param>
/// <param name="fileName">Name of file that corresponds to the input AST.</param>
/// <returns>A an enumerable type containing the violations</returns>
public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
{
if (ast == null)
{
throw new ArgumentNullException(nameof(ast));
}

var functionDefinitions = ast.FindAll(testAst => testAst is FunctionDefinitionAst, true);
if (functionDefinitions.Count() < 1)
{
// There are no function definitions in this AST and so it's not worth checking the rest of this rule
return null;
}

else
{
var diagnosticRecords = new List<DiagnosticRecord>();
if (!initialized)
{
Initialize();
if (!initialized)
{
throw new Exception("Failed to initialize rule " + GetName());
}
}

foreach (var functionDef in functionDefinitions)
{
FunctionDefinitionAst funcDef = functionDef as FunctionDefinitionAst;
if (funcDef == null)
{
continue;
}

string functionName = funcDef.Name;
foreach (var map in cmdletMap)
{
if (map.Value.Contains(functionName))
{
diagnosticRecords.Add(CreateDiagnosticRecord(functionName, map.Key, functionDef.Extent));
}
}
}

return diagnosticRecords;
}
}


private DiagnosticRecord CreateDiagnosticRecord(string FunctionName, string PSVer, IScriptExtent ViolationExtent)
{
var record = new DiagnosticRecord(
string.Format(CultureInfo.CurrentCulture,
string.Format(Strings.AvoidOverwritingBuiltInCmdletsError, FunctionName, PSVer)),
ViolationExtent,
GetName(),
GetDiagnosticSeverity(),
ViolationExtent.File,
null
);
return record;
}


private void Initialize()
{
var psVerList = PowerShellVersion;

string settingsPath = Settings.GetShippedSettingsDirectory();

foreach (string reference in psVerList)
{
if (settingsPath == null || !ContainsReferenceFile(settingsPath, reference))
{
return;
}
}

ProcessDirectory(settingsPath, psVerList);

if (cmdletMap.Keys.Count != psVerList.Count())
{
return;
}

initialized = true;
}


private bool ContainsReferenceFile(string directory, string reference)
{
return File.Exists(Path.Combine(directory, reference + ".json"));
}


private void ProcessDirectory(string path, IEnumerable<string> acceptablePlatformSpecs)
{
foreach (var filePath in Directory.EnumerateFiles(path))
{
var extension = Path.GetExtension(filePath);
if (String.IsNullOrWhiteSpace(extension)
|| !extension.Equals(".json", StringComparison.OrdinalIgnoreCase))
{
continue;
}

var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
if (acceptablePlatformSpecs != null
&& !acceptablePlatformSpecs.Contains(fileNameWithoutExt, StringComparer.OrdinalIgnoreCase))
{
continue;
}

cmdletMap.Add(fileNameWithoutExt, GetCmdletsFromData(JObject.Parse(File.ReadAllText(filePath))));
}
}


private HashSet<string> GetCmdletsFromData(dynamic deserializedObject)
{
var cmdlets = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
dynamic modules = deserializedObject.Modules;
foreach (dynamic module in modules)
{
if (module.ExportedCommands == null)
{
continue;
}

foreach (dynamic cmdlet in module.ExportedCommands)
{
var name = cmdlet.Name as string;
if (name == null)
{
name = cmdlet.Name.ToObject<string>();
}
cmdlets.Add(name);
}
}

return cmdlets;
}


/// <summary>
/// Retrieves the common name of this rule.
/// </summary>
public override string GetCommonName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidOverwritingBuiltInCmdletsCommonName);
}

/// <summary>
/// Retrieves the description of this rule.
/// </summary>
public override string GetDescription()
{
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidOverwritingBuiltInCmdletsDescription);
}

/// <summary>
/// Retrieves the name of this rule.
/// </summary>
public override string GetName()
{
return string.Format(
CultureInfo.CurrentCulture,
Strings.NameSpaceFormat,
GetSourceName(),
Strings.AvoidOverwritingBuiltInCmdletsName);
}

/// <summary>
/// Retrieves the severity of the rule: error, warning or information.
/// </summary>
public override RuleSeverity GetSeverity()
{
return RuleSeverity.Warning;
}

/// <summary>
/// Gets the severity of the returned diagnostic record: error, warning, or information.
/// </summary>
/// <returns></returns>
public DiagnosticSeverity GetDiagnosticSeverity()
{
return DiagnosticSeverity.Warning;
}

/// <summary>
/// Retrieves the name of the module/assembly the rule is from.
/// </summary>
public override string GetSourceName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
}

/// <summary>
/// Retrieves the type of the rule, Builtin, Managed or Module.
/// </summary>
public override SourceType GetSourceType()
{
return SourceType.Builtin;
}
}
}
44 changes: 44 additions & 0 deletions Rules/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading