Skip to content

Commit

Permalink
MA0153 Do not log symbols decorated with DataClassificationAttribute …
Browse files Browse the repository at this point in the history
…directly
  • Loading branch information
meziantou committed Dec 30, 2023
1 parent dd96912 commit b06406f
Show file tree
Hide file tree
Showing 9 changed files with 596 additions and 215 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0150](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0150.md)|Design|Do not call the default object.ToString explicitly|⚠️|✔️||
|[MA0151](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0151.md)|Usage|DebuggerDisplay must contain valid members|⚠️|✔️||
|[MA0152](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0152.md)|Performance|Use Unwrap instead of using await twice|ℹ️|✔️||
|[MA0153](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0153.md)|Design|Do not log symbols decorated with DataClassificationAttribute directly|⚠️|✔️||

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
|[MA0150](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0150.md)|Design|Do not call the default object.ToString explicitly|<span title='Warning'>⚠️</span>|✔️||
|[MA0151](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0151.md)|Usage|DebuggerDisplay must contain valid members|<span title='Warning'>⚠️</span>|✔️||
|[MA0152](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0152.md)|Performance|Use Unwrap instead of using await twice|<span title='Info'>ℹ️</span>|✔️||
|[MA0153](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0153.md)|Design|Do not log symbols decorated with DataClassificationAttribute directly|<span title='Warning'>⚠️</span>|✔️||

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -614,6 +615,9 @@ dotnet_diagnostic.MA0151.severity = warning
# MA0152: Use Unwrap instead of using await twice
dotnet_diagnostic.MA0152.severity = suggestion
# MA0153: Do not log symbols decorated with DataClassificationAttribute directly
dotnet_diagnostic.MA0153.severity = warning
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1071,4 +1075,7 @@ dotnet_diagnostic.MA0151.severity = none
# MA0152: Use Unwrap instead of using await twice
dotnet_diagnostic.MA0152.severity = none
# MA0153: Do not log symbols decorated with DataClassificationAttribute directly
dotnet_diagnostic.MA0153.severity = none
```
26 changes: 26 additions & 0 deletions docs/Rules/MA0153.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# MA0153 - Do not log symbols decorated with DataClassificationAttribute directly

Detects when a log parameter is decorated with an attribute that inherits from `Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute`.
Most of the time, these values should not be used with `[LogProperties]` to redact values.

````c#
using Microsoft.Extensions.Logging;

ILogger logger;

// non-compliant as Prop is decorated with an attribute that inherits from DataClassificationAttribute
logger.LogInformation("{Prop}", new Dummy().Prop);

class Dummy
{
[PiiAttribute]
public string Prop { get; set; }
}

class PiiAttribute : Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute
{
public TaxonomyAttribute() : base(default)
{
}
}
````
5 changes: 4 additions & 1 deletion src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,12 @@ public static bool IsOrImplements(this ITypeSymbol symbol, ITypeSymbol? interfac

foreach (var attribute in symbol.GetAttributes())
{
if (attribute.AttributeClass is null)
continue;

if (inherits)
{
if (attributeType.IsOrInheritFrom(attribute.AttributeClass))
if (attribute.AttributeClass.IsOrInheritFrom(attributeType))
return attribute;
}
else
Expand Down
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ internal static class RuleIdentifiers
public const string DoNotUseToStringIfObject = "MA0150";
public const string DebuggerDisplayAttributeShouldContainValidExpressions = "MA0151";
public const string UseTaskUnwrap = "MA0152";
public const string DoNotLogClassifiedData = "MA0153";

public static string GetHelpUri(string identifier)
{
Expand Down
159 changes: 159 additions & 0 deletions src/Meziantou.Analyzer/Rules/DoNotLogClassifiedDataAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DoNotLogClassifiedDataAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.DoNotLogClassifiedData,
title: "Do not log symbols decorated with DataClassificationAttribute directly",
messageFormat: "Do not log symbols decorated with DataClassificationAttribute directly",
RuleCategories.Design,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.DoNotLogClassifiedData));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.RegisterCompilationStartAction(context =>
{
var ctx = new AnalyzerContext(context.Compilation);
if (!ctx.IsValid)
return;

context.RegisterOperationAction(ctx.AnalyzeInvocationDeclaration, OperationKind.Invocation);
});
}

private sealed class AnalyzerContext
{
public AnalyzerContext(Compilation compilation)
{
LoggerSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger");
if (LoggerSymbol is null)
return;

LoggerExtensionsSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.LoggerExtensions");
LoggerMessageSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.LoggerMessage");
StructuredLogFieldAttributeSymbol = compilation.GetBestTypeByMetadataName("Meziantou.Analyzer.Annotations.StructuredLogFieldAttribute");

DataClassificationAttributeSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute");
}

public INamedTypeSymbol? StructuredLogFieldAttributeSymbol { get; private set; }

public INamedTypeSymbol? LoggerSymbol { get; }
public INamedTypeSymbol? LoggerExtensionsSymbol { get; }
public INamedTypeSymbol? LoggerMessageSymbol { get; }

public INamedTypeSymbol? DataClassificationAttributeSymbol { get; }

public bool IsValid => DataClassificationAttributeSymbol is not null && LoggerSymbol is not null;

public void AnalyzeInvocationDeclaration(OperationAnalysisContext context)
{
var operation = (IInvocationOperation)context.Operation;
if (operation.TargetMethod.ContainingType.IsEqualTo(LoggerExtensionsSymbol) && FindLogParameters(operation.TargetMethod, out var argumentsParameter))
{
foreach (var argument in operation.Arguments)
{
var parameter = argument.Parameter;
if (parameter is null)
continue;

if (parameter.Equals(argumentsParameter, SymbolEqualityComparer.Default))
{
if (argument.ArgumentKind == ArgumentKind.ParamArray && argument.Value is IArrayCreationOperation arrayCreation && arrayCreation.Initializer is not null)
{
ValidateDataClassification(context, arrayCreation.Initializer.ElementValues);
}
}
}
}
}

private void ValidateDataClassification(DiagnosticReporter diagnosticReporter, IEnumerable<IOperation> operations)
{
foreach (var operation in operations)
{
ValidateDataClassification(diagnosticReporter, operation);
}
}

private void ValidateDataClassification(DiagnosticReporter diagnosticReporter, IOperation operation)
{
ValidateDataClassification(diagnosticReporter, operation, operation, DataClassificationAttributeSymbol!);

static void ValidateDataClassification(DiagnosticReporter diagnosticReporter, IOperation operation, IOperation reportOperation, INamedTypeSymbol dataClassificationAttributeSymbol)
{
operation = operation.UnwrapConversionOperations();
if (operation is IParameterReferenceOperation parameterReferenceOperation)
{
if (parameterReferenceOperation.Parameter.HasAttribute(dataClassificationAttributeSymbol, inherits: true))
{
diagnosticReporter.ReportDiagnostic(Rule, reportOperation);
}
}
else if (operation is IPropertyReferenceOperation propertyReferenceOperation)
{
if (propertyReferenceOperation.Property.HasAttribute(dataClassificationAttributeSymbol, inherits: true))
{
diagnosticReporter.ReportDiagnostic(Rule, reportOperation);
}
}
else if (operation is IFieldReferenceOperation fieldReferenceOperation)
{
if (fieldReferenceOperation.Field.HasAttribute(dataClassificationAttributeSymbol, inherits: true))
{
diagnosticReporter.ReportDiagnostic(Rule, reportOperation);
}
}
else if (operation is IArrayElementReferenceOperation arrayElementReferenceOperation)
{
ValidateDataClassification(diagnosticReporter, arrayElementReferenceOperation.ArrayReference, reportOperation, dataClassificationAttributeSymbol);
}
}
}

private static bool FindLogParameters(IMethodSymbol methodSymbol, out IParameterSymbol? arguments)
{
IParameterSymbol? message = null;
arguments = null;
foreach (var parameter in methodSymbol.Parameters)
{
if (parameter.Type.IsString() &&
(string.Equals(parameter.Name, "message", StringComparison.Ordinal) ||
string.Equals(parameter.Name, "messageFormat", StringComparison.Ordinal) ||
string.Equals(parameter.Name, "formatString", StringComparison.Ordinal)))
{
message = parameter;
}
// When calling logger.BeginScope("{Param}") generic overload would be selected
else if (parameter.Type.SpecialType == SpecialType.System_String &&
methodSymbol.Name is "BeginScope" &&
parameter.Name is "state")
{
message = parameter;
}
else if (parameter.IsParams &&
parameter.Name is "args")
{
arguments = parameter;
}
}

return message is not null;
}
}
}
Loading

0 comments on commit b06406f

Please sign in to comment.