Added Snyk API console app

This commit is contained in:
Jurjen Ladenius
2022-08-26 13:44:50 +02:00
parent c00d00acfb
commit 91b4dde5f3
23 changed files with 727 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32505.173
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SnykRestApi", "SnykRestApi\SnykRestApi.csproj", "{388306F9-E67B-4CD2-9876-ACAC06968015}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{388306F9-E67B-4CD2-9876-ACAC06968015}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{388306F9-E67B-4CD2-9876-ACAC06968015}.Debug|Any CPU.Build.0 = Debug|Any CPU
{388306F9-E67B-4CD2-9876-ACAC06968015}.Release|Any CPU.ActiveCfg = Release|Any CPU
{388306F9-E67B-4CD2-9876-ACAC06968015}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BA8B2157-BFB1-4397-9DED-E4522D895591}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,15 @@
namespace SnykRestApi.Models.Parsed
{
public class AuditLog
{
public string GroupId { get; set; }
public string OrganizationId { get; set; }
public string OrganizationName { get; set; }
public string UserId { get; set; }
public string UserName { get; set; }
public string ProjectId { get; set; }
public string ProjectName { get; set; }
public string Event { get; set; }
public DateTime Created { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace SnykRestApi.Models.Raw
{
internal class AuditLogResponse
{
[JsonPropertyName("groupId")]
public string GroupId { get; set; }
[JsonPropertyName("orgId")]
public string OrgId { get; set; }
[JsonPropertyName("userId")]
public string UserId { get; set; }
[JsonPropertyName("projectId")]
public string ProjectId { get; set; }
[JsonPropertyName("event")]
public string Event { get; set; }
//[JsonPropertyName("content")]
//public string Content { get; set; }
[JsonPropertyName("created")]
public DateTime Created { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace SnykRestApi.Models.Raw
{
internal class GroupResponse
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace SnykRestApi.Models.Raw
{
internal class OrganizationListResponse
{
[JsonPropertyName("orgs")]
public List<OrganizationResponse> Orgs { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
namespace SnykRestApi.Models.Raw
{
internal class OrganizationResponse
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("slug")]
public string Slug { get; set; }
[JsonPropertyName("url")]
public string Url { get; set; }
[JsonPropertyName("group")]
public GroupResponse Group { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace SnykRestApi.Models.Raw
{
internal class ProjectListResponse
{
[JsonPropertyName("org")]
public OrganizationResponse Org { get; set; }
[JsonPropertyName("projects")]
public List<ProjectResponse> Projects { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace SnykRestApi.Models.Raw
{
internal class ProjectResponse
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
namespace SnykRestApi.Models.Raw
{
internal class UserResponse
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("username")]
public string UserName { get; set; }
[JsonPropertyName("email")]
public string Email { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
namespace SnykRestApi.Models
{
public class Settings
{
public string KeyVaultName { get; set; } = string.Empty;
public string CsvFolder { get; set; } = string.Empty;
public string SnykBaseUrl { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,40 @@
using SnykRestApi.Models;
using SnykRestApi.Repositories;
using SnykRestApi.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace SnykRestApi
{
class Program
{
static Task Main(string[] args) =>
CreateHostBuilder(args).Build().RunAsync();
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((builder, services) =>
{
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
Settings settings = config.GetRequiredSection("Settings").Get<Settings>();
services.AddSingleton<AccessTokenRepository>();
services.AddSingleton(settings);
services.AddHttpClient<OrganizationRepository>();
services.AddHttpClient<AuditLogRepository>();
services.AddHttpClient<ProjectRepository>();
services.AddHttpClient<UserRepository>();
services.AddTransient<CsvRepository>();
services.AddScoped<AuditLogService>();
services.AddHostedService<OptionService>();
});
}
}

View File

@@ -0,0 +1,29 @@
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using SnykRestApi.Models;
namespace SnykRestApi.Repositories
{
public class AccessTokenRepository
{
private readonly Settings _settings;
private string? _authorizationToken = string.Empty;
public AccessTokenRepository(Settings settings)
{
_settings = settings;
}
public async Task<string> GetAuthorizationToken()
{
if (!string.IsNullOrWhiteSpace(_authorizationToken)) return _authorizationToken;
var keyvaultUri = "https://" + _settings.KeyVaultName + ".vault.azure.net";
var credential = new DefaultAzureCredential();
var client = new SecretClient(new Uri(keyvaultUri), credential);
_authorizationToken = (await client.GetSecretAsync("SnykKey")).Value.Value;
return _authorizationToken;
}
}
}

View File

@@ -0,0 +1,49 @@
using SnykRestApi.Models;
using SnykRestApi.Models.Raw;
using System.Net.Http.Headers;
using System.Text.Json;
namespace SnykRestApi.Repositories
{
public class AuditLogRepository
{
private readonly HttpClient _httpClient;
private readonly AccessTokenRepository _accessTokenRepository;
private readonly Settings _settings;
public AuditLogRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository, Settings settings)
{
_httpClient = httpClient;
_accessTokenRepository = accessTokenRepository;
_settings = settings;
}
internal async Task<List<AuditLogResponse>> GetByOrganizationId(string origanizationId)
{
var authorizationToken = await _accessTokenRepository.GetAuthorizationToken();
List<AuditLogResponse> result = new();
List<AuditLogResponse> responseItems;
int page = 0;
do
{
HttpRequestMessage request = new(HttpMethod.Post, $"{_settings.SnykBaseUrl}org/{origanizationId}/audit?from=2022-07-01&page={++page}");
request.Headers.Accept.Clear();
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("token", authorizationToken);
var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
responseItems = JsonSerializer.Deserialize<List<AuditLogResponse>>(responseString) ?? new List<AuditLogResponse>();
result.AddRange(responseItems);
}
while (responseItems.Count == 100);
return result;
}
}
}

View File

@@ -0,0 +1,32 @@
using CsvHelper;
using SnykRestApi.Models;
using SnykRestApi.Models.Parsed;
using System.Globalization;
namespace SnykRestApi.Repositories
{
public class CsvRepository
{
private readonly Settings _settings;
public CsvRepository(Settings settings)
{
_settings = settings;
}
public async Task WriteAll (List<AuditLog> log)
{
var t = DateTime.Now;
var fileName = $"{_settings.CsvFolder}SnykAuditLog_{t:yyyy}{t:MM}{t:dd}_{t:HH}{t:mm}{t:ss}_{t:FFF}.csv";
using (var writer = new StreamWriter(fileName))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
await csv.WriteRecordsAsync(log);
}
}
}
}

View File

@@ -0,0 +1,40 @@
using SnykRestApi.Models;
using SnykRestApi.Models.Raw;
using System.Net.Http.Headers;
using System.Text.Json;
namespace SnykRestApi.Repositories
{
public class OrganizationRepository
{
private readonly HttpClient _httpClient;
private readonly AccessTokenRepository _accessTokenRepository;
private readonly Settings _settings;
public OrganizationRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository, Settings settings)
{
_httpClient = httpClient;
_accessTokenRepository = accessTokenRepository;
_settings = settings;
}
internal async Task<List<OrganizationResponse>> GetAll()
{
var authorizationToken = await _accessTokenRepository.GetAuthorizationToken();
var uri = $"{_settings.SnykBaseUrl}orgs";
HttpRequestMessage request = new(HttpMethod.Get, uri);
request.Headers.Accept.Clear();
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("token", authorizationToken);
var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var result = JsonSerializer.Deserialize<OrganizationListResponse>(responseString)?.Orgs;
return result ?? new List<OrganizationResponse>();
}
}
}

View File

@@ -0,0 +1,40 @@
using SnykRestApi.Models;
using SnykRestApi.Models.Raw;
using System.Net.Http.Headers;
using System.Text.Json;
namespace SnykRestApi.Repositories
{
public class ProjectRepository
{
private readonly HttpClient _httpClient;
private readonly AccessTokenRepository _accessTokenRepository;
private readonly Settings _settings;
public ProjectRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository, Settings settings)
{
_httpClient = httpClient;
_accessTokenRepository = accessTokenRepository;
_settings = settings;
}
internal async Task<List<ProjectResponse>> GetAll(string organizationId)
{
var authorizationToken = await _accessTokenRepository.GetAuthorizationToken();
var uri = $"{_settings.SnykBaseUrl}org/{organizationId}/projects";
HttpRequestMessage request = new(HttpMethod.Post, uri);
request.Headers.Accept.Clear();
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("token", authorizationToken);
var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var result = JsonSerializer.Deserialize<ProjectListResponse>(responseString)?.Projects;
return result ?? new List<ProjectResponse>();
}
}
}

View File

@@ -0,0 +1,65 @@
using SnykRestApi.Models;
using SnykRestApi.Models.Raw;
using Spectre.Console;
using System.Net.Http.Headers;
using System.Text.Json;
namespace SnykRestApi.Repositories
{
public class UserRepository
{
private readonly HttpClient _httpClient;
private readonly AccessTokenRepository _accessTokenRepository;
private readonly Settings _settings;
public UserRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository, Settings settings)
{
_httpClient = httpClient;
_accessTokenRepository = accessTokenRepository;
_settings = settings;
}
internal async Task<List<UserResponse>> GetAll(List<string> ids)
{
var authorizationToken = await _accessTokenRepository.GetAuthorizationToken();
List<UserResponse> result = new();
UserResponse? responseItem;
foreach (var id in ids)
{
HttpRequestMessage request = new(HttpMethod.Get, $"{_settings.SnykBaseUrl}user/{id}");
request.Headers.Accept.Clear();
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("token", authorizationToken);
var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
try
{
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
responseItem = JsonSerializer.Deserialize<UserResponse>(responseString);
if (responseItem != null)
{
result.Add(responseItem);
}
}
catch
{
AnsiConsole.MarkupLine($"[red bold]Could not find user with id '{id}'[/]");
result.Add(new()
{
Id = id,
Name = "{ Unknown user }",
UserName = "{ Unknown user }",
Email = "{ Unknown user }"
});
}
}
return result;
}
}
}

View File

@@ -0,0 +1,109 @@
using SnykRestApi.Models.Parsed;
using SnykRestApi.Models.Raw;
using SnykRestApi.Repositories;
using Spectre.Console;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SnykRestApi.Services
{
public class AuditLogService
{
private readonly OrganizationRepository _organizationRepository;
private readonly AuditLogRepository _auditLogRepository;
private readonly ProjectRepository _projectRepository;
private readonly UserRepository _userRepository;
private readonly CsvRepository _csvRepository;
public AuditLogService(OrganizationRepository organizationRepository, AuditLogRepository auditLogRepostitory, ProjectRepository projectRepository, UserRepository userRepository, CsvRepository csvRepository)
{
_organizationRepository = organizationRepository;
_auditLogRepository = auditLogRepostitory;
_projectRepository = projectRepository;
_userRepository = userRepository;
_csvRepository = csvRepository;
}
public async Task CreateAuditLog ()
{
var rule = new Rule("[skyblue1]Creating Snyk Audit Log CSV[/]");
rule.Alignment = Justify.Left;
rule.Style = Style.Parse("skyblue1");
AnsiConsole.Write(rule);
await AnsiConsole.Status()
.AutoRefresh(true)
.Spinner(Spinner.Known.Default)
.StartAsync("Retrieving organizations...", async ctx =>
{
var organizations = await _organizationRepository.GetAll();
if (organizations == null || !organizations.Any()) throw new Exception("No organizations found");
var log = new List<AuditLogResponse>();
var projects = new List<ProjectResponse>();
foreach (var organization in organizations)
{
rule = new Rule($"[skyblue1]{organization.Name}[/]");
rule.Alignment = Justify.Left;
rule.Style = Style.Parse("skyblue1 dim");
AnsiConsole.Write(rule);
ctx.Status($"Getting the projects for organization '{organization.Name}'");
var orgProjects = await _projectRepository.GetAll(organization.Id);
projects.AddRange(orgProjects);
AnsiConsole.WriteLine($"Got {orgProjects.Count} projects.");
ctx.Status($"Getting the audit log for organization '{organization.Name}'");
var orgLogs = await _auditLogRepository.GetByOrganizationId(organization.Id);
log.AddRange(orgLogs);
AnsiConsole.WriteLine($"Got {orgLogs.Count} log records.");
}
rule = new Rule($"[skyblue1]Retrieving users[/]");
rule.Alignment = Justify.Left;
rule.Style = Style.Parse("skyblue1 dim");
AnsiConsole.Write(rule);
ctx.Status($"Getting users");
var userIds = log.Select(l => l.UserId).Distinct().ToList();
var users = await _userRepository.GetAll(userIds);
AnsiConsole.WriteLine($"Got {users.Count} users of {userIds.Count} user ids.");
rule = new Rule($"[skyblue1]Creating CSV[/]");
rule.Alignment = Justify.Left;
rule.Style = Style.Parse("skyblue1 dim");
AnsiConsole.Write(rule);
ctx.Status($"Combining all information");
var result = (from l in log
join o in organizations on l.OrgId equals o.Id into gjO
from subO in gjO.DefaultIfEmpty()
join u in users on l.UserId equals u.Id into gjU
from subU in gjU.DefaultIfEmpty()
join p in projects on l.ProjectId equals p.Id into gjP
from subP in gjP.DefaultIfEmpty()
select new AuditLog()
{
GroupId = l.GroupId,
OrganizationId = l.OrgId,
OrganizationName = subO?.Name,
ProjectId = l.ProjectId,
ProjectName = subP?.Name,
UserId = l.UserId,
UserName = subU?.Name,
Event = l.Event,
Created = l.Created
}).ToList();
AnsiConsole.WriteLine($"Prepared {result.Count} lines to export of {log.Count} audit log records.");
ctx.Status($"Writing CSV");
await _csvRepository.WriteAll(result);
});
}
}
}

View File

@@ -0,0 +1,56 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Spectre.Console;
namespace SnykRestApi.Services
{
public class OptionService : IHostedService
{
private readonly ILogger<OptionService> _logger;
private readonly AuditLogService _auditLogService;
public OptionService(ILogger<OptionService> logger, AuditLogService auditLogService)
{
_logger = logger;
_auditLogService = auditLogService;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var rule = new Rule("[yellow]Cloud Egineering Console App[/]");
AnsiConsole.Write(rule);
Console.WriteLine("-- This couldn't be done in the Snyk UI, so here we are.... ");
Console.WriteLine();
var choices = new[]
{
"Create Audit log CSV.",
"Exit"
};
var result = AnsiConsole.Prompt(new SelectionPrompt<string>()
.Title("Select what you want to do:")
.PageSize(10)
.MoreChoicesText("[grey](Move up and down to reveal more choices)[/]")
.AddChoices(choices));
if (result == choices[0])
{
await _auditLogService.CreateAuditLog();
}
//else if (result == choices[1])
//{
// // Do something
//}
rule = new Rule("[yellow]Done. Bye.[/]");
AnsiConsole.Write(rule);
}
public Task StopAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.6.1" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.3.0" />
<PackageReference Include="CsvHelper" Version="28.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Spectre.Console" Version="0.44.0" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
{
"Settings": {
"KeyVaultName": "consoleapp",
"CsvFolder": "c:\\temp\\",
"SnykBaseUrl": "https://api.snyk.io/api/v1/"
},
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "None"
}
}
}