Skip to content

Commit 05f6c3d

Browse files
committed
avoidoverwritingbuiltincmdlets first draft
1 parent e37daeb commit 05f6c3d

File tree

3 files changed

+363
-0
lines changed

3 files changed

+363
-0
lines changed
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
#if !CORECLR
7+
using System.ComponentModel.Composition;
8+
#endif
9+
using System.Globalization;
10+
using System.IO;
11+
using System.Linq;
12+
using System.Management.Automation.Language;
13+
using System.Text.RegularExpressions;
14+
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
15+
16+
using Newtonsoft.Json.Linq;
17+
18+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
19+
{
20+
/// <summary>
21+
/// AvoidLongLines: Checks if a script overwrites a cmdlet that comes with PowerShell
22+
/// </summary>
23+
#if !CORECLR
24+
[Export(typeof(IScriptRule))]
25+
#endif
26+
/// <summary>
27+
/// A class to check if a script overwrites a cmdlet that comes with PowerShell
28+
/// </summary>
29+
public class AvoidOverwritingBuiltInCmdlets : ConfigurableRule
30+
{
31+
/// <summary>
32+
/// Construct an object of AvoidOverwritingBuiltInCmdlets type.
33+
/// </summary>
34+
public AvoidOverwritingBuiltInCmdlets()
35+
{
36+
initialized = false;
37+
}
38+
39+
40+
[ConfigurableRuleProperty(defaultValue: "core-6.1.0-windows")]
41+
public object PowerShellVersion { get; set; }
42+
43+
private Dictionary<string, HashSet<string>> cmdletMap;
44+
private bool initialized;
45+
46+
47+
/// <summary>
48+
/// Analyzes the given ast to find the [violation]
49+
/// </summary>
50+
/// <param name="ast">AST to be analyzed. This should be non-null</param>
51+
/// <param name="fileName">Name of file that corresponds to the input AST.</param>
52+
/// <returns>A an enumerable type containing the violations</returns>
53+
public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
54+
{
55+
if (ast == null)
56+
{
57+
throw new ArgumentNullException(nameof(ast));
58+
}
59+
60+
var functionDefinitions = ast.FindAll(testAst => testAst is FunctionDefinitionAst, true);
61+
if (functionDefinitions.Count() < 1)
62+
{
63+
// There are no function definitions in this AST and so it's not worth checking the rest of this rule
64+
return null;
65+
}
66+
67+
else
68+
{
69+
var diagnosticRecords = new List<DiagnosticRecord>();
70+
if (!initialized)
71+
{
72+
Initialize();
73+
}
74+
75+
foreach (var functionDef in functionDefinitions)
76+
{
77+
FunctionDefinitionAst funcDef = functionDef as FunctionDefinitionAst;
78+
if (funcDef == null)
79+
{
80+
continue;
81+
}
82+
83+
string functionName = funcDef.Name;
84+
foreach (var map in cmdletMap)
85+
{
86+
if (map.Value.Contains(functionName))
87+
{
88+
diagnosticRecords.Add(CreateDiagnosticRecord(functionName, map.Key, functionDef.Extent));
89+
}
90+
}
91+
}
92+
93+
return diagnosticRecords;
94+
}
95+
}
96+
97+
98+
private DiagnosticRecord CreateDiagnosticRecord(string FunctionName, string PSVer, IScriptExtent ViolationExtent)
99+
{
100+
var record = new DiagnosticRecord(
101+
string.Format(CultureInfo.CurrentCulture,
102+
string.Format(Strings.AvoidOverwritingBuiltInCmdletsError, FunctionName, PSVer)),
103+
ViolationExtent,
104+
GetName(),
105+
GetDiagnosticSeverity(),
106+
ViolationExtent.File,
107+
null
108+
);
109+
return record;
110+
}
111+
112+
113+
private void Initialize()
114+
{
115+
var psVerObjectArray = PowerShellVersion as object[];
116+
var psVerList = new List<string>();
117+
118+
if (psVerObjectArray == null)
119+
{
120+
psVerList = PowerShellVersion as List<string>;
121+
if (psVerList == null)
122+
{
123+
return;
124+
}
125+
}
126+
else
127+
{
128+
foreach (var psVer in psVerObjectArray)
129+
{
130+
var psVerString = psVer as string;
131+
if (psVerString == null)
132+
{
133+
// Ignore non-string invalid entries
134+
continue;
135+
}
136+
psVerList.Add(psVerString);
137+
}
138+
}
139+
140+
string settingsPath = Settings.GetShippedSettingsDirectory();
141+
142+
if (settingsPath == null || !ContainsReferenceFile(settingsPath))
143+
{
144+
return;
145+
}
146+
147+
ProcessDirectory(settingsPath, psVerList);
148+
149+
if (cmdletMap.Keys.Count != psVerList.Count())
150+
{
151+
return;
152+
}
153+
154+
initialized = true;
155+
}
156+
157+
158+
private bool ContainsReferenceFile(string directory)
159+
{
160+
return File.Exists(Path.Combine(directory, PowerShellVersion + ".json"));
161+
}
162+
163+
164+
private void ProcessDirectory(string path, IEnumerable<string> acceptablePlatformSpecs)
165+
{
166+
foreach (var filePath in Directory.EnumerateFiles(path))
167+
{
168+
var extension = Path.GetExtension(filePath);
169+
if (String.IsNullOrWhiteSpace(extension)
170+
|| !extension.Equals(".json", StringComparison.OrdinalIgnoreCase))
171+
{
172+
continue;
173+
}
174+
175+
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
176+
if (acceptablePlatformSpecs != null
177+
&& !acceptablePlatformSpecs.Contains(fileNameWithoutExt, StringComparer.OrdinalIgnoreCase))
178+
{
179+
continue;
180+
}
181+
182+
cmdletMap[fileNameWithoutExt] = GetCmdletsFromData(JObject.Parse(File.ReadAllText(filePath)));
183+
}
184+
}
185+
186+
187+
private HashSet<string> GetCmdletsFromData(dynamic deserializedObject)
188+
{
189+
var cmdlets = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
190+
dynamic modules = deserializedObject.Modules;
191+
foreach (dynamic module in modules)
192+
{
193+
if (module.ExportedCommands == null)
194+
{
195+
continue;
196+
}
197+
198+
foreach (dynamic cmdlet in module.ExportedCommands)
199+
{
200+
var name = cmdlet.Name as string;
201+
if (name == null)
202+
{
203+
name = cmdlet.Name.ToObject<string>();
204+
}
205+
cmdlets.Add(name);
206+
}
207+
}
208+
209+
return cmdlets;
210+
}
211+
212+
213+
private bool IsValidPlatformString(string fileNameWithoutExt)
214+
{
215+
string psedition, psversion, os;
216+
return GetVersionInfoFromPlatformString(
217+
fileNameWithoutExt,
218+
out psedition,
219+
out psversion,
220+
out os);
221+
}
222+
223+
224+
private bool GetVersionInfoFromPlatformString(
225+
string fileName,
226+
out string psedition,
227+
out string psversion,
228+
out string os)
229+
{
230+
psedition = null;
231+
psversion = null;
232+
os = null;
233+
const string pattern = @"^(?<psedition>core|desktop)-(?<psversion>[\S]+)-(?<os>windows|linux|macos)$";
234+
var match = Regex.Match(fileName, pattern, RegexOptions.IgnoreCase);
235+
if (match == Match.Empty)
236+
{
237+
return false;
238+
}
239+
psedition = match.Groups["psedition"].Value;
240+
psversion = match.Groups["psversion"].Value;
241+
os = match.Groups["os"].Value;
242+
return true;
243+
}
244+
245+
246+
/// <summary>
247+
/// Retrieves the common name of this rule.
248+
/// </summary>
249+
public override string GetCommonName()
250+
{
251+
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidOverwritingBuiltInCmdletsCommonName);
252+
}
253+
254+
/// <summary>
255+
/// Retrieves the description of this rule.
256+
/// </summary>
257+
public override string GetDescription()
258+
{
259+
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidOverwritingBuiltInCmdletsDescription);
260+
}
261+
262+
/// <summary>
263+
/// Retrieves the name of this rule.
264+
/// </summary>
265+
public override string GetName()
266+
{
267+
return string.Format(
268+
CultureInfo.CurrentCulture,
269+
Strings.NameSpaceFormat,
270+
GetSourceName(),
271+
Strings.AvoidOverwritingBuiltInCmdletsName);
272+
}
273+
274+
/// <summary>
275+
/// Retrieves the severity of the rule: error, warning or information.
276+
/// </summary>
277+
public override RuleSeverity GetSeverity()
278+
{
279+
return RuleSeverity.Warning;
280+
}
281+
282+
/// <summary>
283+
/// Gets the severity of the returned diagnostic record: error, warning, or information.
284+
/// </summary>
285+
/// <returns></returns>
286+
public DiagnosticSeverity GetDiagnosticSeverity()
287+
{
288+
return DiagnosticSeverity.Warning;
289+
}
290+
291+
/// <summary>
292+
/// Retrieves the name of the module/assembly the rule is from.
293+
/// </summary>
294+
public override string GetSourceName()
295+
{
296+
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
297+
}
298+
299+
/// <summary>
300+
/// Retrieves the type of the rule, Builtin, Managed or Module.
301+
/// </summary>
302+
public override SourceType GetSourceType()
303+
{
304+
return SourceType.Builtin;
305+
}
306+
}
307+
}

Rules/Strings.Designer.cs

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Rules/Strings.resx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,18 @@
810810
<data name="UseCompatibleCmdletsError" xml:space="preserve">
811811
<value>'{0}' is not compatible with PowerShell edition '{1}', version '{2}' and OS '{3}'</value>
812812
</data>
813+
<data name="AvoidOverwritingBuiltInCmdletsName" xml:space="preserve">
814+
<value>AvoidOverwritingBuiltInCmdlets</value>
815+
</data>
816+
<data name="AvoidOverwritingBuiltInCmdletsCommonName" xml:space="preserve">
817+
<value>Avoid overwriting built in cmdlets</value>
818+
</data>
819+
<data name="AvoidOverwritingBuiltInCmdletsDescription" xml:space="preserve">
820+
<value>Do not overwrite the definition of cmdlets that are included with PowerShell</value>
821+
</data>
822+
<data name="AvoidOverwritingBuiltInCmdletsError" xml:space="preserve">
823+
<value>'{0}' is a cmdlet that is included with PowerShell (version {1}) whose definition should not be overridden</value>
824+
</data>
813825
<data name="UseCompatibleCommandsName" xml:space="preserve">
814826
<value>UseCompatibleCommands</value>
815827
</data>

0 commit comments

Comments
 (0)