Skip to content

Commit 5eff3c4

Browse files
committed
Merge pull request #90 from PowerShell/customizedRule
Fix bugs in importing external rule
2 parents ff75584 + cdcc4e5 commit 5eff3c4

File tree

5 files changed

+193
-46
lines changed

5 files changed

+193
-46
lines changed

Engine/Generic/ExternalRule.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ internal class ExternalRule : IExternalRule
2828
string param = string.Empty;
2929
string srcName = string.Empty;
3030
string modPath = string.Empty;
31-
31+
string paramType = string.Empty;
3232

3333
public string GetName()
3434
{
@@ -55,6 +55,11 @@ public SourceType GetSourceType()
5555
return SourceType.Module;
5656
}
5757

58+
public string GetParameterType()
59+
{
60+
return this.paramType;
61+
}
62+
5863
//Set the community rule level as warning as the current implementation does not require user to specify rule severity when defining their functions in PS scripts
5964
public RuleSeverity GetSeverity()
6065
{
@@ -80,14 +85,15 @@ public ExternalRule()
8085

8186
}
8287

83-
public ExternalRule(string name, string commonName, string desc, string param, string srcName, string modPath)
88+
public ExternalRule(string name, string commonName, string desc, string param, string paramType, string srcName, string modPath)
8489
{
8590
this.name = name;
8691
this.commonName = commonName;
8792
this.desc = desc;
8893
this.param = param;
8994
this.srcName = srcName;
9095
this.modPath = modPath;
96+
this.paramType = paramType;
9197
}
9298

9399
#endregion

