Skip to content

Commit 19ce6e9

Browse files
Copilotmarkcoleman
andcommitted
feat: Add PhishLabs incident reporting backend and frontend components
Co-authored-by: markcoleman <[email protected]>
1 parent bc3e319 commit 19ce6e9

File tree

8 files changed

+808
-0
lines changed

8 files changed

+808
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using System.ComponentModel.DataAnnotations;
3+
using UmbracoWeb.Models;
4+
using UmbracoWeb.Services;
5+
6+
namespace UmbracoWeb.Controllers;
7+
8+
/// <summary>
9+
/// API controller for PhishLabs incident reporting
10+
/// </summary>
11+
[ApiController]
12+
[Route("api/[controller]")]
13+
public class PhishLabsController : ControllerBase
14+
{
15+
private readonly IPhishLabsService _phishLabsService;
16+
private readonly ILogger<PhishLabsController> _logger;
17+
18+
public PhishLabsController(
19+
IPhishLabsService phishLabsService,
20+
ILogger<PhishLabsController> logger)
21+
{
22+
_phishLabsService = phishLabsService ?? throw new ArgumentNullException(nameof(phishLabsService));
23+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
24+
}
25+
26+
/// <summary>
27+
/// Submit a phishing incident report
28+
/// </summary>
29+
/// <param name="request">The incident request</param>
30+
/// <param name="cancellationToken">Cancellation token</param>
31+
/// <returns>Response indicating success or failure</returns>
32+
[HttpPost("incidents")]
33+
[ValidateAntiForgeryToken]
34+
public async Task<ActionResult<PhishLabsIncidentResponse>> SubmitIncident(
35+
[FromBody] PhishLabsIncidentRequest request,
36+
CancellationToken cancellationToken = default)
37+
{
38+
// Generate correlation ID for this request
39+
var correlationId = Guid.NewGuid().ToString("N")[..12];
40+
41+
// Get client IP for logging (but don't store PII)
42+
var clientIp = GetClientIpHash();
43+
44+
_logger.LogInformation("PhishLabs incident submission started. CorrelationId: {CorrelationId}, ClientIP: {ClientIpHash}",
45+
correlationId, clientIp);
46+
47+
// Validate model state
48+
if (!ModelState.IsValid)
49+
{
50+
_logger.LogWarning("Invalid PhishLabs incident request. CorrelationId: {CorrelationId}, Errors: {Errors}",
51+
correlationId, string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)));
52+
53+
return BadRequest(new PhishLabsIncidentResponse
54+
{
55+
Success = false,
56+
CorrelationId = correlationId,
57+
Message = "Please check your input and try again.",
58+
ErrorDetails = string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage))
59+
});
60+
}
61+
62+
try
63+
{
64+
// Submit to PhishLabs
65+
var response = await _phishLabsService.SubmitIncidentAsync(request, correlationId, cancellationToken);
66+
67+
if (response.Success)
68+
{
69+
_logger.LogInformation("PhishLabs incident submitted successfully. CorrelationId: {CorrelationId}", correlationId);
70+
return Ok(response);
71+
}
72+
else
73+
{
74+
_logger.LogWarning("PhishLabs incident submission failed. CorrelationId: {CorrelationId}", correlationId);
75+
return StatusCode(500, response);
76+
}
77+
}
78+
catch (ValidationException ex)
79+
{
80+
_logger.LogWarning(ex, "Validation error in PhishLabs incident submission. CorrelationId: {CorrelationId}", correlationId);
81+
82+
return BadRequest(new PhishLabsIncidentResponse
83+
{
84+
Success = false,
85+
CorrelationId = correlationId,
86+
Message = "Please check your input and try again.",
87+
ErrorDetails = ex.Message
88+
});
89+
}
90+
catch (Exception ex)
91+
{
92+
_logger.LogError(ex, "Unexpected error in PhishLabs incident submission. CorrelationId: {CorrelationId}", correlationId);
93+
94+
return StatusCode(500, new PhishLabsIncidentResponse
95+
{
96+
Success = false,
97+
CorrelationId = correlationId,
98+
Message = "Something went wrong — please try again. If it keeps failing, contact support.",
99+
ErrorDetails = "Internal server error"
100+
});
101+
}
102+
}
103+
104+
/// <summary>
105+
/// Health check endpoint for the PhishLabs integration
106+
/// </summary>
107+
[HttpGet("health")]
108+
public ActionResult<object> Health()
109+
{
110+
return Ok(new { status = "healthy", timestamp = DateTime.UtcNow });
111+
}
112+
113+
private string GetClientIpHash()
114+
{
115+
var remoteIp = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
116+
// Hash the IP for privacy - don't store actual IPs
117+
return remoteIp.GetHashCode().ToString("X8");
118+
}
119+
}

src/UmbracoWeb/UmbracoWeb/Program.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using Serilog;
22
using Umbraco.Cms.Core.DependencyInjection;
33
using Umbraco.Extensions;
4+
using UmbracoWeb.Models;
5+
using UmbracoWeb.Services;
46

57
// Configure Serilog
68
Log.Logger = new LoggerConfiguration()
@@ -16,6 +18,34 @@
1618
// Add Serilog
1719
builder.Host.UseSerilog();
1820

21+
// Configure PhishLabs settings
22+
builder.Services.Configure<PhishLabsSettings>(
23+
builder.Configuration.GetSection(PhishLabsSettings.SectionName));
24+
25+
// Add HTTP client for PhishLabs service
26+
builder.Services.AddHttpClient<IPhishLabsService, PhishLabsService>();
27+
28+
// Register PhishLabs service
29+
builder.Services.AddScoped<IPhishLabsService, PhishLabsService>();
30+
31+
// Add antiforgery services
32+
builder.Services.AddAntiforgery(options =>
33+
{
34+
options.HeaderName = "X-CSRF-TOKEN";
35+
options.SuppressXFrameOptionsHeader = false;
36+
});
37+
38+
// Add CORS policy
39+
builder.Services.AddCors(options =>
40+
{
41+
options.AddPolicy("PhishLabsPolicy", policy =>
42+
{
43+
policy.AllowAnyOrigin()
44+
.AllowAnyHeader()
45+
.WithMethods("POST");
46+
});
47+
});
48+
1949
// Add Umbraco
2050
builder.CreateUmbracoBuilder()
2151
.AddBackOffice()
@@ -29,6 +59,9 @@
2959
// Configure the HTTP request pipeline
3060
await app.BootUmbracoAsync();
3161

62+
// Use CORS
63+
app.UseCors("PhishLabsPolicy");
64+
3265
app.UseUmbraco()
3366
.WithMiddleware(u =>
3467
{
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using UmbracoWeb.Models;
2+
3+
namespace UmbracoWeb.Services;
4+
5+
/// <summary>
6+
/// Service interface for PhishLabs incident reporting
7+
/// </summary>
8+
public interface IPhishLabsService
9+
{
10+
/// <summary>
11+
/// Submit a phishing incident to PhishLabs
12+
/// </summary>
13+
/// <param name="request">The incident request</param>
14+
/// <param name="correlationId">Correlation ID for tracking</param>
15+
/// <param name="cancellationToken">Cancellation token</param>
16+
/// <returns>Response from PhishLabs</returns>
17+
Task<PhishLabsIncidentResponse> SubmitIncidentAsync(
18+
PhishLabsIncidentRequest request,
19+
string correlationId,
20+
CancellationToken cancellationToken = default);
21+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
using Microsoft.Extensions.Options;
4+
using UmbracoWeb.Models;
5+
6+
namespace UmbracoWeb.Services;
7+
8+
/// <summary>
9+
/// Service for integrating with PhishLabs incident reporting API
10+
/// </summary>
11+
public class PhishLabsService : IPhishLabsService
12+
{
13+
private readonly HttpClient _httpClient;
14+
private readonly PhishLabsSettings _settings;
15+
private readonly ILogger<PhishLabsService> _logger;
16+
private readonly JsonSerializerOptions _jsonOptions;
17+
18+
public PhishLabsService(
19+
HttpClient httpClient,
20+
IOptions<PhishLabsSettings> settings,
21+
ILogger<PhishLabsService> logger)
22+
{
23+
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
24+
_settings = settings?.Value ?? throw new ArgumentNullException(nameof(settings));
25+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
26+
27+
_jsonOptions = new JsonSerializerOptions
28+
{
29+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
30+
WriteIndented = false
31+
};
32+
33+
ConfigureHttpClient();
34+
}
35+
36+
/// <summary>
37+
/// Submit a phishing incident to PhishLabs
38+
/// </summary>
39+
public async Task<PhishLabsIncidentResponse> SubmitIncidentAsync(
40+
PhishLabsIncidentRequest request,
41+
string correlationId,
42+
CancellationToken cancellationToken = default)
43+
{
44+
if (request == null)
45+
throw new ArgumentNullException(nameof(request));
46+
47+
if (string.IsNullOrWhiteSpace(correlationId))
48+
throw new ArgumentException("Correlation ID is required", nameof(correlationId));
49+
50+
_logger.LogInformation("Submitting PhishLabs incident. CorrelationId: {CorrelationId}, URL: {UrlHash}",
51+
correlationId, ComputeUrlHash(request.Url));
52+
53+
try
54+
{
55+
var apiRequest = new PhishLabsApiRequest
56+
{
57+
Url = SanitizeUrl(request.Url),
58+
Description = SanitizeDescription(request.Details),
59+
Source = "umbraco-web",
60+
Timestamp = DateTime.UtcNow
61+
};
62+
63+
var response = await SubmitToPhishLabsAsync(apiRequest, correlationId, cancellationToken);
64+
65+
if (response.Success)
66+
{
67+
_logger.LogInformation("PhishLabs incident submitted successfully. CorrelationId: {CorrelationId}, IncidentId: {IncidentId}",
68+
correlationId, response.IncidentId);
69+
70+
return new PhishLabsIncidentResponse
71+
{
72+
Success = true,
73+
CorrelationId = correlationId,
74+
Message = "Thanks — we received your report and are investigating. If this affects your account, we'll contact you."
75+
};
76+
}
77+
else
78+
{
79+
_logger.LogWarning("PhishLabs incident submission failed. CorrelationId: {CorrelationId}, Error: {Error}",
80+
correlationId, response.Error);
81+
82+
return new PhishLabsIncidentResponse
83+
{
84+
Success = false,
85+
CorrelationId = correlationId,
86+
Message = "Something went wrong — please try again.",
87+
ErrorDetails = response.Error
88+
};
89+
}
90+
}
91+
catch (Exception ex)
92+
{
93+
_logger.LogError(ex, "Error submitting PhishLabs incident. CorrelationId: {CorrelationId}", correlationId);
94+
95+
return new PhishLabsIncidentResponse
96+
{
97+
Success = false,
98+
CorrelationId = correlationId,
99+
Message = "Something went wrong — please try again. If it keeps failing, contact support.",
100+
ErrorDetails = ex.Message
101+
};
102+
}
103+
}
104+
105+
private void ConfigureHttpClient()
106+
{
107+
_httpClient.BaseAddress = new Uri(_settings.ApiBaseUrl);
108+
_httpClient.Timeout = TimeSpan.FromSeconds(_settings.TimeoutSeconds);
109+
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_settings.ApiKey}");
110+
_httpClient.DefaultRequestHeaders.Add("User-Agent", "Umbraco-Web-PhishLabs-Integration/1.0");
111+
}
112+
113+
private async Task<PhishLabsApiResponse> SubmitToPhishLabsAsync(
114+
PhishLabsApiRequest request,
115+
string correlationId,
116+
CancellationToken cancellationToken)
117+
{
118+
var endpoint = _settings.ServicePath.TrimStart('/');
119+
var json = JsonSerializer.Serialize(request, _jsonOptions);
120+
var content = new StringContent(json, Encoding.UTF8, "application/json");
121+
122+
content.Headers.Add("X-Correlation-ID", correlationId);
123+
124+
var httpResponse = await _httpClient.PostAsync(endpoint, content, cancellationToken);
125+
126+
if (httpResponse.IsSuccessStatusCode)
127+
{
128+
var responseContent = await httpResponse.Content.ReadAsStringAsync(cancellationToken);
129+
var apiResponse = JsonSerializer.Deserialize<PhishLabsApiResponse>(responseContent, _jsonOptions);
130+
131+
return apiResponse ?? new PhishLabsApiResponse
132+
{
133+
Success = false,
134+
Error = "Invalid response format"
135+
};
136+
}
137+
else
138+
{
139+
var errorContent = await httpResponse.Content.ReadAsStringAsync(cancellationToken);
140+
return new PhishLabsApiResponse
141+
{
142+
Success = false,
143+
Error = $"HTTP {(int)httpResponse.StatusCode}: {errorContent}"
144+
};
145+
}
146+
}
147+
148+
private static string SanitizeUrl(string url)
149+
{
150+
if (string.IsNullOrWhiteSpace(url))
151+
return string.Empty;
152+
153+
// Basic URL sanitization - remove any dangerous characters
154+
return url.Trim().Replace("\r", "").Replace("\n", "");
155+
}
156+
157+
private static string? SanitizeDescription(string? description)
158+
{
159+
if (string.IsNullOrWhiteSpace(description))
160+
return null;
161+
162+
// Basic description sanitization
163+
return description.Trim().Replace("\r\n", "\n").Replace("\r", "\n");
164+
}
165+
166+
private static string ComputeUrlHash(string url)
167+
{
168+
// Create a simple hash for logging (no PII)
169+
return url.GetHashCode().ToString("X8");
170+
}
171+
}

src/UmbracoWeb/UmbracoWeb/UmbracoWeb.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
1313
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
1414
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
15+
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
1516
</ItemGroup>
1617

1718
</Project>

0 commit comments

Comments
 (0)