Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions SCV.sln
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "db", "db\db.csproj", "{E15B
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pcss-client", "pcss-client\pcss-client.csproj", "{C1736008-CC5B-40AC-A0A4-48C85E49067F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dars-client", "dars-client\dars-client.csproj", "{764F912A-D0FA-4272-ADEF-1335B8BE8D9C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -38,6 +40,10 @@ Global
{C1736008-CC5B-40AC-A0A4-48C85E49067F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1736008-CC5B-40AC-A0A4-48C85E49067F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1736008-CC5B-40AC-A0A4-48C85E49067F}.Release|Any CPU.Build.0 = Release|Any CPU
{764F912A-D0FA-4272-ADEF-1335B8BE8D9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{764F912A-D0FA-4272-ADEF-1335B8BE8D9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{764F912A-D0FA-4272-ADEF-1335B8BE8D9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{764F912A-D0FA-4272-ADEF-1335B8BE8D9C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
94 changes: 94 additions & 0 deletions api/Controllers/DarsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using DARSCommon.Clients.LogNotesServices;
using DARSCommon.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Scv.Api.Infrastructure.Authorization;
using Scv.Api.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Scv.Api.Controllers
{
[Authorize(AuthenticationSchemes = "SiteMinder, OpenIdConnect", Policy = nameof(ProviderAuthorizationHandler))]
[Route("api/[controller]")]
[ApiController]
public class DarsController(IDarsService darsService, ILogger<DarsController> logger) : ControllerBase
{
/// <summary>
/// Search for DARS audio recordings by date, location, and court room.
/// </summary>
/// <param name="date">The date to search for recordings</param>
/// <param name="locationId">The location ID</param>
/// <param name="courtRoomCd">The court room code</param>
/// <returns>A collection of audio recording log notes</returns>
[HttpGet("search")]
[ProducesResponseType(typeof(IEnumerable<DarsSearchResults>), 200)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> Search(DateTime date, int locationId, string courtRoomCd)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also need validation for date and courtRoomCd?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so - Date is already non-nullable by definition, and the courtRoomCd is optional.

{
logger.LogInformation(
"DARS search requested - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
date,
locationId,
courtRoomCd

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 3 days ago

The best way to fix this is to sanitize the user input before passing it to the logger. For this case, since the log entries are plain text, we should strip/remove newline characters (\r, \n) and possibly other dangerous control characters from courtRoomCd before logging it. This should be done immediately prior to logging calls that might include unsanitized user input.

Specifically, in api/Controllers/DarsController.cs, we should define a helper function or inline logic to sanitize courtRoomCd before passing to each logger invocation (LogInformation, LogError, etc.) that includes this user input. We do not need to sanitize the value used for API calls, only for logging.

We do not need new libraries; standard string replacement or regex can be used.

Suggested changeset 1
api/Controllers/DarsController.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/Controllers/DarsController.cs b/api/Controllers/DarsController.cs
--- a/api/Controllers/DarsController.cs
+++ b/api/Controllers/DarsController.cs
@@ -31,11 +31,13 @@
         [ProducesResponseType(500)]
         public async Task<IActionResult> Search(DateTime date, int locationId, string courtRoomCd)
         {
+            // Sanitize user input before logging to prevent log forging
+            var sanitizedCourtRoomCd = string.IsNullOrEmpty(courtRoomCd) ? "" : courtRoomCd.Replace("\r", "").Replace("\n", "");
             logger.LogInformation(
                 "DARS search requested - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
                 date,
                 locationId,
-                courtRoomCd
+                sanitizedCourtRoomCd
             );
 
             // Validate input parameters
@@ -55,7 +53,7 @@
                         "No DARS recordings found for Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
                         date,
                         locationId,
-                        courtRoomCd
+                        sanitizedCourtRoomCd
                     );
                     return NotFound();
                 }
@@ -65,7 +63,7 @@
                     result.Count(),
                     date,
                     locationId,
-                    courtRoomCd
+                    sanitizedCourtRoomCd
                 );
 
                 return Ok(result);
@@ -77,7 +75,7 @@
                     "DARS API exception while searching - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}, Status: {StatusCode}",
                     date,
                     locationId,
-                    courtRoomCd,
+                    sanitizedCourtRoomCd,
                     ex.StatusCode
                 );
 
EOF
@@ -31,11 +31,13 @@
[ProducesResponseType(500)]
public async Task<IActionResult> Search(DateTime date, int locationId, string courtRoomCd)
{
// Sanitize user input before logging to prevent log forging
var sanitizedCourtRoomCd = string.IsNullOrEmpty(courtRoomCd) ? "" : courtRoomCd.Replace("\r", "").Replace("\n", "");
logger.LogInformation(
"DARS search requested - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
date,
locationId,
courtRoomCd
sanitizedCourtRoomCd
);

// Validate input parameters
@@ -55,7 +53,7 @@
"No DARS recordings found for Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
date,
locationId,
courtRoomCd
sanitizedCourtRoomCd
);
return NotFound();
}
@@ -65,7 +63,7 @@
result.Count(),
date,
locationId,
courtRoomCd
sanitizedCourtRoomCd
);

return Ok(result);
@@ -77,7 +75,7 @@
"DARS API exception while searching - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}, Status: {StatusCode}",
date,
locationId,
courtRoomCd,
sanitizedCourtRoomCd,
ex.StatusCode
);

Copilot is powered by AI and may make mistakes. Always verify output.
);

// Validate input parameters
if (locationId <= 0)
{
logger.LogWarning("Invalid locationId provided: {LocationId}", locationId);
return BadRequest("LocationId must be greater than 0.");
}

try
{
var result = await darsService.DarsApiSearch(date, locationId, courtRoomCd);

if (result == null || !result.Any())
{
logger.LogInformation(
"No DARS recordings found for Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
date,
locationId,
courtRoomCd

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 3 days ago

To fix this issue, sanitize the user-provided courtRoomCd value before it is logged. For a plain text log, this means stripping newline (\n, \r) characters from courtRoomCd before passing it to the logger. This can be accomplished using string.Replace, replacing both Environment.NewLine and \n/\r characters, producing a version safe to log.

  • Where to make the change:
    Sanitize courtRoomCd within the Search action prior to any log statement.
  • How:
    Define a local safeCourtRoomCd variable that removes line breaks from courtRoomCd, and use safeCourtRoomCd in all logging method calls instead of the raw value.
  • Methods/imports/definitions needed:
    Only basic string.Replace functionality and Environment.NewLine are required (already available).

Suggested changeset 1
api/Controllers/DarsController.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/Controllers/DarsController.cs b/api/Controllers/DarsController.cs
--- a/api/Controllers/DarsController.cs
+++ b/api/Controllers/DarsController.cs
@@ -31,11 +31,13 @@
         [ProducesResponseType(500)]
         public async Task<IActionResult> Search(DateTime date, int locationId, string courtRoomCd)
         {
+            // Sanitize courtRoomCd before logging
+            var safeCourtRoomCd = courtRoomCd?.Replace("\r", "").Replace("\n", "");
             logger.LogInformation(
                 "DARS search requested - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
                 date,
                 locationId,
-                courtRoomCd
+                safeCourtRoomCd
             );
 
             // Validate input parameters
@@ -55,7 +53,7 @@
                         "No DARS recordings found for Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
                         date,
                         locationId,
-                        courtRoomCd
+                        safeCourtRoomCd
                     );
                     return NotFound();
                 }
@@ -65,7 +63,7 @@
                     result.Count(),
                     date,
                     locationId,
-                    courtRoomCd
+                    safeCourtRoomCd
                 );
 
                 return Ok(result);
@@ -77,7 +75,7 @@
                     "DARS API exception while searching - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}, Status: {StatusCode}",
                     date,
                     locationId,
-                    courtRoomCd,
+                    safeCourtRoomCd,
                     ex.StatusCode
                 );
 
EOF
@@ -31,11 +31,13 @@
[ProducesResponseType(500)]
public async Task<IActionResult> Search(DateTime date, int locationId, string courtRoomCd)
{
// Sanitize courtRoomCd before logging
var safeCourtRoomCd = courtRoomCd?.Replace("\r", "").Replace("\n", "");
logger.LogInformation(
"DARS search requested - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
date,
locationId,
courtRoomCd
safeCourtRoomCd
);

// Validate input parameters
@@ -55,7 +53,7 @@
"No DARS recordings found for Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
date,
locationId,
courtRoomCd
safeCourtRoomCd
);
return NotFound();
}
@@ -65,7 +63,7 @@
result.Count(),
date,
locationId,
courtRoomCd
safeCourtRoomCd
);

return Ok(result);
@@ -77,7 +75,7 @@
"DARS API exception while searching - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}, Status: {StatusCode}",
date,
locationId,
courtRoomCd,
safeCourtRoomCd,
ex.StatusCode
);

Copilot is powered by AI and may make mistakes. Always verify output.
);
return NotFound();
}

logger.LogInformation(
"Found {Count} DARS recording(s) for Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
result.Count(),
date,
locationId,
courtRoomCd

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 3 days ago

To fix this problem, all uses of courtRoomCd in log messages should sanitize, encode, or otherwise neutralize unsafe characters (such as newlines or control characters) before being logged. The most practical remedy for plain text logs is to remove (or substitute) newline and carriage return characters. The best approach is to replace any \r and \n characters in courtRoomCd with an empty string or a placeholder such as space or underscore. This change should be made directly in all log calls where courtRoomCd is written.

Specifically, in api/Controllers/DarsController.cs, sanitize courtRoomCd on all log calls (lines 38, 58, 68, and 80). It's best to define a local variable, e.g., sanitizedCourtRoomCd, early in the method after receiving input. This avoids code duplication and guarantees consistency. Use courtRoomCd.Replace("\r", "").Replace("\n", "") as the sanitization.

No imports are needed for this change.


Suggested changeset 1
api/Controllers/DarsController.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/Controllers/DarsController.cs b/api/Controllers/DarsController.cs
--- a/api/Controllers/DarsController.cs
+++ b/api/Controllers/DarsController.cs
@@ -31,11 +31,14 @@
         [ProducesResponseType(500)]
         public async Task<IActionResult> Search(DateTime date, int locationId, string courtRoomCd)
         {
+            // Sanitize courtRoomCd for logging to prevent log forging
+            var sanitizedCourtRoomCd = courtRoomCd?.Replace("\r", "").Replace("\n", "");
+
             logger.LogInformation(
                 "DARS search requested - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
                 date,
                 locationId,
-                courtRoomCd
+                sanitizedCourtRoomCd
             );
 
             // Validate input parameters
@@ -55,7 +54,7 @@
                         "No DARS recordings found for Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
                         date,
                         locationId,
-                        courtRoomCd
+                        sanitizedCourtRoomCd
                     );
                     return NotFound();
                 }
