Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send assembly references to Telemetry #493

Merged
merged 3 commits into from
Sep 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/BinSkim.Rules/PERules/BA4001.ReportPECompilerData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public override void AnalyzePortableExecutableAndPdb(BinaryAnalyzerContext conte
FileVersion = target.PE.FileVersion?.FileVersion,
CompilerBackEndVersion = target.PE.LinkerVersion.ToString(),
CompilerFrontEndVersion = target.PE.LinkerVersion.ToString(),
AssemblyReferences = string.Join(';', target.PE.GetAssemblyReferenceStrings()),
};

if (!records.ContainsKey(record))
Expand Down
3 changes: 2 additions & 1 deletion src/BinSkim.Sdk/CompilerData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ public struct CompilerData
public string DebuggingFileGuid { get; set; }
public string CompilerBackEndVersion { get; set; }
public string CompilerFrontEndVersion { get; set; }
public string AssemblyReferences { get; set; }

public override string ToString()
{
return $"{CompilerName},{CompilerBackEndVersion},{CompilerFrontEndVersion},{FileVersion},{BinaryType},{Language},{DebuggingFileName},{DebuggingFileGuid},{CommandLine},{Dialect},{ModuleName},{(ModuleLibrary == ModuleName ? string.Empty : ModuleLibrary)}";
return $"{CompilerName},{CompilerBackEndVersion},{CompilerFrontEndVersion},{FileVersion},{BinaryType},{Language},{DebuggingFileName},{DebuggingFileGuid},{CommandLine},{Dialect},{ModuleName},{(ModuleLibrary == ModuleName ? string.Empty : ModuleLibrary)},{AssemblyReferences}";
}
}
}
65 changes: 45 additions & 20 deletions src/BinSkim.Sdk/CompilerDataLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ namespace Microsoft.CodeAnalysis.IL.Sdk
{
public class CompilerDataLogger
{
private const int ChunkSize = 8192;
private const string CompilerEventName = "CompilerInformation";
private const string AssemblyReferencesEventName = "AssemblyReferencesInformation";
private const string CommandLineEventName = "CommandLineInformation";
private const string CompilerEventName = "CompilerInformation";
private const string SummaryEventName = "AnalysisSummary";
Copy link
Contributor

@eddynaka eddynaka Sep 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SummaryEventName

move this to the first, so we will have ordered by length #Closed


private readonly int ChunkSize;
private readonly bool appInsightsRegistered;
private readonly string sha256;
private readonly string relativeFilePath;
Expand All @@ -32,20 +33,31 @@ public class CompilerDataLogger
public static bool TelemetryEnabled => s_telemetryClient != null;

public CompilerDataLogger(IAnalysisContext analysisContext,
IEnumerable<string> targetFileSpecifiers)
IEnumerable<string> targetFileSpecifiers,
TelemetryConfiguration telemetryConfiguration = null,
TelemetryClient telemetryClient = null,
int chunkSize = 8192)
{
try
s_telemetryConfiguration = telemetryConfiguration;
s_telemetryClient = telemetryClient;
s_sessionId = TelemetryEnabled ? Guid.NewGuid().ToString() : null;
ChunkSize = chunkSize;

if (!TelemetryEnabled)
Copy link
Collaborator

@shaopeng-gh shaopeng-gh Sep 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!TelemetryEnabled)

question, why we add this new condition #Closed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the code in below block is to initialize a TelemetryClient instance to be used to send telemetry data.
if caller already passed in the TelemetryClient instance in constructor, we don't need to initialize a new instance.
Added the TelemetryClient / Configuration as constructor parameter so that we can overwrite the built-in instances. The unit tests can pass in mock client/configuration for testing purpose.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, since we are creating the compilerdatalogger for each object, we dont need to keep creating an instance of the telemetry.

with that, once it is initialized, it won't create again

{
string appInsightsKey = RetrieveAppInsightsKey();
if (!string.IsNullOrEmpty(appInsightsKey) && Guid.TryParse(appInsightsKey, out _))
try
{
Initialize(appInsightsKey);
this.appInsightsRegistered = true;
string appInsightsKey = RetrieveAppInsightsKey();
if (!string.IsNullOrEmpty(appInsightsKey) && Guid.TryParse(appInsightsKey, out _))
{
Initialize(appInsightsKey);
this.appInsightsRegistered = true;
}
}
catch (SecurityException)
{
// User does not have access to retrieve information from environment variables.
}
}
catch (SecurityException)
{
// User does not have access to retrieve information from environment variables.
}

this.sha256 = analysisContext?.Hashes?.Sha256 ?? string.Empty;
Expand Down Expand Up @@ -126,6 +138,7 @@ public void Write(CompilerData compilerData)
if (TelemetryEnabled)
{
string commandLineId = string.Empty;
string assemblyReferencesId = string.Empty;
var properties = new Dictionary<string, string>
{
{ "target", this.relativeFilePath },
Expand All @@ -151,11 +164,23 @@ public void Write(CompilerData compilerData)
properties.Add("commandLineId", commandLineId);
}

if (!string.IsNullOrWhiteSpace(compilerData.AssemblyReferences))
{
assemblyReferencesId = Guid.NewGuid().ToString();
properties.Add("assemblyReferencesId", assemblyReferencesId);
}

s_telemetryClient.TrackEvent(CompilerEventName, properties: properties);

// send big size content in chunked pieces
if (!string.IsNullOrWhiteSpace(commandLineId))
{
SendChunkedCommandLine(commandLineId, compilerData.CommandLine);
SendChunkedContent(CommandLineEventName, commandLineId, "commandLine", compilerData.CommandLine);
}

if (!string.IsNullOrWhiteSpace(assemblyReferencesId))
{
SendChunkedContent(AssemblyReferencesEventName, assemblyReferencesId, "assemblyReferences", compilerData.AssemblyReferences);
}
}
else
Expand Down Expand Up @@ -239,21 +264,21 @@ public static void Summarize(AnalysisSummary summary)
}
}