Engine/ScriptAnalyzer.cs

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public void Initilaize(Dictionary<string, List<string>> result)
142142
paths = result.ContainsKey("ValidDllPaths") ? result["ValidDllPaths"] : result["ValidPaths"];
143143
foreach (string path in paths)
144144
{
145-
if (Path.GetExtension(path).ToLower(CultureInfo.CurrentCulture) == ".dll")
145+
if (String.Equals(Path.GetExtension(path),".dll",StringComparison.OrdinalIgnoreCase))
146146
{
147147
catalog.Catalogs.Add(new AssemblyCatalog(path));
148148
}
@@ -241,8 +241,8 @@ public List<ExternalRule> GetExternalRule(string[] moduleNames)
241241

242242
FunctionInfo funcInfo = (FunctionInfo)psobject.ImmediateBaseObject;
243243
ParameterMetadata param = funcInfo.Parameters.Values
244-
.First<ParameterMetadata>(item => item.Name.ToLower(CultureInfo.CurrentCulture).EndsWith("ast", StringComparison.CurrentCulture) ||
245-
item.Name.ToLower(CultureInfo.CurrentCulture).EndsWith("token", StringComparison.CurrentCulture));
244+
.First<ParameterMetadata>(item => item.Name.EndsWith("ast", StringComparison.OrdinalIgnoreCase) ||
245+
item.Name.EndsWith("token", StringComparison.OrdinalIgnoreCase));
246246

247247
//Only add functions that are defined as rules.
248248
if (param != null)
@@ -251,7 +251,7 @@ public List<ExternalRule> GetExternalRule(string[] moduleNames)
251251
string desc =posh.AddScript(script).Invoke()[0].ImmediateBaseObject.ToString()
252252
.Replace("\r\n", " ").Trim();
253253

254-
rules.Add(new ExternalRule(funcInfo.Name, funcInfo.Name, desc, param.Name,
254+
rules.Add(new ExternalRule(funcInfo.Name, funcInfo.Name, desc, param.Name,param.ParameterType.FullName,
255255
funcInfo.ModuleName, funcInfo.Module.Path));
256256
}
257257
}
@@ -289,13 +289,13 @@ public IEnumerable<DiagnosticRecord> GetExternalRecord(Ast ast, Token[] token, E
289289

290290
// Groups rules by AstType and Tokens.
291291
Dictionary<string, List<ExternalRule>> astRuleGroups = rules
292-
.Where<ExternalRule>(item => item.GetParameter().EndsWith("ast", true, CultureInfo.CurrentCulture))
293-
.GroupBy<ExternalRule, string>(item => item.GetParameter())
292+
.Where<ExternalRule>(item => item.GetParameter().EndsWith("ast", StringComparison.OrdinalIgnoreCase))
293+
.GroupBy<ExternalRule, string>(item => item.GetParameterType())
294294
.ToDictionary(item => item.Key, item => item.ToList());
295295

296296
Dictionary<string, List<ExternalRule>> tokenRuleGroups = rules
297-
.Where<ExternalRule>(item => item.GetParameter().EndsWith("token", true, CultureInfo.CurrentCulture))
298-
.GroupBy<ExternalRule, string>(item => item.GetParameter())
297+
.Where<ExternalRule>(item => item.GetParameter().EndsWith("token", StringComparison.OrdinalIgnoreCase))
298+
.GroupBy<ExternalRule, string>(item => item.GetParameterType())
299299
.ToDictionary(item => item.Key, item => item.ToList());
300300

301301
using (rsp)
@@ -337,7 +337,7 @@ public IEnumerable<DiagnosticRecord> GetExternalRecord(Ast ast, Token[] token, E
337337
{
338338
// Find all AstTypes that appeared in rule groups.
339339
IEnumerable<Ast> childAsts = ast.FindAll(new Func<Ast, bool>((testAst) =>
340-
(testAst.GetType().Name.ToLower(CultureInfo.CurrentCulture) == astRuleGroup.Key.ToLower(CultureInfo.CurrentCulture))), false);
340+
(astRuleGroup.Key.IndexOf(testAst.GetType().FullName,StringComparison.OrdinalIgnoreCase) != -1)), false);
341341

342342
foreach (Ast childAst in childAsts)
343343
{
@@ -365,49 +365,63 @@ public IEnumerable<DiagnosticRecord> GetExternalRecord(Ast ast, Token[] token, E
365365
}
366366

367367
#endregion
368-
369368
#region Collects the results from commands.
370-
371-
for (int i = 0; i < powerShellCommands.Count; i++)
369+
List<DiagnosticRecord> diagnostics = new List<DiagnosticRecord>();
370+
try
372371
{
373-
// EndInvoke will wait for each command to finish, so we will be getting the commands
374-
// in the same order that they have been invoked withy BeginInvoke.
375-
PSDataCollection<PSObject> psobjects = powerShellCommands[i].EndInvoke(powerShellCommandResults[i]);
376-
377-
foreach (var psobject in psobjects)
372+
for (int i = 0; i < powerShellCommands.Count; i++)
378373
{
379-
DiagnosticSeverity severity;
380-
IScriptExtent extent;
381-
string message = string.Empty;
382-
string ruleName = string.Empty;
383-
384-
// Because error stream is merged to output stream,
385-
// we need to handle the error records.
386-
if (psobject.ImmediateBaseObject is ErrorRecord)
387-
{
388-
ErrorRecord record = (ErrorRecord)psobject.ImmediateBaseObject;
389-
command.WriteError(record);
390-
continue;
391-
}
374+
// EndInvoke will wait for each command to finish, so we will be getting the commands
375+
// in the same order that they have been invoked withy BeginInvoke.
376+
PSDataCollection<PSObject> psobjects = powerShellCommands[i].EndInvoke(powerShellCommandResults[i]);
392377

393-
// DiagnosticRecord may not be correctly returned from external rule.
394-
try
378+
foreach (var psobject in psobjects)
395379
{
396-
Enum.TryParse<DiagnosticSeverity>(psobject.Properties["Severity"].Value.ToString().ToUpper(), out severity);
397-
message = psobject.Properties["Message"].Value.ToString();
398-
extent = (IScriptExtent)psobject.Properties["Extent"].Value;
399-
ruleName = psobject.Properties["RuleName"].Value.ToString();
380+
DiagnosticSeverity severity;
381+
IScriptExtent extent;
382+
string message = string.Empty;
383+
string ruleName = string.Empty;
384+
385+
if (psobject != null && psobject.ImmediateBaseObject != null)
386+
{
387+
// Because error stream is merged to output stream,
388+
// we need to handle the error records.
389+
if (psobject.ImmediateBaseObject is ErrorRecord)
390+
{
391+
ErrorRecord record = (ErrorRecord)psobject.ImmediateBaseObject;
392+
command.WriteError(record);
393+
continue;
394+
}
395+
396+
// DiagnosticRecord may not be correctly returned from external rule.
397+
try
398+
{
399+
Enum.TryParse<DiagnosticSeverity>(psobject.Properties["Severity"].Value.ToString().ToUpper(), out severity);
400+
message = psobject.Properties["Message"].Value.ToString();
401+
extent = (IScriptExtent)psobject.Properties["Extent"].Value;
402+
ruleName = psobject.Properties["RuleName"].Value.ToString();
403+
}
404+
catch (Exception ex)
405+
{
406+
command.WriteError(new ErrorRecord(ex, ex.HResult.ToString("X"), ErrorCategory.NotSpecified, this));
407+
continue;
408+
}
409+
410+
if (!string.IsNullOrEmpty(message))
411+
{
412+
diagnostics.Add(new DiagnosticRecord(message, extent, ruleName, severity, null));
413+
}
414+
}
400415
}
401-
catch (Exception ex)
402-
{
403-
command.WriteError(new ErrorRecord(ex, ex.HResult.ToString("X"), ErrorCategory.NotSpecified, this));
404-
continue;
405-
}
406-
407-
if (!string.IsNullOrEmpty(message)) yield return new DiagnosticRecord(message, extent, ruleName, severity, null);
408416
}
409417
}
418+
//Catch exception where customized defined rules have exceptins when doing invoke
419+
catch(Exception ex)
420+
{
421+
command.WriteError(new ErrorRecord(ex, ex.HResult.ToString("X"), ErrorCategory.NotSpecified, this));
422+
}
410423

424+
return diagnostics;
411425
#endregion
412426
}
413427
}
@@ -478,7 +492,7 @@ public Dictionary<string, List<string>> CheckRuleExtension(string[] path, PSCmdl
478492

479493
cmdlet.WriteDebug(string.Format(CultureInfo.CurrentCulture, Strings.CheckAssemblyFile, resolvedPath));
480494

481-
if (Path.GetExtension(resolvedPath).ToLower(CultureInfo.CurrentCulture) == ".dll")
495+
if (String.Equals(Path.GetExtension(resolvedPath),".dll", StringComparison.OrdinalIgnoreCase))
482496
{
483497
if (!File.Exists(resolvedPath))
484498
{
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Import-Module PSScriptAnalyzer
2+
$directory = Split-Path -Parent $MyInvocation.MyCommand.Path
3+
$message = "this is help"
4+
$measure = "Measure-RequiresRunAsAdministrator"
5+
6+
Describe "Test importing customized rules with null return results" {
7+
Context "Test Get-ScriptAnalyzer with customized rules" {
8+
It "will not terminate the engine" {
9+
$customizedRulePath = Get-ScriptAnalyzerRule -CustomizedRulePath $directory\samplerule\SampleRulesWithErrors.psm1 | Where-Object {$_.RuleName -eq $measure}
10+
$customizedRulePath.Count | Should Be 1
11+
}
12+
13+
}
14+
15+
Context "Test Invoke-ScriptAnalyzer with customized rules" {
16+
It "will not terminate the engine" {
17+
$customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -CustomizedRulePath $directory\samplerule\SampleRulesWithErrors.psm1 | Where-Object {$_.RuleName -eq $measure}
18+
$customizedRulePath.Count | Should Be 0
19+
}
20+
}
21+
22+
}
23+
24+
Describe "Test importing correct customized rules" {
25+
Context "Test Get-ScriptAnalyzer with customized rules" {
26+
It "will show the customized rule" {
27+
$customizedRulePath = Get-ScriptAnalyzerRule -CustomizedRulePath $directory\samplerule\samplerule.psm1 | Where-Object {$_.RuleName -eq $measure}
28+
$customizedRulePath.Count | Should Be 1
29+
}
30+
31+
}
32+
33+
Context "Test Invoke-ScriptAnalyzer with customized rules" {
34+
It "will show the customized rule in the results" {
35+
$customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -CustomizedRulePath $directory\samplerule\samplerule.psm1 | Where-Object {$_.Message -eq $message}
36+
$customizedRulePath.Count | Should Be 1
37+
}
38+
}
39+
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#Requires -Version 3.0
2+
3+
<#
4+
.SYNOPSIS
5+
Uses #Requires -RunAsAdministrator instead of your own methods.
6+
.DESCRIPTION
7+
The #Requires statement prevents a script from running unless the Windows PowerShell version, modules, snap-ins, and module and snap-in version prerequisites are met.
8+
From Windows PowerShell 4.0, the #Requires statement let script developers require that sessions be run with elevated user rights (run as Administrator).
9+
Script developers does not need to write their own methods any more.
10+
To fix a violation of this rule, please consider to use #Requires -RunAsAdministrator instead of your own methods.
11+
.EXAMPLE
12+
Measure-RequiresRunAsAdministrator -ScriptBlockAst $ScriptBlockAst
13+
.INPUTS
14+
[System.Management.Automation.Language.ScriptBlockAst]
15+
.OUTPUTS
16+
[OutputType([PSCustomObject[])]
17+
.NOTES
18+
None
19+
#>
20+
function Measure-RequiresRunAsAdministrator
21+
{
22+
[CmdletBinding()]
23+
[OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
24+
Param
25+
(
26+
[Parameter(Mandatory = $true)]
27+
[ValidateNotNullOrEmpty()]
28+
[System.Management.Automation.Language.ScriptBlockAst]
29+
$ScriptBlockAst
30+
)
31+
32+
33+
$results = @()
34+
35+
$results += $null
36+
return $results
37+
38+
39+
}
40+
Export-ModuleMember -Function Measure*
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#Requires -Version 3.0
2+
3+
<#
4+
.SYNOPSIS
5+
Uses #Requires -RunAsAdministrator instead of your own methods.
6+
.DESCRIPTION
7+
The #Requires statement prevents a script from running unless the Windows PowerShell version, modules, snap-ins, and module and snap-in version prerequisites are met.
8+
From Windows PowerShell 4.0, the #Requires statement let script developers require that sessions be run with elevated user rights (run as Administrator).
9+
Script developers does not need to write their own methods any more.
10+
To fix a violation of this rule, please consider to use #Requires -RunAsAdministrator instead of your own methods.
11+
.EXAMPLE
12+
Measure-RequiresRunAsAdministrator -ScriptBlockAst $ScriptBlockAst
13+
.INPUTS
14+
[System.Management.Automation.Language.ScriptBlockAst]
15+
.OUTPUTS
16+
[OutputType([PSCustomObject[])]
17+
.NOTES
18+
None
19+
#>
20+
function Measure-RequiresRunAsAdministrator
21+
{
22+
[CmdletBinding()]
23+
[OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
24+
Param
25+
(
26+
[Parameter(Mandatory = $true)]
27+
[ValidateNotNullOrEmpty()]
28+
[System.Management.Automation.Language.ScriptBlockAst]
29+
$testAst
30+
)
31+
32+
33+
$results = @()
34+
35+
$result = [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]]@{"Message" = "this is help";
36+
"Extent" = $ast.Extent;
37+
"RuleName" = $PSCmdlet.MyInvocation.InvocationName;
38+
"Severity" = "Warning"}
39+
40+
$results += $result
41+
42+
43+
return $results
44+
45+
46+
}
47+
Export-ModuleMember -Function Measure*

0 commit comments

Comments
 (0)