@@ -65,7 +64,7 @@
                     result.Count(),
                     date,
                     locationId,
-                    courtRoomCd
+                    sanitizedCourtRoomCd
                 );
 
                 return Ok(result);
@@ -77,7 +76,7 @@
                     "DARS API exception while searching - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}, Status: {StatusCode}",
                     date,
                     locationId,
-                    courtRoomCd,
+                    sanitizedCourtRoomCd,
                     ex.StatusCode
                 );
 
EOF
@@ -31,11 +31,14 @@
[ProducesResponseType(500)]
public async Task<IActionResult> Search(DateTime date, int locationId, string courtRoomCd)
{
// Sanitize courtRoomCd for logging to prevent log forging
var sanitizedCourtRoomCd = courtRoomCd?.Replace("\r", "").Replace("\n", "");

logger.LogInformation(
"DARS search requested - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
date,
locationId,
courtRoomCd
sanitizedCourtRoomCd
);

// Validate input parameters
@@ -55,7 +54,7 @@
"No DARS recordings found for Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
date,
locationId,
courtRoomCd
sanitizedCourtRoomCd
);
return NotFound();
}
@@ -65,7 +64,7 @@
result.Count(),
date,
locationId,
courtRoomCd
sanitizedCourtRoomCd
);

return Ok(result);
@@ -77,7 +76,7 @@
"DARS API exception while searching - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}, Status: {StatusCode}",
date,
locationId,
courtRoomCd,
sanitizedCourtRoomCd,
ex.StatusCode
);

Copilot is powered by AI and may make mistakes. Always verify output.
);

return Ok(result);
}
catch (ApiException ex)
{
logger.LogError(
ex,
"DARS API exception while searching - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}, Status: {StatusCode}",
date,
locationId,
courtRoomCd,

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 3 days ago

To mitigate the risk of log forging, any user-provided data logged must be sanitized to remove line breaks and other characters that can disrupt log file structure. For this scenario, the best fix is to sanitize the courtRoomCd value before passing it to the logger, specifically by replacing or removing newline (\n) and carriage return (\r) characters (and optionally, tab characters for completeness). The fix should apply consistently to all log calls within this method that log courtRoomCd.

This can be done in one of two ways:

  • Create a local sanitized variable (e.g., sanitizedCourtRoomCd) at the start of the method, sanitize courtRoomCd (removing/replacing problematic characters), and use this variable in all log calls.
  • Alternatively, sanitize courtRoomCd inline at the logging points, but this is less maintainable and DRY.

The best-practices approach is to add a helper for sanitizing log fields, but as we must confine the fix to the shown snippet, we'll inline a variable in the method. No additional method or import is needed, just use Replace on the string.

All log statements referencing courtRoomCd will be updated to use the sanitized value.


Suggested changeset 1
api/Controllers/DarsController.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/Controllers/DarsController.cs b/api/Controllers/DarsController.cs
--- a/api/Controllers/DarsController.cs
+++ b/api/Controllers/DarsController.cs
@@ -31,11 +31,16 @@
         [ProducesResponseType(500)]
         public async Task<IActionResult> Search(DateTime date, int locationId, string courtRoomCd)
         {
+            // Sanitize courtRoomCd before logging to prevent log forging
+            var sanitizedCourtRoomCd = (courtRoomCd ?? string.Empty)
+                .Replace("\r", "")
+                .Replace("\n", "");
+
             logger.LogInformation(
                 "DARS search requested - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
                 date,
                 locationId,
-                courtRoomCd
+                sanitizedCourtRoomCd
             );
 
             // Validate input parameters
@@ -55,7 +56,7 @@
                         "No DARS recordings found for Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
                         date,
                         locationId,
-                        courtRoomCd
+                        sanitizedCourtRoomCd
                     );
                     return NotFound();
                 }
@@ -65,7 +66,7 @@
                     result.Count(),
                     date,
                     locationId,
-                    courtRoomCd
+                    sanitizedCourtRoomCd
                 );
 
                 return Ok(result);