private void SendChunkedCommandLine(string commandLineId, string commandLine)
private void SendChunkedContent(string eventName, string contentId, string contentName, string content)
{
int j = 1;
int size = (int)Math.Ceiling(1.0 * commandLine.Length / ChunkSize);
for (int i = 0; i < commandLine.Length; i += ChunkSize)
int size = (int)Math.Ceiling(1.0 * content.Length / ChunkSize);
for (int i = 0; i < content.Length; i += ChunkSize)
{
string tempCommandLine = commandLine.Substring(i, Math.Min(ChunkSize, commandLine.Length - i));
string chunckedContent = content.Substring(i, Math.Min(ChunkSize, content.Length - i));

s_telemetryClient.TrackEvent(CommandLineEventName, properties: new Dictionary<string, string>
s_telemetryClient.TrackEvent(eventName, properties: new Dictionary<string, string>
{
{ "sessionId", s_sessionId },
{ "commandLineId", commandLineId },
{ $"{contentName}Id", contentId },
{ "orderNumber", j.ToString() },
{ "totalNumber", size.ToString() },
{ "chunkedCommandLine", tempCommandLine },
{ $"chunked{contentName}", chunckedContent },
});
j++;
}
Expand Down
15 changes: 15 additions & 0 deletions src/BinaryParsers/PEBinary/PortableExecutable/PE.cs
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,21 @@ public ChecksumAlgorithmType ManagedPdbSourceFileChecksumAlgorithm(PdbFileType p
: ChecksumAlgorithmForPortablePdb();
}

public IEnumerable<string> GetAssemblyReferenceStrings()
{
if (this.IsManaged && this.metadataReader != null)
{
foreach (AssemblyReferenceHandle handle in metadataReader.AssemblyReferences)
{
AssemblyReference assemblyReference = metadataReader.GetAssemblyReference(handle);
string assemblyName = metadataReader.GetString(assemblyReference.Name);
string version = assemblyReference.Version.ToString();

yield return $"{assemblyName} ({version})";
}
}
}

private ChecksumAlgorithmType ChecksumAlgorithmForPortablePdb()
{
if (!this.peReader.TryOpenAssociatedPortablePdb(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Text;

using FluentAssertions;

using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.CodeAnalysis.IL.Sdk;

using Xunit;

namespace Microsoft.CodeAnalysis.BinSkim.Rules
{
public class CompilerDataLoggerUnitTests
{
private TelemetryConfiguration telemetryConfiguration;
private TelemetryClient telemetryClient;
private List<ITelemetry> sendItems;

private void TestSetup()
{
if (this.telemetryConfiguration == null && this.telemetryClient == null)
{
this.telemetryConfiguration = new TelemetryConfiguration();
this.sendItems = new List<ITelemetry>();
this.telemetryConfiguration.TelemetryChannel = new StubTelemetryChannel { OnSend = item => this.sendItems.Add(item) };
this.telemetryConfiguration.InstrumentationKey = Guid.NewGuid().ToString();
this.telemetryConfiguration.TelemetryInitializers.Add(new OperationCorrelationTelemetryInitializer());
this.telemetryClient = new TelemetryClient(this.telemetryConfiguration);
}
}

[Fact]
public void CompilerDataLogger_Write_ShouldSendAssemblyReferencesInChunks()
{
this.TestSetup();

BinaryAnalyzerContext context = new BinaryAnalyzerContext { };
string[] targetFileSpecifier = new[] { @"E:\applications\Tool\*.exe" };
int chunksize = 10;
string assemblies = "Microsoft.DiaSymReader (1.3.0);Newtonsoft.Json (13.0.1)";
int chunkNumber = (assemblies.Length + chunksize - 1) / chunksize;

CompilerDataLogger logger = new CompilerDataLogger(context, targetFileSpecifier, this.telemetryConfiguration, this.telemetryClient, chunksize);
logger.Write(new CompilerData { CompilerName = ".NET Compiler", AssemblyReferences = assemblies });

this.sendItems.Count.Should().Be(chunkNumber + 1); // first item should be CompilerInformation
}

[Fact]
public void CompilerDataLogger_Write_ShouldNotSend_IfNoAssemblyReferences()
{
this.TestSetup();

BinaryAnalyzerContext context = new BinaryAnalyzerContext { };
string[] targetFileSpecifier = new[] { @"E:\applications\Tool\*.exe" };
int chunksize = 10;
string assemblies = null;

CompilerDataLogger logger = new CompilerDataLogger(context, targetFileSpecifier, this.telemetryConfiguration, this.telemetryClient, chunksize);
logger.Write(new CompilerData { CompilerName = ".NET Compiler", AssemblyReferences = assemblies });

this.sendItems.Count.Should().Be(1); // first item should be CompilerInformation
}
}
}
71 changes: 71 additions & 0 deletions src/Test.UnitTests.BinSkim.Driver/StubTelemetryChannel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Text;

using Microsoft.ApplicationInsights.Channel;

namespace Microsoft.CodeAnalysis.BinSkim.Rules
{
public class StubTelemetryChannel : ITelemetryChannel
{
public delegate void TelemetryAction(ITelemetry telemetry);

/// <summary>
/// Initializes a new instance of the <see cref="StubTelemetryChannel"/> class.
/// </summary>
public StubTelemetryChannel()
{
this.OnSend = telemetry => { };
}

/// <summary>
/// Gets or sets a value indicating whether this channel is in developer mode.
/// </summary>
public bool? DeveloperMode { get; set; }

/// <summary>
/// Gets or sets a value indicating the channel's URI. To this URI the telemetry is expected to be sent.
/// </summary>
public string EndpointAddress { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to throw an error.
/// </summary>
public bool ThrowError { get; set; }

/// <summary>
/// Gets or sets the callback invoked by the <see cref="Send"/> method.
/// </summary>
public TelemetryAction OnSend { get; set; }

/// <summary>
/// Implements the <see cref="ITelemetryChannel.Send"/> method by invoking the <see cref="OnSend"/> callback.
/// </summary>
public void Send(ITelemetry item)
{
if (this.ThrowError)
{
throw new Exception("test error");
}

this.OnSend(item);
}

/// <summary>
/// Implements the <see cref="IDisposable.Dispose"/> method.
/// </summary>
public void Dispose()
{
}

/// <summary>
/// Mock for the Flush method in <see cref="ITelemetryChannel"/>.
/// </summary>
public void Flush()
{
}
}
}