From 19f6fdb945e838b6861ae6399260d6f10595b3a7 Mon Sep 17 00:00:00 2001 From: Pavel Mikula <57188685+pavel-mikula-sonarsource@users.noreply.github.com> Date: Wed, 12 Oct 2022 08:45:06 +0200 Subject: [PATCH] Rule S2068: detect hard-coded passwords in web.config files (#6182) --- .../Rules/DatabasePasswordsShouldBeSecure.cs | 2 +- .../Helpers/SonarAnalysisContext.cs | 2 +- .../SonarAnalyzer.Common/Helpers/XmlHelper.cs | 16 +- .../DisablingRequestValidationBase.cs | 2 +- .../Hotspots/DoNotHardcodeCredentialsBase.cs | 164 +++++++++++------- .../RequestsWithExcessiveLengthBase.cs | 2 +- .../Common/SecurityHotspotTest.cs | 2 +- .../Hotspots/DoNotHardcodeCredentialsTest.cs | 90 ++++++---- ... DoNotHardcodeCredentials.CustomValues.cs} | 0 ... DoNotHardcodeCredentials.CustomValues.vb} | 0 ...codeCredentials.DefaultValues.CSharp10.cs} | 0 ...dcodeCredentials.DefaultValues.CSharp8.cs} | 0 ...DoNotHardcodeCredentials.DefaultValues.cs} | 15 +- ...DoNotHardcodeCredentials.DefaultValues.vb} | 2 +- .../Corrupt/Web.config | 5 + .../UnexpectedContent/Web.config | 9 + .../Valid/Web.Custom.config | 5 + .../Valid/Web.Debug.config | 6 + .../Valid/Web.Release.config | 9 + .../DoNotHardcodeCredentials/Valid/Web.config | 57 ++++++ 20 files changed, 273 insertions(+), 115 deletions(-) rename analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/{DoNotHardcodeCredentials_CustomValues.cs => DoNotHardcodeCredentials.CustomValues.cs} (100%) rename analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/{DoNotHardcodeCredentials_CustomValues.vb => DoNotHardcodeCredentials.CustomValues.vb} (100%) rename analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/{DoNotHardcodeCredentials_DefaultValues.CSharp10.cs => DoNotHardcodeCredentials.DefaultValues.CSharp10.cs} (100%) rename analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/{DoNotHardcodeCredentials_DefaultValues.CSharp8.cs => DoNotHardcodeCredentials.DefaultValues.CSharp8.cs} (100%) rename analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/{DoNotHardcodeCredentials_DefaultValues.cs => DoNotHardcodeCredentials.DefaultValues.cs} (96%) rename analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/{DoNotHardcodeCredentials_DefaultValues.vb => DoNotHardcodeCredentials.DefaultValues.vb} (99%) create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Corrupt/Web.config create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/UnexpectedContent/Web.config create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.Custom.config create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.Debug.config create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.Release.config create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.config diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/DatabasePasswordsShouldBeSecure.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/DatabasePasswordsShouldBeSecure.cs index 7eaa1166705..4324b21b85e 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/DatabasePasswordsShouldBeSecure.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/DatabasePasswordsShouldBeSecure.cs @@ -80,7 +80,7 @@ protected override void Initialize(SonarAnalysisContext context) private void CheckWebConfig(SonarAnalysisContext context, CompilationAnalysisContext c) { - foreach (var fullPath in context.GetWebConfig(c)) + foreach (var fullPath in context.WebConfigFiles(c)) { var webConfig = File.ReadAllText(fullPath); if (webConfig.Contains("") && XmlHelper.ParseXDocument(webConfig) is { } doc) diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/SonarAnalysisContext.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/SonarAnalysisContext.cs index cac20f366a3..a530a883a5c 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/SonarAnalysisContext.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/SonarAnalysisContext.cs @@ -140,7 +140,7 @@ internal void RegisterSyntaxNodeAction(Action RegisterContextAction(x => context.RegisterSyntaxNodeAction(x, syntaxKinds), action, c => c.GetSyntaxTree(), c => c.Compilation, c => c.Options); - internal IEnumerable GetWebConfig(CompilationAnalysisContext c) + internal IEnumerable WebConfigFiles(CompilationAnalysisContext c) { return ProjectConfiguration(c.Options).FilesToAnalyze.FindFiles(WebConfigRegex).Where(ShouldProcess); diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/XmlHelper.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/XmlHelper.cs index 3a0ee4945d1..e3e5bd6c92a 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/XmlHelper.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/XmlHelper.cs @@ -46,19 +46,27 @@ public static XDocument ParseXDocument(string text) ? attribute : null; - public static Location CreateLocation(this XAttribute attribute, string path) + public static Location CreateLocation(this XAttribute attribute, string path) => + CreateLocation(attribute, path, attribute.Name); + + public static Location CreateLocation(this XElement element, string path) => + CreateLocation(element, path, element.Name); + + private static Location CreateLocation(IXmlLineInfo startPos, string path, XName name) { // IXmlLineInfo is 1-based, whereas Roslyn is zero-based - var startPos = (IXmlLineInfo)attribute; if (startPos.HasLineInfo()) { // LoadOptions.PreserveWhitespace doesn't preserve whitespace inside nodes and attributes => there's no easy way to find full length of a XAttribute. - var length = attribute.Name.ToString().Length; + var length = name.ToString().Length; var start = new LinePosition(startPos.LineNumber - 1, startPos.LinePosition - 1); var end = new LinePosition(startPos.LineNumber - 1, startPos.LinePosition - 1 + length); return Location.Create(path, new TextSpan(start.Line, length), new LinePositionSpan(start, end)); } - return null; + else + { + return null; + } } } } diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Hotspots/DisablingRequestValidationBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Hotspots/DisablingRequestValidationBase.cs index 749a31c8a0b..7a2b1bb2c28 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Hotspots/DisablingRequestValidationBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Hotspots/DisablingRequestValidationBase.cs @@ -87,7 +87,7 @@ private void CheckWebConfig(SonarAnalysisContext context, CompilationAnalysisCon return; } - foreach (var fullPath in context.GetWebConfig(c)) + foreach (var fullPath in context.WebConfigFiles(c)) { var webConfig = File.ReadAllText(fullPath); if (webConfig.Contains("") && XmlHelper.ParseXDocument(webConfig) is { } doc) diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Hotspots/DoNotHardcodeCredentialsBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Hotspots/DoNotHardcodeCredentialsBase.cs index 538f9df1d71..5d51c820c4f 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Hotspots/DoNotHardcodeCredentialsBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Hotspots/DoNotHardcodeCredentialsBase.cs @@ -21,8 +21,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Text.RegularExpressions; +using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using SonarAnalyzer.Common; @@ -43,6 +45,8 @@ public abstract class DoNotHardcodeCredentialsBase : ParameterLoadi private readonly IAnalyzerConfiguration configuration; private readonly DiagnosticDescriptor rule; + private readonly Regex validCredentialPattern = new(@"^(\?|:\w+|\{\d+[^}]*\}|""|')$", RegexOptions.IgnoreCase); + private readonly Regex uriUserInfoPattern; private string credentialWords; private IEnumerable splitCredentialWords; private Regex passwordValuePattern; @@ -71,6 +75,13 @@ public string CredentialWords protected DoNotHardcodeCredentialsBase(IAnalyzerConfiguration configuration) { + const string Rfc3986_Unreserved = "-._~"; // Numbers and letters are embedded in regex itself without escaping + const string Rfc3986_Pct = "%"; + const string Rfc3986_SubDelims = "!$&'()*+,;="; + const string UriPasswordSpecialCharacters = Rfc3986_Unreserved + Rfc3986_Pct + Rfc3986_SubDelims; + // See https://tools.ietf.org/html/rfc3986 Userinfo can contain groups: unreserved | pct-encoded | sub-delims + var uriUserInfoPart = @"[\w\d" + Regex.Escape(UriPasswordSpecialCharacters) + "]+"; + uriUserInfoPattern = new Regex(@"\w+:\/\/(?" + uriUserInfoPart + "):(?" + uriUserInfoPart + ")@", RegexOptions.Compiled); this.configuration = configuration; rule = Language.CreateDescriptor(DiagnosticId, MessageFormat); CredentialWords = DefaultCredentialWords; // Property will initialize multiple state variables @@ -98,6 +109,7 @@ protected sealed override void Initialize(ParameterLoadingAnalysisContext contex pa.MatchProperty(new MemberDescriptor(KnownType.System_Net_NetworkCredential, "Password"))); InitializeActions(context); + context.Context.RegisterCompilationAction(c => CheckWebConfig(context.Context, c)); } protected bool IsEnabled(AnalyzerOptions options) @@ -106,29 +118,102 @@ protected bool IsEnabled(AnalyzerOptions options) return configuration.IsEnabled(DiagnosticId); } + private void CheckWebConfig(SonarAnalysisContext context, CompilationAnalysisContext c) + { + foreach (var path in context.WebConfigFiles(c)) + { + if (XmlHelper.ParseXDocument(File.ReadAllText(path)) is { } doc) + { + CheckWebConfig(c, path, doc.Descendants()); + } + } + } + + private void CheckWebConfig(CompilationAnalysisContext c, string path, IEnumerable elements) + { + foreach (var element in elements) + { + if (!element.HasElements && IssueMessage(element.Name.LocalName, element.Value) is { } elementMessage && element.CreateLocation(path) is { } elementLocation) + { + c.ReportIssue(Diagnostic.Create(rule, elementLocation, elementMessage)); + } + foreach (var attribute in element.Attributes()) + { + if (IssueMessage(attribute.Name.LocalName, attribute.Value) is { } attributeMessage && attribute.CreateLocation(path) is { } attributeLocation) + { + c.ReportIssue(Diagnostic.Create(rule, attributeLocation, attributeMessage)); + } + } + } + } + + private string IssueMessage(string variableName, string variableValue) + { + var bannedWords = FindCredentialWords(variableName, variableValue); + if (bannedWords.Any()) + { + return string.Format(MessageFormatCredential, bannedWords.JoinAnd()); + } + else if (ContainsUriUserInfo(variableValue)) + { + return MessageUriUserInfo; + } + else + { + return null; + } + } + + private IEnumerable FindCredentialWords(string variableName, string variableValue) + { + var credentialWordsFound = variableName + .SplitCamelCaseToWords() + .Intersect(splitCredentialWords) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + if (credentialWordsFound.Any(x => variableValue.IndexOf(x, StringComparison.InvariantCultureIgnoreCase) >= 0)) + { + // See https://github.com/SonarSource/sonar-dotnet/issues/2868 + return Enumerable.Empty(); + } + + var match = passwordValuePattern.Match(variableValue); + if (match.Success && !IsValidCredential(match.Groups["suffix"].Value)) + { + credentialWordsFound.Add(match.Groups["credential"].Value); + } + + // Rule was initially implemented with everything lower (which is wrong) so we have to force lower before reporting to avoid new issues to appear on SQ/SC. + return credentialWordsFound.Select(x => x.ToLowerInvariant()); + } + + private bool IsValidCredential(string suffix) + { + var candidateCredential = suffix.Split(CredentialSeparator).First().Trim(); + return string.IsNullOrWhiteSpace(candidateCredential) || validCredentialPattern.IsMatch(candidateCredential); + } + + private bool ContainsUriUserInfo(string variableValue) + { + var match = uriUserInfoPattern.Match(variableValue); + return match.Success + && match.Groups["Password"].Value is { } password + && !string.Equals(match.Groups["Login"].Value, password, StringComparison.OrdinalIgnoreCase) + && password != CredentialSeparator.ToString() + && !validCredentialPattern.IsMatch(password); + } + protected abstract class CredentialWordsFinderBase where TSyntaxNode : SyntaxNode { - private readonly Regex validCredentialPattern = new(@"^\?|:\w+|\{\d+[^}]*\}|""|'$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private readonly Regex uriUserInfoPattern; private readonly DoNotHardcodeCredentialsBase analyzer; protected abstract bool ShouldHandle(TSyntaxNode syntaxNode, SemanticModel semanticModel); protected abstract string GetVariableName(TSyntaxNode syntaxNode); protected abstract string GetAssignedValue(TSyntaxNode syntaxNode, SemanticModel semanticModel); - protected CredentialWordsFinderBase(DoNotHardcodeCredentialsBase analyzer) - { - // See https://tools.ietf.org/html/rfc3986 Userinfo can contain groups: unreserved | pct-encoded | sub-delims - const string Rfc3986_Unreserved = "-._~"; // Numbers and letters are embeded in regex itself without escaping - const string Rfc3986_Pct = "%"; - const string Rfc3986_SubDelims = "!$&'()*+,;="; - const string UriPasswordSpecialCharacters = Rfc3986_Unreserved + Rfc3986_Pct + Rfc3986_SubDelims; - var uriUserInfoPart = @"[\w\d" + Regex.Escape(UriPasswordSpecialCharacters) + "]+"; - + protected CredentialWordsFinderBase(DoNotHardcodeCredentialsBase analyzer) => this.analyzer = analyzer; - uriUserInfoPattern = new Regex(@"\w+:\/\/(?" + uriUserInfoPart + "):(?" + uriUserInfoPart + ")@", RegexOptions.Compiled); - } public Action AnalysisAction() => context => @@ -136,59 +221,12 @@ protected CredentialWordsFinderBase(DoNotHardcodeCredentialsBase an var declarator = (TSyntaxNode)context.Node; if (ShouldHandle(declarator, context.SemanticModel) && GetAssignedValue(declarator, context.SemanticModel) is { } variableValue - && !string.IsNullOrWhiteSpace(variableValue)) + && !string.IsNullOrWhiteSpace(variableValue) + && analyzer.IssueMessage(GetVariableName(declarator), variableValue) is { } message) { - var bannedWords = FindCredentialWords(GetVariableName(declarator), variableValue); - if (bannedWords.Any()) - { - context.ReportIssue(Diagnostic.Create(analyzer.rule, declarator.GetLocation(), string.Format(MessageFormatCredential, bannedWords.JoinAnd()))); - } - else if (ContainsUriUserInfo(variableValue)) - { - context.ReportIssue(Diagnostic.Create(analyzer.rule, declarator.GetLocation(), MessageUriUserInfo)); - } + context.ReportIssue(Diagnostic.Create(analyzer.rule, declarator.GetLocation(), message)); } }; - - private IEnumerable FindCredentialWords(string variableName, string variableValue) - { - var credentialWordsFound = variableName - .SplitCamelCaseToWords() - .Intersect(analyzer.splitCredentialWords) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - if (credentialWordsFound.Any(x => variableValue.IndexOf(x, StringComparison.InvariantCultureIgnoreCase) >= 0)) - { - // See https://github.com/SonarSource/sonar-dotnet/issues/2868 - return Enumerable.Empty(); - } - - var match = analyzer.passwordValuePattern.Match(variableValue); - if (match.Success && !IsValidCredential(match.Groups["suffix"].Value)) - { - credentialWordsFound.Add(match.Groups["credential"].Value); - } - - // Rule was initially implemented with everything lower (which is wrong) so we have to force lower - // before reporting to avoid new issues to appear on SQ/SC. - return credentialWordsFound.Select(x => x.ToLowerInvariant()); - } - - private bool IsValidCredential(string suffix) - { - var candidateCredential = suffix.Split(CredentialSeparator).First().Trim(); - return string.IsNullOrWhiteSpace(candidateCredential) || validCredentialPattern.IsMatch(candidateCredential); - } - - private bool ContainsUriUserInfo(string variableValue) - { - var match = uriUserInfoPattern.Match(variableValue); - return match.Success - && match.Groups["Password"].Value is { } password - && !string.Equals(match.Groups["Login"].Value, password, StringComparison.OrdinalIgnoreCase) - && password != CredentialSeparator.ToString() - && !validCredentialPattern.IsMatch(password); - } } } } diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/Hotspots/RequestsWithExcessiveLengthBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/Hotspots/RequestsWithExcessiveLengthBase.cs index b85d76917f3..04fdbfe28bd 100644 --- a/analyzers/src/SonarAnalyzer.Common/Rules/Hotspots/RequestsWithExcessiveLengthBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/Rules/Hotspots/RequestsWithExcessiveLengthBase.cs @@ -147,7 +147,7 @@ private bool IsEnabled(AnalyzerOptions options) private void CheckWebConfig(SonarAnalysisContext context, CompilationAnalysisContext c) { - foreach (var fullPath in context.GetWebConfig(c)) + foreach (var fullPath in context.WebConfigFiles(c)) { var webConfig = File.ReadAllText(fullPath); if (webConfig.Contains(" "ConfiguringLoggers_Log4Net", "CookieShouldBeHttpOnly" => "CookieShouldBeHttpOnly_Nancy", "CookieShouldBeSecure" => "CookieShouldBeSecure_Nancy", - "DoNotHardcodeCredentials" => "DoNotHardcodeCredentials_DefaultValues", + "DoNotHardcodeCredentials" => "DoNotHardcodeCredentials.DefaultValues", "DeliveringDebugFeaturesInProduction" => "DeliveringDebugFeaturesInProduction.NetCore2", #if NETFRAMEWORK "ExecutingSqlQueries" => "ExecutingSqlQueries_Net46", diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/Hotspots/DoNotHardcodeCredentialsTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/Hotspots/DoNotHardcodeCredentialsTest.cs index 43d4d134ab9..5b3470afb82 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/Hotspots/DoNotHardcodeCredentialsTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/Hotspots/DoNotHardcodeCredentialsTest.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.IO; using SonarAnalyzer.Common; using CS = SonarAnalyzer.Rules.CSharp; using VB = SonarAnalyzer.Rules.VisualBasic; @@ -27,84 +28,101 @@ namespace SonarAnalyzer.UnitTest.Rules [TestClass] public class DoNotHardcodeCredentialsTest { - private readonly VerifierBuilder builderCS = new VerifierBuilder().AddAnalyzer(() => new CS.DoNotHardcodeCredentials(AnalyzerConfiguration.AlwaysEnabled)); - private readonly VerifierBuilder builderVB = new VerifierBuilder().AddAnalyzer(() => new VB.DoNotHardcodeCredentials(AnalyzerConfiguration.AlwaysEnabled)); + private readonly VerifierBuilder builderCS = CreateVerifierCS(); + private readonly VerifierBuilder builderVB = CreateVerifierVB(); [TestMethod] public void DoNotHardcodeCredentials_CS_DefaultValues() => - builderCS.AddPaths(@"Hotspots\DoNotHardcodeCredentials_DefaultValues.cs") - .AddReferences(AdditionalReferences) - .Verify(); + builderCS.AddPaths("DoNotHardcodeCredentials.DefaultValues.cs").Verify(); [TestMethod] public void DoNotHardcodeCredentials_CSharp8_DefaultValues() => - builderCS.AddPaths(@"Hotspots\DoNotHardcodeCredentials_DefaultValues.CSharp8.cs") - .AddReferences(AdditionalReferences) - .WithOptions(ParseOptionsHelper.FromCSharp8) - .Verify(); + builderCS.AddPaths("DoNotHardcodeCredentials.DefaultValues.CSharp8.cs").WithOptions(ParseOptionsHelper.FromCSharp8).Verify(); #if NET [TestMethod] public void DoNotHardcodeCredentials_CSharp10_DefaultValues() => - builderCS.AddPaths(@"Hotspots\DoNotHardcodeCredentials_DefaultValues.CSharp10.cs") - .AddReferences(AdditionalReferences) - .WithOptions(ParseOptionsHelper.FromCSharp10) - .Verify(); + builderCS.AddPaths("DoNotHardcodeCredentials.DefaultValues.CSharp10.cs").WithOptions(ParseOptionsHelper.FromCSharp10).Verify(); #endif [TestMethod] public void DoNotHardcodeCredentials_CS_CustomValues() => - new VerifierBuilder().AddAnalyzer(() => new CS.DoNotHardcodeCredentials(AnalyzerConfiguration.AlwaysEnabled) { CredentialWords = @"kode,facal-faire,*,x\*+?|}{][)(^$.# " }) - .AddPaths(@"Hotspots\DoNotHardcodeCredentials_CustomValues.cs") - .AddReferences(AdditionalReferences) + CreateVerifierCS(@"kode,facal-faire,*,x\*+?|}{][)(^$.# ") + .AddPaths("DoNotHardcodeCredentials.CustomValues.cs") .Verify(); [TestMethod] public void DoNotHardcodeCredentials_CS_CustomValues_CaseInsensitive() => - new VerifierBuilder().AddAnalyzer(() => new CS.DoNotHardcodeCredentials(AnalyzerConfiguration.AlwaysEnabled) { CredentialWords = @"KODE ,,,, FaCaL-FaIrE, x\*+?|}{][)(^$.# " }) - .AddPaths(@"Hotspots\DoNotHardcodeCredentials_CustomValues.cs") - .AddReferences(AdditionalReferences) + CreateVerifierCS(@"KODE ,,,, FaCaL-FaIrE, x\*+?|}{][)(^$.# ") + .AddPaths("DoNotHardcodeCredentials.CustomValues.cs") .Verify(); + [TestMethod] + public void DoNotHardcodeCredentials_CS_WebConfig() => + DoNotHardcodeCredentials_WebConfig(AnalyzerLanguage.CSharp, new CS.DoNotHardcodeCredentials()); + [TestMethod] public void DoNotHardcodeCredentials_VB_DefaultValues() => - builderVB.AddPaths(@"Hotspots\DoNotHardcodeCredentials_DefaultValues.vb") - .AddReferences(AdditionalReferences) - .Verify(); + builderVB.AddPaths("DoNotHardcodeCredentials.DefaultValues.vb").Verify(); [TestMethod] public void DoNotHardcodeCredentials_VB_CustomValues() => - new VerifierBuilder().AddAnalyzer(() => new VB.DoNotHardcodeCredentials(AnalyzerConfiguration.AlwaysEnabled) { CredentialWords = @"kode,facal-faire,*,x\*+?|}{][)(^$.# " }) - .AddPaths(@"Hotspots\DoNotHardcodeCredentials_CustomValues.vb") - .AddReferences(AdditionalReferences) + CreateVerifierVB(@"kode,facal-faire,*,x\*+?|}{][)(^$.# ") + .AddPaths("DoNotHardcodeCredentials.CustomValues.vb") .Verify(); [TestMethod] public void DoNotHardcodeCredentials_VB_CustomValues_CaseInsensitive() => - new VerifierBuilder().AddAnalyzer(() => new VB.DoNotHardcodeCredentials(AnalyzerConfiguration.AlwaysEnabled) { CredentialWords = @"KODE ,,,, FaCaL-FaIrE,x\*+?|}{][)(^$.# " }) - .AddPaths(@"Hotspots\DoNotHardcodeCredentials_CustomValues.vb") - .AddReferences(AdditionalReferences) + CreateVerifierVB(@"KODE ,,,, FaCaL-FaIrE,x\*+?|}{][)(^$.# ") + .AddPaths("DoNotHardcodeCredentials.CustomValues.vb") .Verify(); + [TestMethod] + public void DoNotHardcodeCredentials_VB_WebConfig() => + DoNotHardcodeCredentials_WebConfig(AnalyzerLanguage.VisualBasic, new VB.DoNotHardcodeCredentials()); + [TestMethod] public void DoNotHardcodeCredentials_ConfiguredCredentialsAreRead() { - var cs = new CS.DoNotHardcodeCredentials - { - CredentialWords = "Lorem, ipsum" - }; + var cs = new CS.DoNotHardcodeCredentials { CredentialWords = "Lorem, ipsum" }; cs.CredentialWords.Should().Be("Lorem, ipsum"); - var vb = new CS.DoNotHardcodeCredentials - { - CredentialWords = "Lorem, ipsum" - }; + var vb = new CS.DoNotHardcodeCredentials { CredentialWords = "Lorem, ipsum" }; vb.CredentialWords.Should().Be("Lorem, ipsum"); } internal static IEnumerable AdditionalReferences => MetadataReferenceFacade.SystemSecurityCryptography.Concat(MetadataReferenceFacade.SystemNetHttp); + + private static VerifierBuilder CreateVerifierCS(string credentialWords = null) => + new VerifierBuilder().AddAnalyzer(() => credentialWords is null + ? new CS.DoNotHardcodeCredentials(AnalyzerConfiguration.AlwaysEnabled) + : new CS.DoNotHardcodeCredentials(AnalyzerConfiguration.AlwaysEnabled) { CredentialWords = credentialWords }) + .WithBasePath("Hotspots") + .AddReferences(AdditionalReferences); + + private static VerifierBuilder CreateVerifierVB(string credentialWords = null) => + new VerifierBuilder().AddAnalyzer(() => credentialWords is null + ? new VB.DoNotHardcodeCredentials(AnalyzerConfiguration.AlwaysEnabled) + : new VB.DoNotHardcodeCredentials(AnalyzerConfiguration.AlwaysEnabled) { CredentialWords = credentialWords }) + .WithBasePath("Hotspots") + .AddReferences(AdditionalReferences); + + private static void DoNotHardcodeCredentials_WebConfig(AnalyzerLanguage language, DiagnosticAnalyzer analyzer) + { + var root = @"TestCases\WebConfig\DoNotHardcodeCredentials"; + var webConfigPaths = Directory.GetFiles(root, "*.config", SearchOption.AllDirectories); + webConfigPaths.Should().HaveCount(6); + var compilation = CreateCompilation(language); + foreach (var webConfigPath in webConfigPaths) + { + DiagnosticVerifier.VerifyExternalFile(compilation, analyzer, webConfigPath, TestHelper.CreateSonarProjectConfig(root, TestHelper.CreateFilesToAnalyze(root, webConfigPath))); + } + } + + private static Compilation CreateCompilation(AnalyzerLanguage language) => + SolutionBuilder.Create().AddProject(language).GetCompilation(); } } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_CustomValues.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.CustomValues.cs similarity index 100% rename from analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_CustomValues.cs rename to analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.CustomValues.cs diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_CustomValues.vb b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.CustomValues.vb similarity index 100% rename from analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_CustomValues.vb rename to analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.CustomValues.vb diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_DefaultValues.CSharp10.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.DefaultValues.CSharp10.cs similarity index 100% rename from analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_DefaultValues.CSharp10.cs rename to analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.DefaultValues.CSharp10.cs diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_DefaultValues.CSharp8.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.DefaultValues.CSharp8.cs similarity index 100% rename from analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_DefaultValues.CSharp8.cs rename to analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.DefaultValues.CSharp8.cs diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_DefaultValues.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.DefaultValues.cs similarity index 96% rename from analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_DefaultValues.cs rename to analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.DefaultValues.cs index 4b8db4b0d40..8ebd34c28b7 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_DefaultValues.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.DefaultValues.cs @@ -7,7 +7,7 @@ namespace Tests.Diagnostics { class Program { - public const string DBConnectionString = "Server=localhost; Database=Test; User=SA; Password=Secret123"; // Noncompliant + public const string DBConnectionString = "Server=localhost; Database=Test; User=SA; Password=Secret123"; // Noncompliant {{"password" detected here, make sure this is not a hard-coded credential.}} public const string EditPasswordPageUrlToken = "{Account.EditPassword.PageUrl}"; // Compliant private const string secretConst = "constantValue"; @@ -84,8 +84,11 @@ public void DefaultKeywords() public void Constants() { - const string ConnectionString = "Server=localhost; Database=Test; User=SA; Password=Secret123"; // Noncompliant - const string ConnectionStringWithSpaces = "Server=localhost; Database=Test; User=SA; Password = Secret123"; // Noncompliant + const string ConnectionString = "Server=localhost; Database=Test; User=SA; Password=Secret123"; // Noncompliant + const string ConnectionStringWithSpaces = "Server=localhost; Database=Test; User=SA; Password = Secret123"; // Noncompliant + const string inTheMiddle = "Server=localhost; Database=Test; User=SA; Password=Secret123; Application Name=Sonar"; // Noncompliant + const string withSemicolon = @"Server=localhost; Database=Test; User=SA; Password=""Secret;'123"""; // Noncompliant + const string withApostroph = @"Server=localhost; Database=Test; User=SA; Password='Secret""123"; // Noncompliant const string Password = "Secret123"; // Noncompliant const string LoginName = "Admin"; @@ -113,8 +116,8 @@ public void Concatenations(string arg) a = "Server = localhost; Database = Test; User = SA; Password = " + secretVariableMethod; // Compliant, not initialized to constant a = "Server = localhost; Database = Test; User = SA; Password = " + arg; // Compliant, not initialized to constant - const string passwordPrefixConst = "Password = "; // Compliant by it's name - var passwordPrefixVariable = "Password = "; // Compliant by it's name + const string passwordPrefixConst = "Password = "; // Compliant by its name + var passwordPrefixVariable = "Password = "; // Compliant by its name a = "Server = localhost;" + " Database = Test; User = SA; Password = " + secretConst; // Noncompliant a = "Server = localhost;" + " Database = Test; User = SA; Pa" + "ssword = " + secretConst; // FN, we don't track all concatenations to avoid duplications a = "Server = localhost;" + " Database = Test; User = SA; " + passwordPrefixConst + secretConst; // Noncompliant @@ -386,7 +389,7 @@ public void Foo(string user) this.password = "foo"; // False Negative Configuration.Password = "foo"; // False Negative this.password = Configuration.Password = "foo"; // False Negative - string query1 = "password=':crazy;secret';user=xxx"; // False Negative - passwords enclosed in '' are not covered + string query1 = "password=':crazy;secret';user=xxx"; // Noncompliant } class Configuration diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_DefaultValues.vb b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.DefaultValues.vb similarity index 99% rename from analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_DefaultValues.vb rename to analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.DefaultValues.vb index b1ae61d9167..c0eb80023d6 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials_DefaultValues.vb +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/Hotspots/DoNotHardcodeCredentials.DefaultValues.vb @@ -364,7 +364,7 @@ Phone: +0000000" Me.password = "foo" ' False Negative Configuration.Password = "foo" ' False Negative Me.password = Configuration.Password = "foo" ' False Negative - Dim query1 As String = "password=':crazy;secret';user=xxx" ' False Negative - passwords enclosed in '' are not covered + Dim query1 As String = "password=':crazy;secret';user=xxx" ' Noncompliant End Sub Class Configuration diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Corrupt/Web.config b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Corrupt/Web.config new file mode 100644 index 00000000000..519f9ad73cb --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Corrupt/Web.config @@ -0,0 +1,5 @@ + + + + + <<< Corrupted, we don't raise here \ No newline at end of file diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/UnexpectedContent/Web.config b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/UnexpectedContent/Web.config new file mode 100644 index 00000000000..8c4e692b13d --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/UnexpectedContent/Web.config @@ -0,0 +1,9 @@ + + 4.0.0 + + com.mycompany.app + my-app + 1.0-SNAPSHOT + + diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.Custom.config b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.Custom.config new file mode 100644 index 00000000000..43ac610e225 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.Custom.config @@ -0,0 +1,5 @@ + + + + + diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.Debug.config b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.Debug.config new file mode 100644 index 00000000000..0c54bf651c4 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.Debug.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.Release.config b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.Release.config new file mode 100644 index 00000000000..0b46d4f0f61 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.Release.config @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.config b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.config new file mode 100644 index 00000000000..aac186c0121 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/WebConfig/DoNotHardcodeCredentials/Valid/Web.config @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Password=Secret42 + 42 + 42 + + + 42 + + + + 42 + + +