@@ -77,7 +78,7 @@
                     "DARS API exception while searching - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}, Status: {StatusCode}",
                     date,
                     locationId,
-                    courtRoomCd,
+                    sanitizedCourtRoomCd,
                     ex.StatusCode
                 );
 
EOF
@@ -31,11 +31,16 @@
[ProducesResponseType(500)]
public async Task<IActionResult> Search(DateTime date, int locationId, string courtRoomCd)
{
// Sanitize courtRoomCd before logging to prevent log forging
var sanitizedCourtRoomCd = (courtRoomCd ?? string.Empty)
.Replace("\r", "")
.Replace("\n", "");

logger.LogInformation(
"DARS search requested - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
date,
locationId,
courtRoomCd
sanitizedCourtRoomCd
);

// Validate input parameters
@@ -55,7 +56,7 @@
"No DARS recordings found for Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
date,
locationId,
courtRoomCd
sanitizedCourtRoomCd
);
return NotFound();
}
@@ -65,7 +66,7 @@
result.Count(),
date,
locationId,
courtRoomCd
sanitizedCourtRoomCd
);

return Ok(result);
@@ -77,7 +78,7 @@
"DARS API exception while searching - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}, Status: {StatusCode}",
date,
locationId,
courtRoomCd,
sanitizedCourtRoomCd,
ex.StatusCode
);

Copilot is powered by AI and may make mistakes. Always verify output.
ex.StatusCode
);

// Return 404 for not found from upstream API
if (ex.StatusCode == 404)
{
return NotFound();
}

return StatusCode(500, "An error occurred while searching for audio recordings.");
}
}
}
}
19 changes: 19 additions & 0 deletions api/Infrastructure/Mappings/DarsMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Mapster;
using DARSCommon.Clients.LogNotesServices;
using DARSCommon.Models;

namespace Scv.Api.Infrastructure.Mappings;

public class DarsMapping : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<Lognotes, DarsSearchResults>()
.Map(dest => dest.Date, src => src.DateTime)
.Map(dest => dest.LocationId, src => src.Location)
.Map(dest => dest.CourtRoomCd, src => src.Room)
.Map(dest => dest.Url, src => src.Url)
.Map(dest => dest.FileName, src => src.FileName)
.Map(dest => dest.LocationNm, src => src.LocationName);
}
}
7 changes: 7 additions & 0 deletions api/Infrastructure/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
using PCSSPersonServices = PCSSCommon.Clients.PersonServices;
using PCSSReportServices = PCSSCommon.Clients.ReportServices;
using PCSSSearchDateServices = PCSSCommon.Clients.SearchDateServices;
using LogNotesServices = DARSCommon.Clients.LogNotesServices;
using Microsoft.AspNetCore.Hosting;

namespace Scv.Api.Infrastructure
{
Expand Down Expand Up @@ -186,6 +188,9 @@ public static IServiceCollection AddHttpClientsAndScvServices(this IServiceColle
services
.AddHttpClient<PCSSPersonServices.PersonServicesClient>(client => { ConfigureHttpClient(client, configuration, "PCSS"); })
.AddHttpMessageHandler<TimingHandler>();
services
.AddHttpClient<LogNotesServices.LogNotesServicesClient>(client => { ConfigureHttpClient(client, configuration, "DARS"); })
.AddHttpMessageHandler<TimingHandler>();

services.AddHttpContextAccessor();
services.AddTransient(s => s.GetService<IHttpContextAccessor>()?.HttpContext?.User);
Expand All @@ -194,6 +199,7 @@ public static IServiceCollection AddHttpClientsAndScvServices(this IServiceColle
services.AddScoped<LocationService>();
services.AddScoped<CourtListService>();
services.AddScoped<VcCivilFileAccessHandler>();
services.AddScoped<DarsService>();
services.AddSingleton<JCUserService>();
services.AddSingleton<AesGcmEncryption>();
services.AddSingleton<JudicialCalendarService>();
Expand All @@ -210,6 +216,7 @@ public static IServiceCollection AddHttpClientsAndScvServices(this IServiceColle
services.AddScoped<ICrudService<GroupDto>, GroupService>();
services.AddScoped<ICaseService, CaseService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IDarsService, DarsService>();
services.AddScoped<IBinderFactory, BinderFactory>();
services.AddScoped<IBinderService, BinderService>();
services.AddTransient<IRecurringJob, SyncDocumentCategoriesJob>();
Expand Down
1 change: 1 addition & 0 deletions api/Models/Civil/Appearances/CivilAppearance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class CivilAppearance : JCCommon.Clients.FileServices.CivilAppearanceDeta
public string AppearanceStatusDsc { get; set; }
public string CourtLocationId { get; set; }
public string CourtLocation { get; set; }
public string LocationId { get; set; } //PCSS - DARS location id.
public string DocumentTypeDsc { get; set; }
[AdaptIgnore]
[JsonIgnore]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class CriminalAppearanceDetail : JCCommon.Clients.FileServices.CriminalAp
public string AppearanceReasonDsc { get; set; }
public string AppearanceResultDsc { get; set; }
public string AppearanceStatusDsc { get; set; }
public string LocationId { get; set; } // The location id from PCSS, DARS.
public string CourtLocationId { get; set; }
public string CourtLocation { get; set; }
[AdaptIgnore]
Expand Down
81 changes: 81 additions & 0 deletions api/Services/DarsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DARSCommon.Clients.LogNotesServices;
using DARSCommon.Models;
using MapsterMapper;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Scv.Api.Helpers;

namespace Scv.Api.Services
{
public interface IDarsService
{
Task<IEnumerable<DarsSearchResults>> DarsApiSearch(DateTime date, int locationId, string courtRoomCd);
}
public class DarsService(
IConfiguration configuration,
ILogger<DarsService> logger,
LogNotesServicesClient logNotesServicesClient,
IMapper mapper) : IDarsService
{

#region Variables
private readonly string _logsheetBaseUrl = configuration.GetNonEmptyValue("DARS:LogsheetUrl");

#endregion Variables

public async Task<IEnumerable<DarsSearchResults>> DarsApiSearch(DateTime date, int locationId, string courtRoomCd)
{
logger.LogInformation("DarsApiSearch called for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", locationId, courtRoomCd, date.ToString("yyyy-MM-dd"));

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 3 days ago

To fix the log forging vulnerability, you must sanitize the courtRoomCd value before writing it to any log entry. The recommended approach for plain text logs is to remove all newline characters from the user input via e.g., Replace("\r", "") and Replace("\n", ""), as this prevents log entry splitting.
The change should be made in api/Services/DarsService.cs at the logging call sites (lines 32 and 34), or better, at the start of the method, by creating a sanitized local variable and always using that for logging.
No new methods are needed, but you will need to assign (or define) the sanitized version for all log entries in this method.

Steps:

  • In DarsApiSearch, create a sanitized version of courtRoomCd at the start (e.g., courtRoomCdSanitized).
  • Use this sanitized value in all logger calls in this method.
  • Do not change logic elsewhere (e.g., for the call to downstream services, keep passing the original value).
  • No new imports required, as string replacement functions are in the .NET base.

Suggested changeset 1
api/Services/DarsService.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/Services/DarsService.cs b/api/Services/DarsService.cs
--- a/api/Services/DarsService.cs
+++ b/api/Services/DarsService.cs
@@ -29,9 +29,10 @@
 
         public async Task<IEnumerable<DarsSearchResults>> DarsApiSearch(DateTime date, int locationId, string courtRoomCd)
         {
-            logger.LogInformation("DarsApiSearch called for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", locationId, courtRoomCd, date.ToString("yyyy-MM-dd"));
+            var courtRoomCdSanitized = courtRoomCd?.Replace("\r", "").Replace("\n", "");
+            logger.LogInformation("DarsApiSearch called for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", locationId, courtRoomCdSanitized, date.ToString("yyyy-MM-dd"));
             var darsResult = await logNotesServicesClient.GetBaseAsync(room: courtRoomCd, datetime: date, location: locationId);
-            logger.LogInformation("DarsApiSearch returned {ResultCount} results for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", darsResult?.Count() ?? 0, locationId, courtRoomCd, date.ToString("yyyy-MM-dd"));
+            logger.LogInformation("DarsApiSearch returned {ResultCount} results for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", darsResult?.Count() ?? 0, locationId, courtRoomCdSanitized, date.ToString("yyyy-MM-dd"));
             var mappedResults = mapper.Map<IEnumerable<DarsSearchResults>>(darsResult).ToList();
 
             // Use LINQ's Select to append the base URL to each result's Url property
EOF
@@ -29,9 +29,10 @@

public async Task<IEnumerable<DarsSearchResults>> DarsApiSearch(DateTime date, int locationId, string courtRoomCd)
{
logger.LogInformation("DarsApiSearch called for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", locationId, courtRoomCd, date.ToString("yyyy-MM-dd"));
var courtRoomCdSanitized = courtRoomCd?.Replace("\r", "").Replace("\n", "");
logger.LogInformation("DarsApiSearch called for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", locationId, courtRoomCdSanitized, date.ToString("yyyy-MM-dd"));
var darsResult = await logNotesServicesClient.GetBaseAsync(room: courtRoomCd, datetime: date, location: locationId);
logger.LogInformation("DarsApiSearch returned {ResultCount} results for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", darsResult?.Count() ?? 0, locationId, courtRoomCd, date.ToString("yyyy-MM-dd"));
logger.LogInformation("DarsApiSearch returned {ResultCount} results for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", darsResult?.Count() ?? 0, locationId, courtRoomCdSanitized, date.ToString("yyyy-MM-dd"));
var mappedResults = mapper.Map<IEnumerable<DarsSearchResults>>(darsResult).ToList();

// Use LINQ's Select to append the base URL to each result's Url property
Copilot is powered by AI and may make mistakes. Always verify output.
var darsResult = await logNotesServicesClient.GetBaseAsync(room: courtRoomCd, datetime: date, location: locationId);
logger.LogInformation("DarsApiSearch returned {ResultCount} results for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", darsResult?.Count() ?? 0, locationId, courtRoomCd, date.ToString("yyyy-MM-dd"));

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 3 days ago

To fix the vulnerability, all user-provided data that is written to logs should have log-forgery dangerous characters sanitized, particularly newlines (\r, \n). The fix here is to sanitize courtRoomCd before logging in all affected logger statements. The best way is to use string.Replace (or a small utility function) to remove or replace newlines and other control characters from the value, immediately before it is logged. Because the data is used as a parameter and not via string interpolation, you only need to sanitize the instance being logged, not the value used elsewhere.

There are two main code locations where unsanitized logging of courtRoomCd occurs:

  1. In api/Controllers/DarsController.cs, inside the Search method, lines 38 (logger), 58 (logger), and 68 (logger).
  2. In api/Services/DarsService.cs, inside DarsApiSearch, lines 32 and 34 (logger calls).

Both files should sanitize courtRoomCd before logging. This is best done inline for clarity:

  • Before logging, produce a sanitized copy (e.g., SanitizeForLog(courtRoomCd)) and use this in all affected logger.LogInformation calls.

A static utility function such as:

private static string SanitizeForLog(string input) =>
    input?.Replace("\r", "").Replace("\n", "");

can be added to both files for reusability and clarity.

Required changes:

  • Add a static helper function to sanitize loggable values.
  • Use the helper to sanitize courtRoomCd in all logger calls in both affected files.

Suggested changeset 2
api/Services/DarsService.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/Services/DarsService.cs b/api/Services/DarsService.cs
--- a/api/Services/DarsService.cs
+++ b/api/Services/DarsService.cs
@@ -29,9 +29,10 @@
 
         public async Task<IEnumerable<DarsSearchResults>> DarsApiSearch(DateTime date, int locationId, string courtRoomCd)
         {
-            logger.LogInformation("DarsApiSearch called for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", locationId, courtRoomCd, date.ToString("yyyy-MM-dd"));
+            var sanitizedCourtRoomCd = SanitizeForLog(courtRoomCd);
+            logger.LogInformation("DarsApiSearch called for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", locationId, sanitizedCourtRoomCd, date.ToString("yyyy-MM-dd"));
             var darsResult = await logNotesServicesClient.GetBaseAsync(room: courtRoomCd, datetime: date, location: locationId);
-            logger.LogInformation("DarsApiSearch returned {ResultCount} results for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", darsResult?.Count() ?? 0, locationId, courtRoomCd, date.ToString("yyyy-MM-dd"));
+            logger.LogInformation("DarsApiSearch returned {ResultCount} results for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", darsResult?.Count() ?? 0, locationId, sanitizedCourtRoomCd, date.ToString("yyyy-MM-dd"));
             var mappedResults = mapper.Map<IEnumerable<DarsSearchResults>>(darsResult).ToList();
 
             // Use LINQ's Select to append the base URL to each result's Url property
@@ -77,5 +77,8 @@
             }
             return filteredResults;
         }
+        // Helper to sanitize strings before logging
+        private static string SanitizeForLog(string input) =>
+            input?.Replace("\r", "").Replace("\n", "");
     }
 }
\ No newline at end of file
EOF
@@ -29,9 +29,10 @@

public async Task<IEnumerable<DarsSearchResults>> DarsApiSearch(DateTime date, int locationId, string courtRoomCd)
{
logger.LogInformation("DarsApiSearch called for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", locationId, courtRoomCd, date.ToString("yyyy-MM-dd"));
var sanitizedCourtRoomCd = SanitizeForLog(courtRoomCd);
logger.LogInformation("DarsApiSearch called for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", locationId, sanitizedCourtRoomCd, date.ToString("yyyy-MM-dd"));
var darsResult = await logNotesServicesClient.GetBaseAsync(room: courtRoomCd, datetime: date, location: locationId);
logger.LogInformation("DarsApiSearch returned {ResultCount} results for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", darsResult?.Count() ?? 0, locationId, courtRoomCd, date.ToString("yyyy-MM-dd"));
logger.LogInformation("DarsApiSearch returned {ResultCount} results for LocationId: {LocationId}, CourtRoomCd: {CourtRoomCd}, Date: {Date}", darsResult?.Count() ?? 0, locationId, sanitizedCourtRoomCd, date.ToString("yyyy-MM-dd"));
var mappedResults = mapper.Map<IEnumerable<DarsSearchResults>>(darsResult).ToList();

// Use LINQ's Select to append the base URL to each result's Url property
@@ -77,5 +77,8 @@
}
return filteredResults;
}
// Helper to sanitize strings before logging
private static string SanitizeForLog(string input) =>
input?.Replace("\r", "").Replace("\n", "");
}
}
api/Controllers/DarsController.cs
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/Controllers/DarsController.cs b/api/Controllers/DarsController.cs
--- a/api/Controllers/DarsController.cs
+++ b/api/Controllers/DarsController.cs
@@ -31,11 +31,12 @@
         [ProducesResponseType(500)]
         public async Task<IActionResult> Search(DateTime date, int locationId, string courtRoomCd)
         {
+            var sanitizedCourtRoomCd = SanitizeForLog(courtRoomCd);
             logger.LogInformation(
                 "DARS search requested - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
                 date,
                 locationId,
-                courtRoomCd
+                sanitizedCourtRoomCd
             );
 
             // Validate input parameters
@@ -55,7 +52,7 @@
                         "No DARS recordings found for Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
                         date,
                         locationId,
-                        courtRoomCd
+                        sanitizedCourtRoomCd
                     );
                     return NotFound();
                 }
@@ -65,7 +62,7 @@
                     result.Count(),
                     date,
                     locationId,
-                    courtRoomCd
+                    sanitizedCourtRoomCd
                 );
 
