Created
October 23, 2023 19:36
-
-
Save mjsabby/867c165431367438f4dbbad6f13ebf45 to your computer and use it in GitHub Desktop.
Dump Azure DevOps Audit Logs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>net6.0</TargetFramework> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="Azure.Identity" Version="1.10.3" /> | |
</ItemGroup> | |
</Project> | |
*/ | |
namespace AuditLogsDump | |
{ | |
using System; | |
using System.Globalization; | |
using System.Net.Http; | |
using System.Net.Http.Headers; | |
using System.Text.Json; | |
using System.Text.Json.Serialization; | |
using System.Text.RegularExpressions; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using Azure.Core; | |
using Azure.Identity; | |
internal static class Program | |
{ | |
private const string AzureDevOpsScope = "499b84ac-1321-427f-aa17-267ca6975798/.default"; | |
public static async Task Main(string[] args) | |
{ | |
if (args.Length < 4) | |
{ | |
Console.WriteLine($"Usage: AuditLogsDump Organization Filter StartTime EndTime [ManagedIdentity]"); | |
Console.WriteLine($"Usage: AuditLogsDump msasg Git.RepositoryCreated 09/23/2023 10/23/2023"); | |
Console.WriteLine($"Usage: AuditLogsDump msasg Git.RepositoryCreated 09/23/2023 10/23/2023 {Guid.NewGuid()}"); | |
return; | |
} | |
var organization = args[0]; | |
var filter = new Regex(args[1], RegexOptions.Compiled | RegexOptions.IgnoreCase); | |
var startTime = DateTime.Parse(args[2], CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); | |
var endTime = DateTime.Parse(args[3], CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); | |
DefaultAzureCredential credential; | |
if (args.Length == 5) | |
{ | |
credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions { ManagedIdentityClientId = args[5] }); | |
} | |
else | |
{ | |
credential = new DefaultAzureCredential(includeInteractiveCredentials: true); | |
} | |
var accessToken = (await credential.GetTokenAsync(new TokenRequestContext(new string[] { AzureDevOpsScope }))).Token; | |
using (HttpClient client = new()) | |
{ | |
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); | |
AuditLogResponse auditLogs; | |
var continuationToken = string.Empty; | |
while (true) | |
{ | |
auditLogs = await GetAuditLogsAsync(client, organization, continuationToken, startTime, endTime, 5000); | |
foreach (var decoratedAuditLogEntry in auditLogs.DecoratedAuditLogEntries) | |
{ | |
if (filter.IsMatch(decoratedAuditLogEntry.ActionId)) | |
{ | |
Console.WriteLine(decoratedAuditLogEntry.Details); | |
} | |
} | |
if (auditLogs.HasMore) | |
{ | |
continuationToken = auditLogs.ContinuationToken; | |
Thread.Sleep(1000); | |
} | |
else | |
{ | |
break; | |
} | |
} | |
} | |
} | |
private static async Task<AuditLogResponse> GetAuditLogsAsync(HttpClient client, string organization, string continuationToken, DateTime startTime, DateTime endTime, int batchSize) => await GetAsync<AuditLogResponse>(client, $"https://auditservice.dev.azure.com/{organization}/_apis/audit/auditlog?continuationToken={continuationToken}&startTime={startTime:o}&endTime={endTime:o}&batchSize={batchSize}&skipAggregation=false&api-version=7.1-preview.1"); | |
private static async Task<T> GetAsync<T>(HttpClient client, string url) | |
{ | |
var response = await client.GetAsync(url); | |
response.EnsureSuccessStatusCode(); | |
var responseBody = await response.Content.ReadAsStringAsync(); | |
return JsonSerializer.Deserialize<T>(responseBody); | |
} | |
internal sealed class AuditLogResponse | |
{ | |
[JsonPropertyName("decoratedAuditLogEntries")] | |
public DecoratedAuditLogEntry[] DecoratedAuditLogEntries { get; set; } | |
public sealed class DecoratedAuditLogEntry | |
{ | |
[JsonPropertyName("id")] | |
public string Id { get; set; } | |
[JsonPropertyName("correlationId")] | |
public string CorrelationId { get; set; } | |
[JsonPropertyName("activityId")] | |
public string ActivityId { get; set; } | |
[JsonPropertyName("actorCUID")] | |
public string ActorCUID { get; set; } | |
[JsonPropertyName("actorUserId")] | |
public string ActorUserId { get; set; } | |
[JsonPropertyName("actorClientId")] | |
public string ActorClientId { get; set; } | |
[JsonPropertyName("actorUPN")] | |
public string ActorUPN { get; set; } | |
[JsonPropertyName("authenticationMechanism")] | |
public string AuthenticationMechanism { get; set; } | |
[JsonPropertyName("timestamp")] | |
public DateTime Timestamp { get; set; } | |
[JsonPropertyName("scopeType")] | |
public string ScopeType { get; set; } | |
[JsonPropertyName("scopeDisplayName")] | |
public string ScopeDisplayName { get; set; } | |
[JsonPropertyName("scopeId")] | |
public string ScopeId { get; set; } | |
[JsonPropertyName("projectId")] | |
public string ProjectId { get; set; } | |
[JsonPropertyName("projectName")] | |
public string ProjectName { get; set; } | |
[JsonPropertyName("ipAddress")] | |
public string IpAddress { get; set; } | |
[JsonPropertyName("userAgent")] | |
public string UserAgent { get; set; } | |
[JsonPropertyName("actionId")] | |
public string ActionId { get; set; } | |
[JsonPropertyName("data")] | |
public DataClass Data { get; set; } | |
[JsonPropertyName("details")] | |
public string Details { get; set; } | |
[JsonPropertyName("area")] | |
public string Area { get; set; } | |
[JsonPropertyName("category")] | |
public string Category { get; set; } | |
[JsonPropertyName("categoryDisplayName")] | |
public string CategoryDisplayName { get; set; } | |
[JsonPropertyName("actorDisplayName")] | |
public string ActorDisplayName { get; set; } | |
[JsonPropertyName("actorImageUrl")] | |
public string ActorImageUrl { get; set; } | |
public sealed class DataClass | |
{ | |
[JsonPropertyName("CallerProcedure")] | |
public string CallerProcedure { get; set; } | |
#if !NETFRAMEWORK | |
[JsonPropertyName("PipelineId")] | |
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] | |
public int PipelineId { get; set; } | |
#endif | |
[JsonPropertyName("PipelineName")] | |
public string PipelineName { get; set; } | |
#if !NETFRAMEWORK | |
[JsonPropertyName("ReleaseId")] | |
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] | |
public int ReleaseId { get; set; } | |
#endif | |
[JsonPropertyName("RequesterId")] | |
public string RequesterId { get; set; } | |
[JsonPropertyName("Reason")] | |
public string Reason { get; set; } | |
[JsonPropertyName("ReleaseName")] | |
public string ReleaseName { get; set; } | |
} | |
} | |
[JsonPropertyName("continuationToken")] | |
public string ContinuationToken { get; set; } | |
[JsonPropertyName("hasMore")] | |
public bool HasMore { get; set; } | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment