Skip to content

Commit a154270

Browse files
MJVLbergmeistersdwheeler
authored
Add AvoidUsingBrokenHashAlgorithms (#1787)
* Add AvoidUsingBrokenHashAlgorithms rule * Add AvoidUsingBrokenHashAlgorithms strings * Add AvoidUsingBrokenhashAlgorithms test suite * Fix tests and improve style * Add AvoidUsingBrokenHashAlgorithms to docs * Fix severity and unit tests Co-authored-by: Christoph Bergmeister [MVP] <c.bergmeister@gmail.com> * Fix error severity count in unit test * Adding frontmatter * Update Tests/Engine/GetScriptAnalyzerRule.tests.ps1 Co-authored-by: Christoph Bergmeister [MVP] <c.bergmeister@gmail.com> Co-authored-by: Sean Wheeler <sean.wheeler@microsoft.com>
1 parent d7a9fa8 commit a154270

7 files changed

+333
-1
lines changed

Diff for: Rules/AvoidUsingBrokenHashAlgorithms.cs

+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Management.Automation.Language;
6+
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
7+
#if !CORECLR
8+
using System.ComponentModel.Composition;
9+
#endif
10+
using System.Globalization;
11+
using System.Linq;
12+
13+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
14+
{
15+
/// <summary>
16+
/// AvoidUsingBrokenHashAlgorithms: Avoid using the broken algorithms MD5 or SHA-1.
17+
/// </summary>
18+
#if !CORECLR
19+
[Export(typeof(IScriptRule))]
20+
#endif
21+
public class AvoidUsingBrokenHashAlgorithms : AvoidParameterGeneric
22+
{
23+
private readonly string[] brokenAlgorithms = new string[]
24+
{
25+
"MD5",
26+
"SHA1",
27+
};
28+
29+
private string algorithm;
30+
31+
/// <summary>
32+
/// Condition on the cmdlet that must be satisfied for the error to be raised
33+
/// </summary>
34+
/// <param name="CmdAst"></param>
35+
/// <returns></returns>
36+
public override bool CommandCondition(CommandAst CmdAst)
37+
{
38+
return true;
39+
}
40+
41+
/// <summary>
42+
/// Condition on the parameter that must be satisfied for the error to be raised.
43+
/// </summary>
44+
/// <param name="CmdAst"></param>
45+
/// <param name="CeAst"></param>
46+
/// <returns></returns>
47+
public override bool ParameterCondition(CommandAst CmdAst, CommandElementAst CeAst)
48+
{
49+
if (CeAst is CommandParameterAst)
50+
{
51+
CommandParameterAst cmdParamAst = CeAst as CommandParameterAst;
52+
53+
if (String.Equals(cmdParamAst.ParameterName, "algorithm", StringComparison.OrdinalIgnoreCase))
54+
{
55+
Ast hashAlgorithmArgument = cmdParamAst.Argument;
56+
if (hashAlgorithmArgument is null)
57+
{
58+
hashAlgorithmArgument = GetHashAlgorithmArg(CmdAst, cmdParamAst.Extent.StartOffset);
59+
if (hashAlgorithmArgument is null)
60+
{
61+
return false;
62+
}
63+
}
64+
65+
var constExprAst = hashAlgorithmArgument as ConstantExpressionAst;
66+
if (constExprAst != null)
67+
{
68+
algorithm = constExprAst.Value as string;
69+
return IsBrokenAlgorithm(algorithm);
70+
}
71+
}
72+
}
73+
74+
return false;
75+
}
76+
77+
private bool IsBrokenAlgorithm(string algorithm)
78+
{
79+
if (algorithm != null)
80+
{
81+
return brokenAlgorithms.Contains<string>(
82+
algorithm,
83+
StringComparer.OrdinalIgnoreCase);
84+
}
85+
86+
return false;
87+
}
88+
89+
private Ast GetHashAlgorithmArg(CommandAst CmdAst, int StartIndex)
90+
{
91+
int small = int.MaxValue;
92+
Ast hashAlgorithmArg = null;
93+
foreach (Ast ast in CmdAst.CommandElements)
94+
{
95+
if (ast.Extent.StartOffset > StartIndex && ast.Extent.StartOffset < small)
96+
{
97+
hashAlgorithmArg = ast;
98+
small = ast.Extent.StartOffset;
99+
}
100+
}
101+
102+
return hashAlgorithmArg;
103+
}
104+
105+
/// <summary>
106+
/// Retrieves the error message
107+
/// </summary>
108+
/// <param name="FileName"></param>
109+
/// <param name="CmdAst"></param>
110+
/// <returns></returns>
111+
public override string GetError(string FileName, CommandAst CmdAst)
112+
{
113+
if (CmdAst is null)
114+
{
115+
return string.Empty;
116+
}
117+
118+
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingBrokenHashAlgorithmsError, CmdAst.GetCommandName(), algorithm);
119+
}
120+
121+
/// <summary>
122+
/// GetName: Retrieves the name of this rule.
123+
/// </summary>
124+
/// <returns>The name of this rule</returns>
125+
public override string GetName()
126+
{
127+
return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.AvoidUsingBrokenHashAlgorithmsName);
128+
}
129+
130+
/// <summary>
131+
/// GetCommonName: Retrieves the common name of this rule.
132+
/// </summary>
133+
/// <returns>The common name of this rule</returns>
134+
public override string GetCommonName()
135+
{
136+
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingBrokenHashAlgorithmsCommonName);
137+
}
138+
139+
/// <summary>
140+
/// GetDescription: Retrieves the description of this rule.
141+
/// </summary>
142+
/// <returns>The description of this rule</returns>
143+
public override string GetDescription()
144+
{
145+
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingBrokenHashAlgorithmsDescription);
146+
}
147+
148+
/// <summary>
149+
/// GetSourceType: Retrieves the type of the rule: builtin, managed or module.
150+
/// </summary>
151+
public override SourceType GetSourceType()
152+
{
153+
return SourceType.Builtin;
154+
}
155+
156+
/// <summary>
157+
/// GetSeverity: Retrieves the severity of the rule: error, warning or information.
158+
/// </summary>
159+
/// <returns></returns>
160+
public override RuleSeverity GetSeverity()
161+
{
162+
return RuleSeverity.Warning;
163+
}
164+
165+
/// <summary>
166+
/// DiagnosticSeverity: Retrieves the severity of the rule of type DiagnosticSeverity: error, warning or information.
167+
/// </summary>
168+
/// <returns></returns>
169+
public override DiagnosticSeverity GetDiagnosticSeverity()
170+
{
171+
return DiagnosticSeverity.Warning;
172+
}
173+
174+
/// <summary>
175+
/// GetSourceName: Retrieves the module/assembly name the rule is from.
176+
/// </summary>
177+
public override string GetSourceName()
178+
{
179+
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
180+
}
181+
}
182+
}
183+
184+
185+
186+

