Skip to content

Commit 586302f

Browse files
committed
Add new AvoidSecretDisclosure rule
1 parent a143b9f commit 586302f

5 files changed

Lines changed: 460 additions & 0 deletions

File tree

Rules/AvoidSecretDisclosure.cs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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.Management.Automation.Language;
11+
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
12+
13+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
14+
{
15+
/// <summary>
16+
/// AvoidSecretDisclosure: Checks whether a plaintext secret is being retrieved which can lead to
17+
/// security vulnerabilities such as memory trails or logging trails.
18+
/// The general approach of dealing with credentials is to avoid them and instead rely on other means
19+
/// to authenticate, such as certificates or Windows authentication.
20+
/// </summary>
21+
#if !CORECLR
22+
[Export(typeof(IScriptRule))]
23+
#endif
24+
public class AvoidSecretDisclosure : ConfigurableRule
25+
{
26+
27+
/// <summary>
28+
/// Construct an object of AvoidSecretDisclosure type.
29+
/// </summary>
30+
public AvoidSecretDisclosure() {
31+
Enable = true;
32+
}
33+
34+
/// <summary>
35+
/// Analyzes the given ast to find the [violation]
36+
/// </summary>
37+
/// <param name="ast">AST to be analyzed. This should be non-null</param>
38+
/// <param name="fileName">Name of file that corresponds to the input AST.</param>
39+
/// <returns>A an enumerable type containing the violations</returns>
40+
public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
41+
{
42+
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);
43+
44+
// Check for ConvertFrom-SecureString with -AsPlainText parameter
45+
IEnumerable<Ast> convertFromSecureStringAsts = ast.FindAll(testAst =>
46+
testAst is CommandAst cmdAst &&
47+
cmdAst.GetCommandName() != null &&
48+
cmdAst.GetCommandName().Equals("ConvertFrom-SecureString", StringComparison.OrdinalIgnoreCase),
49+
true
50+
);
51+
52+
foreach (CommandAst cmdAst in convertFromSecureStringAsts)
53+
{
54+
// Use StaticParameterBinder to reliably get parameter values
55+
var bindingResult = StaticParameterBinder.BindCommand(cmdAst, true);
56+
57+
// Check for -AsPlainText parameter
58+
if (
59+
bindingResult.BoundParameters.ContainsKey("AsPlainText") &&
60+
(bool)bindingResult.BoundParameters["AsPlainText"].ConstantValue == true
61+
) {
62+
yield return GetDiagnosticRecord(cmdAst.Extent, fileName, "AsPlainText");
63+
}
64+
}
65+
66+
// Check for any invocation of a method that starts with "SecureStringTo"
67+
// (e.g. SecureStringToBSTR, SecureStringToCoTaskMemAnsi, etc.)
68+
IEnumerable<Ast> secureStringToAsts = ast.FindAll(testAst =>
69+
testAst is InvokeMemberExpressionAst invokeAst &&
70+
invokeAst.Member != null &&
71+
invokeAst.Member.ToString().StartsWith("SecureStringTo", StringComparison.OrdinalIgnoreCase),
72+
true
73+
);
74+
75+
foreach (InvokeMemberExpressionAst secureStringToAst in secureStringToAsts) {
76+
yield return GetDiagnosticRecord(secureStringToAst.Extent, fileName, secureStringToAst.Member.ToString());
77+
}
78+
79+
// Check for any member access of a property named "Password".
80+
// Note that this is a heuristic and may lead to false positives,
81+
// as not all properties named "Password" necessarily contain secrets,
82+
// and there may be secrets stored in properties with other names.
83+
// However, it is too complex to reliably determine whether a Password
84+
// property is a result of e.g. a PSCredential.GetNetworkCredential() call.
85+
// Anyways, this is still a useful common check to have.
86+
IEnumerable<Ast> passwordAsts = ast.FindAll(testAst =>
87+
testAst is MemberExpressionAst memberAst &&
88+
memberAst.Member != null &&
89+
string.Equals(memberAst.Member.ToString(), "Password", StringComparison.OrdinalIgnoreCase),
90+
true
91+
);
92+
93+
foreach (MemberExpressionAst passwordAst in passwordAsts) {
94+
yield return GetDiagnosticRecord(passwordAst.Extent, fileName, passwordAst.Member.ToString());
95+
}
96+
97+
}
98+
99+
/// <summary>
100+
/// Helper function to create a DiagnosticRecord for a given violation
101+
/// </summary>
102+
private DiagnosticRecord GetDiagnosticRecord(IScriptExtent Extent, string fileName, string suppressionId)
103+
{
104+
return new DiagnosticRecord(
105+
string.Format(
106+
CultureInfo.CurrentCulture,
107+
Strings.AvoidSecretDisclosureError,
108+
Extent.Text),
109+
Extent,
110+
GetName(),
111+
DiagnosticSeverity.Warning,
112+
fileName,
113+
suppressionId
114+
);
115+
}
116+
117+
/// <summary>
118+
/// Retrieves the common name of this rule.
119+
/// </summary>
120+
public override string GetCommonName()
121+
{
122+
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidSecretDisclosureCommonName);
123+
}
124+
125+
/// <summary>
126+
/// Retrieves the description of this rule.
127+
/// </summary>
128+
public override string GetDescription()
129+
{
130+
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidSecretDisclosureDescription);
131+
}
132+
133+
/// <summary>
134+
/// Retrieves the name of this rule.
135+
/// </summary>
136+
public override string GetName()
137+
{
138+
return string.Format(
139+
CultureInfo.CurrentCulture,
140+
Strings.NameSpaceFormat,
141+
GetSourceName(),
142+
Strings.AvoidSecretDisclosureName);
143+
}
144+
145+
/// <summary>
146+
/// Retrieves the severity of the rule: error, warning or information.
147+
/// </summary>
148+
public override RuleSeverity GetSeverity()
149+
{
150+
return RuleSeverity.Warning;
151+
}
152+
153+
/// <summary>
154+
/// Gets the severity of the returned diagnostic record: error, warning, or information.
155+
/// </summary>
156+
/// <returns></returns>
157+
public DiagnosticSeverity GetDiagnosticSeverity()
158+
{
159+
return DiagnosticSeverity.Warning;
160+
}
161+
162+
/// <summary>
163+
/// Retrieves the name of the module/assembly the rule is from.
164+
/// </summary>
165+
public override string GetSourceName()
166+
{
167+
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
168+
}
169+
170+
/// <summary>
171+
/// Retrieves the type of the rule, Builtin, Managed or Module.
172+
/// </summary>
173+
public override SourceType GetSourceType()
174+
{
175+
return SourceType.Builtin;
176+
}
177+
}
178+
}
179+

Rules/Strings.resx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,18 @@
921921
<data name="AvoidLongLinesError" xml:space="preserve">
922922
<value>Line exceeds the configured maximum length of {0} characters</value>
923923
</data>
924+
<data name="AvoidSecretDisclosureName" xml:space="preserve">
925+
<value>AvoidSecretDisclosure</value>
926+
</data>
927+
<data name="AvoidSecretDisclosureCommonName" xml:space="preserve">
928+
<value>Avoid secret disclosure</value>
929+
</data>
930+
<data name="AvoidSecretDisclosureDescription" xml:space="preserve">
931+
<value>Disclosing a secret might result in security vulnerabilities such as memory trails or logging trails</value>
932+
</data>
933+
<data name="AvoidSecretDisclosureError" xml:space="preserve">
934+
<value>Avoid disclosing a secret</value>
935+
</data>
924936
<data name="AvoidSemicolonsAsLineTerminatorsName" xml:space="preserve">
925937
<value>AvoidSemicolonsAsLineTerminators</value>
926938
</data>

0 commit comments

Comments
 (0)