using System.Net.Http.Headers; using System.Text.Json; using Azure.Core; using Bastion.Rules.Snapshot; namespace Bastion.Scanner; /// /// Reads service principals and their credentials from Microsoft Graph. Kept as a plain HTTP /// client over the two Graph endpoints we need rather than pulling in the full Graph SDK — the /// shape we read is tiny and stable. Requires the calling identity to hold /// Application.Read.All (application) or Directory.Read.All. /// public sealed class GraphServicePrincipalReader { private static readonly string[] GraphScope = ["https://graph.microsoft.com/.default"]; private readonly HttpClient _http; private readonly TokenCredential _credential; public GraphServicePrincipalReader(HttpClient http, TokenCredential credential) { _http = http; _credential = credential; } public async Task> ReadAsync(CancellationToken cancellationToken = default) { var token = await _credential.GetTokenAsync(new TokenRequestContext(GraphScope), cancellationToken); _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token); var principals = new List(); var url = "https://graph.microsoft.com/v1.0/servicePrincipals" + "?$select=id,displayName,passwordCredentials,keyCredentials&$top=100"; while (url is not null) { using var response = await _http.GetAsync(url, cancellationToken); response.EnsureSuccessStatusCode(); using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(cancellationToken)); foreach (var item in doc.RootElement.GetProperty("value").EnumerateArray()) { principals.Add(Map(item)); } url = doc.RootElement.TryGetProperty("@odata.nextLink", out var next) ? next.GetString() : null; } return principals; } private static ServicePrincipal Map(JsonElement item) { var credentials = new List(); foreach (var (property, kind) in new[] { ("passwordCredentials", CredentialKind.ClientSecret), ("keyCredentials", CredentialKind.Certificate), }) { if (!item.TryGetProperty(property, out var list)) { continue; } foreach (var cred in list.EnumerateArray()) { credentials.Add(new CredentialInfo { Kind = kind, CreatedAt = ReadDate(cred, "startDateTime"), ExpiresAt = ReadDate(cred, "endDateTime"), }); } } return new ServicePrincipal { ObjectId = item.GetProperty("id").GetString() ?? "", DisplayName = item.TryGetProperty("displayName", out var name) ? name.GetString() ?? "" : "", Credentials = credentials, }; } private static DateTimeOffset ReadDate(JsonElement element, string property) => element.TryGetProperty(property, out var value) && value.TryGetDateTimeOffset(out var parsed) ? parsed : DateTimeOffset.MinValue; }