Diff for: Rules/Strings.Designer.cs

+36
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Rules/Strings.resx

+12
Original file line numberDiff line numberDiff line change
@@ -1176,4 +1176,16 @@
11761176
<data name="AvoidMultipleTypeAttributesName" xml:space="preserve">
11771177
<value>AvoidMultipleTypeAttributes</value>
11781178
</data>
1179+
<data name="AvoidUsingBrokenHashAlgorithmsCommonName" xml:space="preserve">
1180+
<value>Avoid Using Broken Hash Algorithms</value>
1181+
</data>
1182+
<data name="AvoidUsingBrokenHashAlgorithmsDescription" xml:space="preserve">
1183+
<value>Avoid using the broken algorithms MD5 or SHA-1.</value>
1184+
</data>
1185+
<data name="AvoidUsingBrokenHashAlgorithmsError" xml:space="preserve">
1186+
<value>The Algorithm parameter of cmdlet '{0}' was used with the broken algorithm '{1}'.</value>
1187+
</data>
1188+
<data name="AvoidUsingBrokenHashAlgorithmsName" xml:space="preserve">
1189+
<value>AvoidUsingBrokenHashAlgorithms</value>
1190+
</data>
11791191
</root>

Diff for: Tests/Engine/GetScriptAnalyzerRule.tests.ps1

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Describe "Test Name parameters" {
6363

6464
It "get Rules with no parameters supplied" {
6565
$defaultRules = Get-ScriptAnalyzerRule
66-
$expectedNumRules = 67
66+
$expectedNumRules = 68
6767
if ($PSVersionTable.PSVersion.Major -le 4)
6868
{
6969
# for PSv3 PSAvoidGlobalAliases is not shipped because

Diff for: Tests/Rules/AvoidUsingBrokenHashAlgorithms.tests.ps1

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
BeforeAll {
5+
$settings = @{
6+
IncludeRules = @('PSAvoidUsingBrokenHashAlgorithms')
7+
Rules = @{
8+
PSAvoidUsingBrokenHashAlgorithms = @{
9+
Enable = $true
10+
}
11+
}
12+
}
13+
14+
$violationMessageMD5 = [regex]::Escape("The Algorithm parameter of cmdlet 'Get-FileHash' was used with the broken algorithm 'MD5'.")
15+
$violationMessageSHA1 = [regex]::Escape("The Algorithm parameter of cmdlet 'Get-FileHash' was used with the broken algorithm 'SHA1'.")
16+
}
17+
18+
Describe "AvoidUsingBrokenHashAlgorithms" {
19+
Context "When there are violations" {
20+
It "detects broken hash algorithms violations" {
21+
(Invoke-ScriptAnalyzer -ScriptDefinition 'Get-FileHash foo -Algorithm MD5' -Settings $settings).Count | Should -Be 1
22+
(Invoke-ScriptAnalyzer -ScriptDefinition 'Get-FileHash foo -Algorithm SHA1' -Settings $settings).Count | Should -Be 1
23+
}
24+
25+
It "has the correct description message for MD5" {
26+
(Invoke-ScriptAnalyzer -ScriptDefinition 'Get-FileHash foo -Algorithm MD5' -Settings $settings).Message | Should -Match $violationMessageMD5
27+
}
28+
29+
It "has the correct description message for SHA-1" {
30+
(Invoke-ScriptAnalyzer -ScriptDefinition 'Get-FileHash foo -Algorithm SHA1' -Settings $settings).Message | Should -Match $violationMessageSHA1
31+
}
32+
33+
It "detects arbitrary cmdlets" {
34+
(Invoke-ScriptAnalyzer -ScriptDefinition 'Fake-Cmdlet foo -Algorithm MD5' -Settings $settings).Count | Should -Be 1
35+
(Invoke-ScriptAnalyzer -ScriptDefinition 'Fake-Cmdlet foo -Algorithm SHA1' -Settings $settings).Count | Should -Be 1
36+
}
37+
38+
}
39+
40+
Context "When there are no violations" {
41+
It "does not flag default algorithm" {
42+
(Invoke-ScriptAnalyzer -ScriptDefinition 'Get-FileHash foo' -Settings $settings).Count | Should -Be 0
43+
}
44+
45+
It "does not flag safe algorithms" {
46+
(Invoke-ScriptAnalyzer -ScriptDefinition 'Get-FileHash foo -Algorithm SHA256' -Settings $settings).Count | Should -Be 0
47+
(Invoke-ScriptAnalyzer -ScriptDefinition 'Get-FileHash foo -Algorithm SHA384' -Settings $settings).Count | Should -Be 0
48+
(Invoke-ScriptAnalyzer -ScriptDefinition 'Get-FileHash foo -Algorithm SHA512' -Settings $settings).Count | Should -Be 0
49+
}
50+
}
51+
}

Diff for: docs/Rules/AvoidUsingBrokenHashAlgorithms.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
description: Cmdlet Verbs
3+
ms.custom: PSSA v1.21.0
4+
ms.date: 05/31/2022
5+
ms.topic: reference
6+
title: AvoidUsingBrokenHashAlgorithms
7+
---
8+
# AvoidUsingBrokenHashAlgorithms
9+
10+
**Severity Level: Warning**
11+
12+
## Description
13+
14+
Avoid using the broken algorithms MD5 or SHA-1.
15+
16+
## How
17+
18+
Replace broken algorithms with secure alternatives. MD5 and SHA-1 should be replaced with SHA256, SHA384, SHA512, or other safer algorithms when possible, with MD5 and SHA-1 only being utilized by necessity for backwards compatibility.
19+
20+
## Example 1
21+
22+
### Wrong
23+
24+
```powershell
25+
Get-FileHash foo.txt -Algorithm MD5
26+
```
27+
28+
### Correct
29+
30+
```powershell
31+
Get-FileHash foo.txt -Algorithm SHA256
32+
```
33+
34+
## Example 2
35+
36+
### Wrong
37+
38+
```powershell
39+
Get-FileHash foo.txt -Algorithm SHA1
40+
```
41+
42+
### Correct
43+
44+
```powershell
45+
Get-FileHash foo.txt
46+
```

Diff for: docs/Rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The PSScriptAnalyzer contains the following rule definitions.
2626
| [AvoidSemicolonsAsLineTerminators](./AvoidSemicolonsAsLineTerminators.md) | Warning | No | |
2727
| [AvoidShouldContinueWithoutForce](./AvoidShouldContinueWithoutForce.md) | Warning | Yes | |
2828
| [AvoidTrailingWhitespace](./AvoidTrailingWhitespace.md) | Warning | Yes | |
29+
| [AvoidUsingBrokenHashAlgorithms](./AvoidUsingBrokenHashAlgorithms.md) | Warning | Yes | |
2930
| [AvoidUsingCmdletAliases](./AvoidUsingCmdletAliases.md) | Warning | Yes | Yes<sup>2</sup> |
3031
| [AvoidUsingComputerNameHardcoded](./AvoidUsingComputerNameHardcoded.md) | Error | Yes | |
3132
| [AvoidUsingConvertToSecureStringWithPlainText](./AvoidUsingConvertToSecureStringWithPlainText.md) | Error | Yes | |

0 commit comments

Comments
 (0)