Skip to content

Commit

Permalink
feat: send analytics after scans by calling the CLI (#252)
Browse files Browse the repository at this point in the history
* feat: send analytics after scans by calling the CLI

* feat: add tests

* fix: tests and injection

* fix: stupid error

* docs: add CHANGELOG.md entry

* refactor: use collection initializer

* chore: remove old comment

* fix: implement PR suggestions

- convert JSON correctly into snake_case
- use architecture from operating system
- log output of CLI command
- escape the payload for the command line

* refactor: change called analytics command

- use `--experimental`
- use `analytics report` as command

This needs an update from Go Application Framework to actually report
  • Loading branch information
bastiandoetsch authored Nov 22, 2023
1 parent 029b164 commit a7b79c6
Show file tree
Hide file tree
Showing 15 changed files with 671 additions and 95 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Snyk Changelog

## [1.1.44]
- Support for IDE analytics

## [1.1.43]

### Added
Expand Down
9 changes: 8 additions & 1 deletion Snyk.Common/Settings/ISnykOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
/// </summary>
public interface ISnykOptions
{
String Application { get; set; }
String ApplicationVersion { get; set; }
String IntegrationName { get; }
String IntegrationVersion { get; }
String IntegrationEnvironment { get; set; }
String IntegrationEnvironmentVersion { get; set; }

/// <summary>
/// Gets or sets a value indicating whether Snyk user API token.
/// </summary>
Expand Down Expand Up @@ -105,6 +112,6 @@ public interface ISnykOptions
/// </summary>
void LoadSettingsFromStorage();

SastSettings SastSettings { get; set; }
SastSettings SastSettings { get; set; }
}
}
1 change: 1 addition & 0 deletions Snyk.VisualStudio.Extension.Shared/CLI/ICli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ public interface ICli
void Authenticate();

string RunCommand(string basePath);
Task ReportAnalyticsAsync(string data);
}
}
7 changes: 7 additions & 0 deletions Snyk.VisualStudio.Extension.Shared/CLI/ICliProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Snyk.VisualStudio.Extension.Shared.CLI
{
public interface ICliProvider
{
public ICli Cli { get; }
}
}
102 changes: 59 additions & 43 deletions Snyk.VisualStudio.Extension.Shared/CLI/SnykCli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,10 @@
using System.Security.Authentication;
using System.Threading.Tasks;
using Serilog;
using Snyk.Common;
using Snyk.Common.Authentication;
using Common;
using Common.Authentication;
using Snyk.Common.Service;
using Snyk.Common.Settings;
using Snyk.VisualStudio.Extension.Shared.Service;
using Snyk.VisualStudio.Extension.Shared.Settings;
using Snyk.VisualStudio.Extension.Shared.UI.Notifications;

/// <summary>
/// Incapsulate work logic with Snyk CLI.
Expand Down Expand Up @@ -45,14 +42,8 @@ public SnykCli(ISnykOptions options, string ideVersion = "")
/// </summary>
public ISnykOptions Options
{
get
{
return this.options;
}
set
{
this.options = value;
}
get { return this.options; }
set { this.options = value; }
}

/// <inheritdoc/>
Expand Down Expand Up @@ -144,12 +135,12 @@ public void Authenticate()
{
args.Add("--auth-type=oauth");
environmentVariables.Add("INTERNAL_SNYK_OAUTH_ENABLED", "1");

}

environmentVariables.Add(ApiEnvironmentVariableName, apiEndpointResolver.SnykApiEndpoint);

var authResultMessage = this.ConsoleRunner.Run(this.GetCliPath(), string.Join(" ", args), environmentVariables);
var authResultMessage =
this.ConsoleRunner.Run(this.GetCliPath(), string.Join(" ", args), environmentVariables);
var authenticated = authResultMessage.Contains("Your account has been authenticated.");
if (authenticated)
{
Expand All @@ -165,23 +156,20 @@ public void Authenticate()
/// <inheritdoc/>
public async Task<CliResult> ScanAsync(string basePath)
{
Logger.Information("Path to scan {BasePath}", basePath);

var cliPath = this.GetCliPath();

Logger.Information("Path to scan {BasePath}", basePath);
Logger.Information("CLI path is {CliPath}", cliPath);

var arguments = await this.BuildScanArgumentsAsync();

this.ConsoleRunner.CreateProcess(cliPath, arguments, this.BuildScanEnvironmentVariables(), basePath);
ConsoleRunner.CreateProcess(cliPath, arguments, this.BuildScanEnvironmentVariables(), basePath);

Logger.Information("Start run console process");

var consoleResult = this.ConsoleRunner.Execute();

Logger.Information("Start convert console string result to CliResult and return value");

return ConvertRawCliStringToCliResult(consoleResult);
var result = ConvertRawCliStringToCliResult(consoleResult);
return result;
}

/// <inheritdoc/>
Expand All @@ -206,6 +194,25 @@ public string RunCommand(string arguments)
return consoleResult;
}

public async Task ReportAnalyticsAsync(string data)
{
var escapedData = "\"" + data.Replace("\"", "\\\"") + "\"";
List<string> args = new()
{
"analytics",
"report",
"--experimental",
"-i",
escapedData
};
await AddGeneralArgsFromConfigAsync(args);
ConsoleRunner.CreateProcess(GetCliPath(), string.Join(" ", args),
BuildScanEnvironmentVariables());

var result = ConsoleRunner.Execute();
if (result.Length > 0) Logger.Warning("ReportAnalyticsAsync: Unexpected output: {Result}", result);
}

private string GetCliPath()
{
var snykCliCustomPath = this.options?.CliCustomPath;
Expand Down Expand Up @@ -233,7 +240,7 @@ public StringDictionary BuildScanEnvironmentVariables()

var apiEndpointResolver = new ApiEndpointResolver(this.options);
environmentVariables.Add(ApiEnvironmentVariableName, apiEndpointResolver.SnykApiEndpoint);

return environmentVariables;
}

Expand All @@ -251,22 +258,7 @@ public async Task<string> BuildScanArgumentsAsync()
"test",
};

if (this.Options.IgnoreUnknownCA)
{
arguments.Add("--insecure");
}

if (!string.IsNullOrEmpty(this.Options.Organization))
{
arguments.Add($"--org={this.Options.Organization}");
}

var additionalOptions = await this.Options.GetAdditionalOptionsAsync();

if (!string.IsNullOrEmpty(additionalOptions))
{
arguments.Add($"{additionalOptions}");
}
await AddGeneralArgsFromConfigAsync(arguments);

var isScanAllProjects = await this.Options.IsScanAllProjectsAsync();

Expand All @@ -283,6 +275,26 @@ public async Task<string> BuildScanArgumentsAsync()
return cliOptions;
}

private async Task AddGeneralArgsFromConfigAsync(ICollection<string> arguments)
{
if (!string.IsNullOrEmpty(this.Options.Organization))
{
arguments.Add($"--org={this.Options.Organization}");
}

if (this.Options.IgnoreUnknownCA)
{
arguments.Add("--insecure");
}

var additionalOptions = await this.Options.GetAdditionalOptionsAsync();

if (!string.IsNullOrEmpty(additionalOptions))
{
arguments.Add($"{additionalOptions}");
}
}

/// <summary>
/// Convert raw json string to <see cref="CliResult"/> object.
/// Check is json object is array. If it's array of cli vulnerability objects it will create <see cref="CliVulnerabilities"/> list.
Expand All @@ -299,7 +311,8 @@ public static CliResult ConvertRawCliStringToCliResult(string rawResult)
{
CliVulnerabilitiesList = Json.Deserialize<List<CliVulnerabilities>>(rawResult),
};
} else if (rawResult.First() == '{')
}
else if (rawResult.First() == '{')
{
if (IsSuccessCliJsonString(rawResult))
{
Expand All @@ -311,14 +324,16 @@ public static CliResult ConvertRawCliStringToCliResult(string rawResult)
{
CliVulnerabilitiesList = cliVulnerabilitiesList,
};
} else
}
else
{
return new CliResult
{
Error = Json.Deserialize<CliError>(rawResult),
};
}
} else
}
else
{
return new CliResult
{
Expand All @@ -337,6 +352,7 @@ public static CliResult ConvertRawCliStringToCliResult(string rawResult)
/// </summary>
/// <param name="json">Source json string.</param>
/// <returns>True if json string contains vulnerabilities object(s).</returns>
public static bool IsSuccessCliJsonString(string json) => json.Contains("\"vulnerabilities\":") && !json.Contains("\"error\":");
public static bool IsSuccessCliJsonString(string json) =>
json.Contains("\"vulnerabilities\":") && !json.Contains("\"error\":");
}
}
68 changes: 38 additions & 30 deletions Snyk.VisualStudio.Extension.Shared/Service/SnykService.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,32 @@
using Microsoft.VisualStudio.Shell;

