Skip to content
This repository was archived by the owner on Nov 1, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/webhook_events.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ If webhook is set to have Event Grid message format then the payload will look a
"title": "Onefuzz Version",
"type": "string"
},
"report_url": {
"title": "Report Url",
"type": "string"
},
"scariness_description": {
"title": "Scariness Description",
"type": "string"
Expand Down Expand Up @@ -2158,6 +2162,10 @@ If webhook is set to have Event Grid message format then the payload will look a
"title": "Onefuzz Version",
"type": "string"
},
"report_url": {
"title": "Report Url",
"type": "string"
},
"scariness_description": {
"title": "Scariness Description",
"type": "string"
Expand Down Expand Up @@ -6578,6 +6586,10 @@ If webhook is set to have Event Grid message format then the payload will look a
"title": "Onefuzz Version",
"type": "string"
},
"report_url": {
"title": "Report Url",
"type": "string"
},
"scariness_description": {
"title": "Scariness Description",
"type": "string"
Expand Down
2 changes: 1 addition & 1 deletion src/ApiService/ApiService/Functions/Notifications.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ private async Async.Task<HttpResponseData> Get(HttpRequestData req) {
return await _context.RequestHandling.NotOk(req, request.ErrorV, "notification search");
}

var entries = request.OkV switch { { Container: null, NotificationId: null } => _context.NotificationOperations.SearchAll(), { Container: var c, NotificationId: null } => _context.NotificationOperations.SearchByRowKeys(c.Select(x => x.String)), { Container: var _, NotificationId: var n } => new[] { await _context.NotificationOperations.GetNotification(n.Value) }.ToAsyncEnumerable(),
var entries = request.OkV switch { { Container: null, NotificationId: null } => _context.NotificationOperations.SearchAll(), { Container: var c, NotificationId: null } => _context.NotificationOperations.SearchByRowKeys(c.Select(x => x.String)), { Container: var _, NotificationId: var n } => new[] { await _context.NotificationOperations.GetNotification(n.Value) }.ToAsyncEnumerable()
};

var response = req.CreateResponse(HttpStatusCode.OK);
Expand Down
41 changes: 41 additions & 0 deletions src/ApiService/ApiService/Functions/NotificationsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;

namespace Microsoft.OneFuzz.Service.Functions;

public class NotificationsTest {
private readonly ILogTracer _log;
private readonly IEndpointAuthorization _auth;
private readonly IOnefuzzContext _context;

public NotificationsTest(ILogTracer log, IEndpointAuthorization auth, IOnefuzzContext context) {
_log = log;
_auth = auth;
_context = context;
}

private async Async.Task<HttpResponseData> Post(HttpRequestData req) {
_log.WithTag("HttpRequest", "GET").Info($"Notification test");
var request = await RequestHandling.ParseRequest<NotificationTest>(req);
if (!request.IsOk) {
return await _context.RequestHandling.NotOk(req, request.ErrorV, "notification search");
}

var notificationTest = request.OkV;
var result = await _context.NotificationOperations.TriggerNotification(notificationTest.Notification.Container, notificationTest.Notification,
notificationTest.Report, isLastRetryAttempt: true);
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new NotificationTestResponse(result.IsOk, result.ErrorV?.ToString()));
return response;
}


[Function("NotificationsTest")]
public Async.Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "POST", Route = "notifications/test")] HttpRequestData req) {
return _auth.CallIfUser(req, r => r.Method switch {
"POST" => Post(r),
_ => throw new InvalidOperationException("Unsupported HTTP method"),
});
}
}
4 changes: 4 additions & 0 deletions src/ApiService/ApiService/OneFuzzTypes/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,10 @@ public RegressionReport Truncate(int maxLength) {
}
}

public record UnknownReportType(
Uri? ReportUrl
) : IReport;

[JsonConverter(typeof(NotificationTemplateConverter))]
#pragma warning disable CA1715
public interface NotificationTemplate {
Expand Down
6 changes: 6 additions & 0 deletions src/ApiService/ApiService/OneFuzzTypes/Requests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ public record NotificationSearch(
Guid? NotificationId
) : BaseRequest;


public record NotificationTest(
[property: Required] Report Report,
[property: Required] Notification Notification
) : BaseRequest;

public record NotificationGet(
[property: Required] Guid NotificationId
) : BaseRequest;
Expand Down
5 changes: 5 additions & 0 deletions src/ApiService/ApiService/OneFuzzTypes/Responses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,8 @@ List<Guid> FailedNotificationIds
public record JinjaToScribanMigrationDryRunResponse(
List<Guid> NotificationIdsToUpdate
) : BaseResponse();

public record NotificationTestResponse(
bool Success,
string? Error = null
) : BaseResponse();
39 changes: 23 additions & 16 deletions src/ApiService/ApiService/onefuzzlib/NotificationOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ public interface INotificationOperations : IOrm<Notification> {
IAsyncEnumerable<(Task, IEnumerable<Container>)> GetQueueTasks();
Async.Task<OneFuzzResult<Notification>> Create(Container container, NotificationTemplate config, bool replaceExisting);
Async.Task<Notification?> GetNotification(Guid notifificationId);

System.Threading.Tasks.Task<OneFuzzResultVoid> TriggerNotification(Container container,
Notification notification, IReport? reportOrRegression, bool isLastRetryAttempt = false);
}

public class NotificationOperations : Orm<Notification>, INotificationOperations {
Expand All @@ -30,22 +33,7 @@ public async Async.Task NewFiles(Container container, string filename, bool isLa
}

done.Add(notification.Config);

if (notification.Config is TeamsTemplate teamsTemplate) {
await _context.Teams.NotifyTeams(teamsTemplate, container, filename, reportOrRegression!, notification.NotificationId);
}

if (reportOrRegression == null) {
continue;
}

if (notification.Config is AdoTemplate adoTemplate) {
await _context.Ado.NotifyAdo(adoTemplate, container, filename, reportOrRegression, isLastRetryAttempt, notification.NotificationId);
}

if (notification.Config is GithubIssuesTemplate githubIssuesTemplate) {
await _context.GithubIssues.GithubIssue(githubIssuesTemplate, container, filename, reportOrRegression, notification.NotificationId);
}
_ = await TriggerNotification(container, notification, reportOrRegression, isLastRetryAttempt);
}
}

Expand Down Expand Up @@ -74,6 +62,25 @@ public async Async.Task NewFiles(Container container, string filename, bool isLa
}
}

public async System.Threading.Tasks.Task<OneFuzzResultVoid> TriggerNotification(Container container,
Notification notification, IReport? reportOrRegression, bool isLastRetryAttempt = false) {
switch (notification.Config) {
case TeamsTemplate teamsTemplate:
await _context.Teams.NotifyTeams(teamsTemplate, container, reportOrRegression!,
notification.NotificationId);
break;
case AdoTemplate adoTemplate when reportOrRegression is not null:
return await _context.Ado.NotifyAdo(adoTemplate, container, reportOrRegression, isLastRetryAttempt,
notification.NotificationId);
case GithubIssuesTemplate githubIssuesTemplate when reportOrRegression is not null:
await _context.GithubIssues.GithubIssue(githubIssuesTemplate, container, reportOrRegression,
notification.NotificationId);
break;
}

return OneFuzzResultVoid.Ok;
}

public IAsyncEnumerable<Notification> GetNotifications(Container container) {
return SearchByRowKeys(new[] { container.String });
}
Expand Down
25 changes: 19 additions & 6 deletions src/ApiService/ApiService/onefuzzlib/Reports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,17 @@ public Reports(ILogTracer log, IContainers containers) {
return null;
}

var blob = await _containers.GetBlob(container, fileName, StorageType.Corpus);
var containerClient = await _containers.FindContainer(container, StorageType.Corpus);
if (containerClient == null) {
if (expectReports) {
_log.Error($"get_report invalid container: {filePath:Tag:FilePath}");
}
return null;
}

Uri reportUrl = containerClient.GetBlobClient(fileName).Uri;

var blob = (await containerClient.GetBlobClient(fileName).DownloadContentAsync()).Value.Content;

if (blob == null) {
if (expectReports) {
Expand All @@ -44,11 +54,9 @@ public Reports(ILogTracer log, IContainers containers) {
return null;
}

var reportUrl = await _containers.GetFileUrl(container, fileName, StorageType.Corpus);

var reportOrRegression = ParseReportOrRegression(blob.ToString(), reportUrl);

if (reportOrRegression == null && expectReports) {
if (reportOrRegression is UnknownReportType && expectReports) {
_log.Error($"unable to parse report ({filePath:Tag:FilePath}) as a report or regression");
}

Expand All @@ -64,7 +72,7 @@ public Reports(ILogTracer log, IContainers containers) {
}
}

public static IReport? ParseReportOrRegression(string content, Uri? reportUrl) {
public static IReport ParseReportOrRegression(string content, Uri reportUrl) {
var regressionReport = TryDeserialize<RegressionReport>(content);
if (regressionReport is { CrashTestResult: { } }) {
return regressionReport with { ReportUrl = reportUrl };
Expand All @@ -73,12 +81,17 @@ public Reports(ILogTracer log, IContainers containers) {
if (report is { CrashType: { } }) {
return report with { ReportUrl = reportUrl };
}
return null;
return new UnknownReportType(reportUrl);
}
}

public interface IReport {
Uri? ReportUrl {
init;
get;
}
public string FileName() {
var segments = (this.ReportUrl ?? throw new ArgumentException()).Segments.Skip(2);
return string.Concat(segments);
}
};
12 changes: 5 additions & 7 deletions src/ApiService/ApiService/onefuzzlib/Secrets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,11 @@ public virtual async Task<SecretData<T>> SaveToKeyvault<T>(SecretData<T> secretD
}

public async Task<string?> GetSecretStringValue<T>(SecretData<T> data) {

if (data.Secret is SecretAddress<T> secretAddress) {
var secret = await GetSecret(secretAddress.Url);
return secret.Value;
} else {
return data.Secret.ToString();
}
return (data.Secret) switch {
SecretAddress<T> secretAddress => (await GetSecret(secretAddress.Url)).Value,
SecretValue<T> sValue => sValue.Value?.ToString(),
_ => data.Secret.ToString(),
};
}

public Uri GetKeyvaultAddress() {
Expand Down
85 changes: 42 additions & 43 deletions src/ApiService/ApiService/onefuzzlib/notifications/Ado.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@
namespace Microsoft.OneFuzz.Service;

public interface IAdo {
public Async.Task NotifyAdo(AdoTemplate config, Container container, string filename, IReport reportable, bool isLastRetryAttempt, Guid notificationId);
public Async.Task<OneFuzzResultVoid> NotifyAdo(AdoTemplate config, Container container, IReport reportable, bool isLastRetryAttempt, Guid notificationId);
}

public class Ado : NotificationsBase, IAdo {
public Ado(ILogTracer logTracer, IOnefuzzContext context) : base(logTracer, context) {
}

public async Async.Task NotifyAdo(AdoTemplate config, Container container, string filename, IReport reportable, bool isLastRetryAttempt, Guid notificationId) {
public async Async.Task<OneFuzzResultVoid> NotifyAdo(AdoTemplate config, Container container, IReport reportable, bool isLastRetryAttempt, Guid notificationId) {
var filename = reportable.FileName();
if (reportable is RegressionReport) {
_logTracer.Info($"ado integration does not support regression report. container:{container:Tag:Container} filename:{filename:Tag:Filename}");
return;
return OneFuzzResultVoid.Ok;
}

var report = (Report)reportable;
Expand All @@ -44,8 +45,11 @@ public async Async.Task NotifyAdo(AdoTemplate config, Container container, strin
} else {
_logTracer.WithTags(notificationInfo).Exception(e, $"Failed to process ado notification");
await LogFailedNotification(report, e, notificationId);
return OneFuzzResultVoid.Error(ErrorCode.NOTIFICATION_FAILURE,
$"Failed to process ado notification : exception: {e}");
}
}
return OneFuzzResultVoid.Ok;
}

private static bool IsTransient(Exception e) {
Expand Down Expand Up @@ -205,7 +209,7 @@ public async IAsyncEnumerable<WorkItem> ExistingWorkItems((string, string)[] not
}
}

var query = "select [System.Id] from WorkItems";
var query = "select [System.Id] from WorkItems order by [System.Id]";
if (parts != null && parts.Any()) {
query += " where " + string.Join(" AND ", parts);
}
Expand Down Expand Up @@ -331,47 +335,42 @@ private async Async.Task<WorkItem> CreateNew() {
}

public async Async.Task Process((string, string)[] notificationInfo) {
var matchingWorkItems = await ExistingWorkItems(notificationInfo).ToListAsync();

var nonDuplicateWorkItems = matchingWorkItems
.Where(wi => !IsADODuplicateWorkItem(wi))
.ToList();

if (nonDuplicateWorkItems.Count > 1) {
var nonDuplicateWorkItemIds = nonDuplicateWorkItems.Select(wi => wi.Id);
var matchingWorkItemIds = matchingWorkItems.Select(wi => wi.Id);

var extraTags = new List<(string, string)> {
("NonDuplicateWorkItemIds", JsonSerializer.Serialize(nonDuplicateWorkItemIds)),
("MatchingWorkItemIds", JsonSerializer.Serialize(matchingWorkItemIds))
};
extraTags.AddRange(notificationInfo);

_logTracer.WithTags(extraTags).Info($"Found more than 1 matching, non-duplicate work item");
foreach (var workItem in nonDuplicateWorkItems) {
_ = await UpdateExisting(workItem, notificationInfo);
var updated = false;
WorkItem? oldestWorkItem = null;
await foreach (var workItem in ExistingWorkItems(notificationInfo)) {
// work items are ordered by id, so the oldest one is the first one
oldestWorkItem ??= workItem;
_logTracer.WithTags(new List<(string, string)> { ("MatchingWorkItemIds", $"{workItem.Id}") }).Info($"Found matching work item");
if (IsADODuplicateWorkItem(workItem)) {
continue;
}
} else if (nonDuplicateWorkItems.Count == 1) {
_ = await UpdateExisting(nonDuplicateWorkItems.Single(), notificationInfo);
} else if (matchingWorkItems.Any()) {
// We have matching work items but all are duplicates
_logTracer.WithTags(notificationInfo).Info($"All matching work items were duplicates, re-opening the oldest one");
var oldestWorkItem = matchingWorkItems.OrderBy(wi => wi.Id).First();
var stateChanged = await UpdateExisting(oldestWorkItem, notificationInfo);
if (stateChanged) {
// add a comment if we re-opened the bug
_ = await _client.AddCommentAsync(
new CommentCreate() {
Text = "This work item was re-opened because OneFuzz could only find related work items that are marked as duplicate."
},
_project,
(int)oldestWorkItem.Id!);
_logTracer.WithTags(new List<(string, string)> { ("NonDuplicateWorkItemId", $"{workItem.Id}") }).Info($"Found matching non-duplicate work item");
_ = await UpdateExisting(workItem, notificationInfo);
updated = true;
}

if (!updated) {
if (oldestWorkItem != null) {
// We have matching work items but all are duplicates
_logTracer.WithTags(notificationInfo)
.Info($"All matching work items were duplicates, re-opening the oldest one");
var stateChanged = await UpdateExisting(oldestWorkItem, notificationInfo);
if (stateChanged) {
// add a comment if we re-opened the bug
_ = await _client.AddCommentAsync(
new CommentCreate() {
Text =
"This work item was re-opened because OneFuzz could only find related work items that are marked as duplicate."
},
_project,
(int)oldestWorkItem.Id!);
}
} else {
// We never saw a work item like this before, it must be new
var entry = await CreateNew();
var adoEventType = "AdoNewItem";
_logTracer.WithTags(notificationInfo).Event($"{adoEventType} {entry.Id:Tag:WorkItemId}");
}
} else {
// We never saw a work item like this before, it must be new
var entry = await CreateNew();
var adoEventType = "AdoNewItem";
_logTracer.WithTags(notificationInfo).Event($"{adoEventType} {entry.Id:Tag:WorkItemId}");
}
}

Expand Down
Loading