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+ }
0 commit comments