namespace Snyk.VisualStudio.Extension.Shared.Service
namespace Snyk.VisualStudio.Extension.Shared.Service
{
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.Settings;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Settings;
using Newtonsoft.Json.Linq;
using Serilog;
using Snyk.Analytics;
using Analytics;
using Snyk.Code.Library.Service;
using Snyk.Common;
using Snyk.Common.Authentication;
using Common;
using Snyk.Common.Service;
using Snyk.Common.Settings;
using Snyk.VisualStudio.Extension.Shared.CLI;
using Snyk.VisualStudio.Extension.Shared.Settings;
using Snyk.VisualStudio.Extension.Shared.Theme;
using Snyk.VisualStudio.Extension.Shared.UI;
using Snyk.VisualStudio.Extension.Shared.UI.Notifications;
using Snyk.VisualStudio.Extension.Shared.UI.Toolwindow;
using IAsyncServiceProvider = Microsoft.VisualStudio.Shell.IAsyncServiceProvider;
using CLI;
using Settings;
using Theme;
using UI;
using UI.Notifications;
using UI.Toolwindow;
using Task = System.Threading.Tasks.Task;

/// <summary>
/// Main logic for Snyk extension.
/// </summary>
public class SnykService : ISnykServiceProvider, ISnykService
public class SnykService : ISnykServiceProvider, ISnykService, ICliProvider
{
private static readonly ILogger Logger = LogManager.ForContext<SnykService>();

Expand All @@ -51,6 +42,8 @@ public class SnykService : ISnykServiceProvider, ISnykService

private ISnykAnalyticsService analyticsService;

private SnykIdeAnalyticsService ideAnalyticsService;

private SnykUserStorageSettingsService userStorageSettingsService;

private ISnykCodeService snykCodeService;
Expand Down Expand Up @@ -119,6 +112,20 @@ public SnykService(IAsyncServiceProvider serviceProvider, string vsVersion = "")
/// </summary>
public SnykVsThemeService VsThemeService => this.vsThemeService;

public SnykIdeAnalyticsService SnykIdeAnalyticsService
{
get
{
if (ideAnalyticsService == null)
{
ideAnalyticsService =
new SnykIdeAnalyticsService(Package, this, TasksService);
}

return ideAnalyticsService;
}
}

/// <summary>
/// Gets Analytics service instance. If analytics service not created yet it will create it and return.
/// </summary>
Expand All @@ -135,7 +142,8 @@ public ISnykAnalyticsService AnalyticsService
{
Logger.Information("Notifying analytics service after settings change");
this.InitializeAnalyticsService();
ThreadHelper.JoinableTaskFactory.Run(async () => await this.analyticsService.ObtainUserAsync(this.Options.ApiToken));
ThreadHelper.JoinableTaskFactory.Run(async () =>
await this.analyticsService.ObtainUserAsync(this.Options.ApiToken));
Logger.Information("Analytics service re-initialized");
};
}
Expand Down Expand Up @@ -230,7 +238,8 @@ public ISentryService SentryService
/// </summary>
/// <param name="serviceType">Needed service type.</param>
/// <returns>Result VS service instance</returns>
public async Task<object> GetServiceAsync(Type serviceType) => await this.serviceProvider.GetServiceAsync(serviceType);
public async Task<object> GetServiceAsync(Type serviceType) =>
await this.serviceProvider.GetServiceAsync(serviceType);

/// <summary>
/// Initialize service.
Expand Down Expand Up @@ -260,7 +269,8 @@ public async Task InitializeAsync(CancellationToken cancellationToken)
NotificationService.Initialize(this);
VsStatusBar.Initialize(this);
VsCodeService.Initialize();


SnykIdeAnalyticsService.Initialize();
Logger.Information("Leave SnykService.InitializeAsync");
}
catch (Exception ex)
Expand Down Expand Up @@ -301,17 +311,13 @@ private void SetupSnykCodeService()
Logger.Error(e, string.Empty);
}
}

private void InitializeAnalyticsService()
{
Logger.Information("Initialize Analytics Service...");
var writeKey = SnykExtension.AppSettings?.SegmentAnalyticsWriteKey;

string anonymousId = this.Options.AnonymousId;
if (string.IsNullOrEmpty(anonymousId))
{
anonymousId = System.Guid.NewGuid().ToString();
this.Options.AnonymousId = anonymousId;
}

var enabled = this.Options.UsageAnalyticsEnabled && !this.Options.IsFedramp();
var endpoint = this.ApiEndpointResolver.UserMeEndpoint;
Expand All @@ -321,5 +327,7 @@ private void InitializeAnalyticsService()
this.analyticsService = SnykAnalyticsService.Instance;
Logger.Information("Analytics service initialized");
}

public ICli Cli => NewCli();
}
}
}
Loading

0 comments on commit a7b79c6

Please sign in to comment.