                 return Ok(result);
@@ -77,7 +74,7 @@
                     "DARS API exception while searching - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}, Status: {StatusCode}",
                     date,
                     locationId,
-                    courtRoomCd,
+                    sanitizedCourtRoomCd,
                     ex.StatusCode
                 );
 
@@ -91,4 +88,7 @@
             }
         }
     }
+        // Helper to sanitize strings before logging
+        private static string SanitizeForLog(string input) =>
+            input?.Replace("\r", "").Replace("\n", "");
 }
EOF
@@ -31,11 +31,12 @@
[ProducesResponseType(500)]
public async Task<IActionResult> Search(DateTime date, int locationId, string courtRoomCd)
{
var sanitizedCourtRoomCd = SanitizeForLog(courtRoomCd);
logger.LogInformation(
"DARS search requested - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
date,
locationId,
courtRoomCd
sanitizedCourtRoomCd
);

// Validate input parameters
@@ -55,7 +52,7 @@
"No DARS recordings found for Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}",
date,
locationId,
courtRoomCd
sanitizedCourtRoomCd
);
return NotFound();
}
@@ -65,7 +62,7 @@
result.Count(),
date,
locationId,
courtRoomCd
sanitizedCourtRoomCd
);

return Ok(result);
@@ -77,7 +74,7 @@
"DARS API exception while searching - Date: {Date}, LocationId: {LocationId}, CourtRoom: {CourtRoom}, Status: {StatusCode}",
date,
locationId,
courtRoomCd,
sanitizedCourtRoomCd,
ex.StatusCode
);

@@ -91,4 +88,7 @@
}
}
}
// Helper to sanitize strings before logging
private static string SanitizeForLog(string input) =>
input?.Replace("\r", "").Replace("\n", "");
}
Copilot is powered by AI and may make mistakes. Always verify output.
var mappedResults = mapper.Map<IEnumerable<DarsSearchResults>>(darsResult).ToList();

// Use LINQ's Select to append the base URL to each result's Url property
var resultsWithUrl = mappedResults
.Select(result =>
{
if (!string.IsNullOrWhiteSpace(result.Url) && !string.IsNullOrWhiteSpace(_logsheetBaseUrl))
{
result.Url = $"{_logsheetBaseUrl.TrimEnd('/')}/{result.Url.TrimStart('/')}";
}
return result;
})
.ToList();

var darsResultsPerRoom = GetResultPerRoom(resultsWithUrl);
return darsResultsPerRoom;
}

// only return result for each room, preferring CCD json, then FLS, then CCD html.
// Note: multiple logsheets should be returned if there are multiple (FLS only) logsheets. CODE ORIGINALLY FROM PCSS.
private static List<DarsSearchResults> GetResultPerRoom(List<DarsSearchResults> allResults)
{
var resultsForRoom = allResults.GroupBy(result => result.CourtRoomCd);
List<DarsSearchResults> filteredResults = new List<DarsSearchResults>(allResults.Count);
foreach (IGrouping<string, DarsSearchResults> roomResults in resultsForRoom)
{
var actualResult = roomResults.FirstOrDefault(roomResult => roomResult.FileName != null && roomResult.FileName.ToLowerInvariant().Contains("json"));
if (actualResult != null)
{
filteredResults.Add(actualResult);
}
var flsResults = roomResults.Where(roomResult => roomResult.FileName != null && roomResult.FileName.ToLowerInvariant().Contains("fls")).ToList();
if (flsResults.Count >= 1)
{
filteredResults.AddRange(flsResults);
continue;
}
if (actualResult == null)
{
filteredResults.Add(roomResults.FirstOrDefault());
}

}
return filteredResults;
}
}
}
1 change: 1 addition & 0 deletions api/Services/Files/CivilFilesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ private async Task<CivilAppearanceResponse> PopulateDetailAppearancesAsync(Futur
appearance.AppearanceStatusDsc = await _lookupService.GetCivilAppearanceStatusDescription(appearance.AppearanceStatusCd.ToString());
appearance.CourtLocationId = await _locationService.GetLocationAgencyIdentifier(appearance.CourtAgencyId);
appearance.CourtLocation = await _locationService.GetLocationName(appearance.CourtAgencyId);
appearance.LocationId = await _locationService.GetLocationId(appearance.CourtAgencyId);
appearance.DocumentTypeDsc = await _lookupService.GetDocumentDescriptionAsync(appearance.DocumentTypeCd);
}
return civilAppearances;
Expand Down
1 change: 1 addition & 0 deletions api/Services/Files/CriminalFilesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ private async Task<CriminalFileAppearances> PopulateDetailsAppearancesAsync(stri
tasks.Add(Task.Run(async () => appearance.AppearanceStatusDsc = await _lookupService.GetCriminalAppearanceStatusDescription(appearance.AppearanceStatusCd.ToString())));
tasks.Add(Task.Run(async () => appearance.CourtLocationId = await _locationService.GetLocationAgencyIdentifier(appearance.CourtAgencyId)));
tasks.Add(Task.Run(async () => appearance.CourtLocation = await _locationService.GetLocationName(appearance.CourtAgencyId)));
appearance.LocationId = await _locationService.GetLocationId(appearance.CourtAgencyId);
}

