Last active
September 30, 2016 09:36
-
-
Save nul800sebastiaan/1d3cc51deedae8dbd26c33e597edd3db to your computer and use it in GitHub Desktop.
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
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net.Http; | |
using Newtonsoft.Json; | |
using Umbraco.Core.Logging; | |
using Umbraco.Core.Models.Membership; | |
using Umbraco.Core.Services; | |
using Umbraco.Web.HealthCheck; | |
namespace HealthCheckContrib | |
{ | |
/// <summary> | |
/// Checks Umbraco backoffice users against the HaveIBeenPwned database to check if they've been breached | |
/// </summary> | |
[HealthCheck( | |
"E1368DBE-5066-4646-A13C-5A7C92C4105A", | |
"Have I Been Pwned", | |
Description = "Checks if any of the users in this backoffice have been pwned, as reported by https://haveibeenpwned.com", | |
Group = "Security")] | |
public class HaveIBeenPwnedCheck : HealthCheck | |
{ | |
public HaveIBeenPwnedCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext) | |
{ | |
_services = HealthCheckContext.ApplicationContext.Services; | |
} | |
private readonly ServiceContext _services; | |
/// <summary> | |
/// Get the status for this health check | |
/// </summary> | |
/// <returns></returns> | |
public override IEnumerable<HealthCheckStatus> GetStatus() | |
{ | |
//return the statuses | |
return new[] { CheckPwnage() }; | |
} | |
/// <summary> | |
/// Executes the action and returns it's status | |
/// </summary> | |
/// <param name="action"></param> | |
/// <returns></returns> | |
public override HealthCheckStatus ExecuteAction(HealthCheckAction action) | |
{ | |
switch (action.Alias) | |
{ | |
case "sendNotificationMails": | |
return SendBreachNotifications(); | |
default: | |
throw new ArgumentOutOfRangeException(); | |
} | |
} | |
private HealthCheckStatus CheckPwnage() | |
{ | |
int totalUsers; | |
var results = new List<HaveIBeenPwnedBreach>(); | |
var allUsers = _services.UserService.GetAll(0, int.MaxValue, out totalUsers).ToArray(); | |
var affectedEmails = new List<string>(); | |
foreach (var user in allUsers) | |
{ | |
var email = user.Email; | |
var breaches = CheckAccount(email).ToArray(); | |
if (breaches.Any() == false) continue; | |
affectedEmails.Add(email); | |
results.AddRange(breaches); | |
} | |
var actions = new List<HealthCheckAction>(); | |
if (results.Any()) | |
actions.Add(new HealthCheckAction("sendNotificationMails", Id) { Name = "Notify users", Description = "This will send a notification email to all affected users to let them know they should change their passwords everywhere." }); | |
return new HealthCheckStatus(string.Format("Checked {0} backoffice users, {1} breaches found, affected email addresses: {2}", totalUsers, results.Count, string.Join(",", affectedEmails))) | |
{ | |
ResultType = results.Any() ? StatusResultType.Warning : StatusResultType.Success, | |
Actions = actions | |
}; | |
} | |
private HealthCheckStatus SendBreachNotifications() | |
{ | |
int totalUsers; | |
var results = new List<IUser>(); | |
var allUsers = _services.UserService.GetAll(0, int.MaxValue, out totalUsers); | |
foreach (var user in allUsers) | |
{ | |
var email = user.Email; | |
var breaches = CheckAccount(email); | |
if (breaches.Any()) | |
{ | |
//TODO: Implement sending of notification mail | |
results.Add(user); | |
} | |
} | |
return new HealthCheckStatus(string.Format("Sent notification to {0} backoffice users about breaches related to their account", results.Count)) | |
{ | |
ResultType = StatusResultType.Success | |
}; | |
} | |
private IEnumerable<HaveIBeenPwnedBreach> CheckAccount(string account) | |
{ | |
// Rate limited to one query per second so wait a second befor each call | |
Thread.Sleep(1000); | |
var breaches = new List<HaveIBeenPwnedBreach>(); | |
try | |
{ | |
using (var httpClient = new HttpClient()) | |
{ | |
const string pwnedUrl = "https://haveibeenpwned.com/api/v2/breachedaccount/"; | |
httpClient.DefaultRequestHeaders.Add("User-Agent", "UmbracoHealthCheck"); | |
var response = httpClient.GetAsync(new Uri(pwnedUrl + account)).Result; | |
if (response.IsSuccessStatusCode) | |
{ | |
var jsonData = response.Content.ReadAsStringAsync().Result; | |
breaches = JsonConvert.DeserializeObject<List<HaveIBeenPwnedBreach>>(jsonData); | |
return breaches; | |
} | |
} | |
} | |
catch (Exception ex) | |
{ | |
LogHelper.Error<HaveIBeenPwnedCheck>("An error occurred trying to access the HaveIBeenPwned API", ex); | |
} | |
return breaches; | |
} | |
internal class HaveIBeenPwnedBreach | |
{ | |
public string Title { get; set; } | |
public string Name { get; set; } | |
public string Domain { get; set; } | |
public string BreachDate { get; set; } | |
public DateTime AddedDate { get; set; } | |
public int PwnCount { get; set; } | |
public string Description { get; set; } | |
public string[] DataClasses { get; set; } | |
public bool IsVerified { get; set; } | |
public bool IsSensitive { get; set; } | |
public bool IsRetired { get; set; } | |
public string LogoType { get; set; } | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment