mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 05:00:07 +00:00
61892592e4
Tenant admins submit references to their own gather/analyze rules; the server freezes the rule, records pre-flight findings and fire stats, and stores it under a 12-character id (RuleSubmissions, SessionReports layout). Attribution is the submitter's frozen choice (anonymous by default) and becomes the published author. Review (approve with reserved id / decline with comment) through /admin/reports/rule-submissions or the MCP tools list_rule_submissions, get_rule_submission, review_rule_submission, reseed_rules_from_github; published is derived from the global catalog, never stored. Analyze rules now get the same server-side author stamp as gather rules; ANALYZE-ID-001 carries author Community. Privacy page names the credit; bumps internal and .claude/commands gitlinks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
177 lines
7.7 KiB
C#
177 lines
7.7 KiB
C#
using System.Net;
|
|
using AutopilotMonitor.Functions.Helpers;
|
|
using AutopilotMonitor.Functions.Services;
|
|
using AutopilotMonitor.Shared.Models;
|
|
using Microsoft.Azure.Functions.Worker;
|
|
using Microsoft.Azure.Functions.Worker.Http;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace AutopilotMonitor.Functions.Functions.Rules
|
|
{
|
|
/// <summary>
|
|
/// CRUD API for managing analyze rules (portal-facing, JWT auth)
|
|
/// </summary>
|
|
public class AnalyzeRulesFunction
|
|
{
|
|
private readonly ILogger<AnalyzeRulesFunction> _logger;
|
|
private readonly AnalyzeRuleService _ruleService;
|
|
|
|
public AnalyzeRulesFunction(
|
|
ILogger<AnalyzeRulesFunction> logger,
|
|
AnalyzeRuleService ruleService)
|
|
{
|
|
_logger = logger;
|
|
_ruleService = ruleService;
|
|
}
|
|
|
|
[Function("GetAnalyzeRules")]
|
|
public async Task<HttpResponseData> GetRules(
|
|
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "rules/analyze")] HttpRequestData req)
|
|
{
|
|
// Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
|
|
var tenantId = TenantHelper.GetTenantId(req);
|
|
|
|
var rules = await _ruleService.GetAllRulesForTenantAsync(tenantId);
|
|
|
|
var response = req.CreateResponse(HttpStatusCode.OK);
|
|
await response.WriteAsJsonAsync(new AnalyzeRuleListResponse { Success = true, Rules = rules });
|
|
return response;
|
|
}
|
|
|
|
[Function("CreateAnalyzeRule")]
|
|
public async Task<HttpResponseData> CreateRule(
|
|
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "rules/analyze")] HttpRequestData req)
|
|
{
|
|
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
|
|
var tenantId = TenantHelper.GetTenantId(req);
|
|
|
|
// Newtonsoft on purpose: rule documents stay on the Newtonsoft path (TypedRequestGuardTests baseline).
|
|
var read = await req.ReadNewtonsoftAsync<AnalyzeRule>(1_048_576);
|
|
if (read.Error != null) return read.Error;
|
|
var rule = read.Value!;
|
|
|
|
if (string.IsNullOrEmpty(rule.RuleId))
|
|
{
|
|
return await req.BadRequestAsync("Invalid rule data");
|
|
}
|
|
|
|
// Author is stamped from the creator's token, never from the payload
|
|
// (anti-spoof), and stays immutable through every later update.
|
|
rule.Author = TenantHelper.GetUserDisplayName(req) ?? "Autopilot Monitor";
|
|
|
|
try
|
|
{
|
|
var success = await _ruleService.CreateRuleAsync(tenantId, rule);
|
|
|
|
var response = req.CreateResponse(success ? HttpStatusCode.Created : HttpStatusCode.InternalServerError);
|
|
await response.WriteAsJsonAsync(new SuccessMessageResponse { Success = success, Message = success ? "Rule created" : "Failed to create rule" });
|
|
return response;
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
return await req.ConflictAsync(ex.Message);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return await req.BadRequestAsync(ex.Message);
|
|
}
|
|
}
|
|
|
|
[Function("UpdateAnalyzeRule")]
|
|
public async Task<HttpResponseData> UpdateRule(
|
|
[HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "rules/analyze/{ruleId}")] HttpRequestData req,
|
|
string ruleId)
|
|
{
|
|
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
|
|
var tenantId = TenantHelper.GetTenantId(req);
|
|
|
|
// Newtonsoft on purpose: rule documents stay on the Newtonsoft path (TypedRequestGuardTests baseline).
|
|
var read = await req.ReadNewtonsoftAsync<AnalyzeRule>(1_048_576);
|
|
if (read.Error != null) return read.Error;
|
|
var rule = read.Value!;
|
|
|
|
rule.RuleId = ruleId;
|
|
|
|
// Same anti-spoof stamp as CreateRule: on a true update the service replaces
|
|
// this with the original author (immutable attribution), but a full-payload PUT
|
|
// that upserts a rule with no existing row must not store the payload's author.
|
|
rule.Author = TenantHelper.GetUserDisplayName(req) ?? "Autopilot Monitor";
|
|
|
|
try
|
|
{
|
|
var success = await _ruleService.UpdateRuleAsync(tenantId, rule);
|
|
var response = req.CreateResponse(success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError);
|
|
await response.WriteAsJsonAsync(new SuccessMessageResponse { Success = success, Message = success ? "Rule updated" : "Failed to update rule" });
|
|
return response;
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
return await req.ConflictAsync(ex.Message);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return await req.BadRequestAsync(ex.Message);
|
|
}
|
|
}
|
|
|
|
[Function("CreateAnalyzeRuleFromTemplate")]
|
|
public async Task<HttpResponseData> CreateFromTemplate(
|
|
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "rules/analyze/{ruleId}/create-from-template")] HttpRequestData req,
|
|
string ruleId)
|
|
{
|
|
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
|
|
var tenantId = TenantHelper.GetTenantId(req);
|
|
|
|
var read = await req.ReadAsync<CreateAnalyzeRuleFromTemplateRequest>();
|
|
if (read.Error != null) return read.Error;
|
|
var variables = read.Value!.Variables;
|
|
|
|
if (variables == null || variables.Count == 0)
|
|
{
|
|
return await req.BadRequestAsync("Template variable values are required");
|
|
}
|
|
|
|
try
|
|
{
|
|
var author = TenantHelper.GetUserDisplayName(req) ?? "Autopilot Monitor";
|
|
var newRule = await _ruleService.CreateFromTemplateAsync(tenantId, ruleId, variables, author);
|
|
|
|
var response = req.CreateResponse(HttpStatusCode.Created);
|
|
await response.WriteAsJsonAsync(new CreateAnalyzeRuleFromTemplateResponse { Success = true, Rule = newRule, Message = "Custom rule created from template" });
|
|
return response;
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
return await req.ConflictAsync(ex.Message);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return await req.BadRequestAsync(ex.Message);
|
|
}
|
|
}
|
|
|
|
[Function("DeleteAnalyzeRule")]
|
|
public async Task<HttpResponseData> DeleteRule(
|
|
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "rules/analyze/{ruleId}")] HttpRequestData req,
|
|
string ruleId)
|
|
{
|
|
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
|
|
var tenantId = TenantHelper.GetTenantId(req);
|
|
|
|
// Load the rule to determine its type (built-in/community vs. custom)
|
|
var rules = await _ruleService.GetAllRulesForTenantAsync(tenantId);
|
|
var rule = rules.FirstOrDefault(r => r.RuleId == ruleId);
|
|
|
|
if (rule == null)
|
|
{
|
|
return await req.NotFoundAsync("Rule not found");
|
|
}
|
|
|
|
var success = await _ruleService.DeleteRuleAsync(tenantId, rule);
|
|
var response = req.CreateResponse(success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError);
|
|
await response.WriteAsJsonAsync(new SuccessMessageResponse { Success = success, Message = success ? "Rule deleted" : "Failed to delete rule" });
|
|
return response;
|
|
}
|
|
}
|
|
}
|