await Task.WhenAll(tasks);
Expand Down
8 changes: 7 additions & 1 deletion api/Services/LocationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,13 @@ public virtual async Task<string> GetLocationShortName(string locationId)
public async Task<string> GetLocationCodeFromId(string code)
{
var locations = await GetLocations();
return locations.FirstOrDefault(loc => loc.LocationId == code)?.Code;
return locations.FirstOrDefault(loc => loc.Code == code)?.Code;
}

public async Task<string> GetLocationId(string code)
{
var locations = await GetLocations();
return locations.FirstOrDefault(loc => loc.Code == code)?.LocationId;
}

public async Task<string> GetLocationAgencyIdentifier(string code) => FindShortDescriptionFromCode(await GetLocations(), code);
Expand Down
1 change: 1 addition & 0 deletions api/api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\dars-client\dars-client.csproj" />
<ProjectReference Include="..\db\db.csproj" />
<ProjectReference Include="..\jc-interface-client\jc-interface-client.csproj" />
<ProjectReference Include="..\pcss-client\pcss-client.csproj" />
Expand Down
6 changes: 5 additions & 1 deletion api/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Default": "Debug",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"DARS": {
"LogsheetUrl": "https://test.jag.gov.bc.ca/darspc/",
"URL": "https://wsgw.test.jag.gov.bc.ca/courts/DARS"
}
}
9 changes: 6 additions & 3 deletions api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"FileExpiryMinutes": "2"
},
"MaximumArchiveDocumentCount": "100",
"ExcludeDocumentTypeCodesForCounsel": "IPR,SRR,RFR,SCL,VOC,CAR,RFS,RFSR,WAA,WAE,WAW,WFA,WOA,WOC,WOI,FULL,OI,FF"
}

"ExcludeDocumentTypeCodesForCounsel": "IPR,SRR,RFR,SCL,VOC,CAR,RFS,RFSR,WAA,WAE,WAW,WFA,WOA,WOC,WOI,FULL,OI,FF",
"DARS": {
"LogsheetUrl": "https://jag.gov.bc.ca/darspc/",
"URL": "https://wsgw.jag.gov.bc.ca/courts/DARS"
}
}
Loading
Loading