Conversation
|
Thanks for opening this, but we'd appreciate a little more information. Could you update it with more details? |
WalkthroughWarning Review details and warnings were omitted to fit the comment limit. |
Code Review Completed! 🔥The code review was successfully completed based on your current configurations.
Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
| /// Broker side: accept the legacy <see cref="BrokerApiKey"/> during the migration window (plan section 8.5 | ||
| /// rule 6). Turn off, and remove the key, once the broker logs show no legacy use. | ||
| /// </summary> | ||
| public static bool BrokerLegacySharedKeyEnabled = true; |
There was a problem hiding this comment.
BrokerLegacySharedKeyEnabled enables a full-authority legacy shared credential by default, violating deny-by-default and least-privilege principles during migration. Disable it by default and require an explicit, scoped authorization path.
Kody rule violation: Implement RBAC with least privilege and deny-by-default
public static bool BrokerLegacySharedKeyEnabled = false;Prompt for LLM
File Core/Resgrid.Config/DataProtectionConfig.cs:
Line 67:
`BrokerLegacySharedKeyEnabled` enables a full-authority legacy shared credential by default, violating deny-by-default and least-privilege principles during migration. Disable it by default and require an explicit, scoped authorization path.
Suggested Code:
public static bool BrokerLegacySharedKeyEnabled = false;
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| <value>Sessions Unit, IC et Dispatch que ce département exécute toujours comme sessions partagées, quel que soit le réglage de l'installation ; les connexions qui n'indiquent pas leur application comptent aussi. Nécessite des versions d'application compatibles avec le mode partagé. Membre gestionnaire uniquement.</value> | ||
| </data> | ||
| <data xml:space="preserve" name="TableHelp.DepartmentSecurityPolicy.SharedShiftHours"> | ||
| <value>Nombre d'heures après la connexion au bout desquelles une session partagée se termine, quelle que soit l'activité, de 1 à 24. Une valeur plus courte termine plus tôt les sessions en cours ; une valeur plus longue ne les prolonge jamais. Membre gestionnaire uniquement.</value> |
There was a problem hiding this comment.
The documented shared-session absolute timeout permits up to 24 hours, exceeding the required maximum of 12 hours. Change the documentation and corresponding limits in Core/Resgrid.Config/PasskeyConfig.cs:89 and Core/Resgrid.Model/DepartmentSecurityPolicy.cs:131 to a maximum of 12 hours.
Kody rule violation: Harden session management with idle and absolute timeouts
<value>Nombre d'heures après la connexion au bout desquelles une session partagée se termine, quelle que soit l'activité, de 1 à 12. Une valeur plus courte termine plus tôt les sessions en cours ; une valeur plus longue ne les prolonge jamais. Membre gestionnaire uniquement.</value>Prompt for LLM
File Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.fr.resx:
Line 5521:
The documented shared-session absolute timeout permits up to 24 hours, exceeding the required maximum of 12 hours. Change the documentation and corresponding limits in `Core/Resgrid.Config/PasskeyConfig.cs:89` and `Core/Resgrid.Model/DepartmentSecurityPolicy.cs:131` to a maximum of 12 hours.
Suggested Code:
<value>Nombre d'heures après la connexion au bout desquelles une session partagée se termine, quelle que soit l'activité, de 1 à 12. Une valeur plus courte termine plus tôt les sessions en cours ; une valeur plus longue ne les prolonge jamais. Membre gestionnaire uniquement.</value>
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| @@ -29,5 +29,8 @@ void RegisterForEvents(Func<int, string, Task> personnelStatusChanged, | |||
| /// </summary> | |||
| void RegisterForChatEvents(Func<int, string, Task> chatEvent); | |||
| void RegisterForChecklistEvents(Func<int, string, Task> checklistEvent); | |||
|
|
|||
| /// <summary>Events for one session (session id, serialized <c>SessionEventMessage</c>).</summary> | |||
| void RegisterForSessionEvents(Func<string, string, Task> sessionEvent); | |||
There was a problem hiding this comment.
RegisterForSessionEvents lacks an explicit error handler and deterministic unsubscribe mechanism, so session-event failures and cleanup cannot be managed reliably. Accept an error callback and return an IDisposable cleanup handle.
Kody rule violation: Provide error handlers to subscription/listener APIs
IDisposable RegisterForSessionEvents(Func<string, string, Task> sessionEvent, Func<Exception, Task> errorHandler);Prompt for LLM
File Core/Resgrid.Model/Providers/IRabbitInboundEventProvider.cs:
Line 34:
RegisterForSessionEvents lacks an explicit error handler and deterministic unsubscribe mechanism, so session-event failures and cleanup cannot be managed reliably. Accept an error callback and return an IDisposable cleanup handle.
Suggested Code:
IDisposable RegisterForSessionEvents(Func<string, string, Task> sessionEvent, Func<Exception, Task> errorHandler);
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| { | ||
| public async Task<bool> EnrollPinAsync(int departmentId, string userId, string grantToken, string pin, CancellationToken cancellationToken = default) | ||
| { | ||
| if (pin == null || !Regex.IsMatch(pin, "^[0-9]{6,12}$")) return false; | ||
| var policy = await protection.GetPolicyByDepartmentIdAsync(departmentId, bypassCache: true); | ||
| if (policy == null || grants.ValidateGrant(grantToken, departmentId, policy.PolicyEpoch, ProtectedDataGrantScopes.Read, | ||
| out var grant) != ProtectedDataGrantValidationOutcome.Valid || grant.UserId != userId || grant.StepUpExempt || | ||
| out var grant) != ProtectedDataGrantValidationOutcome.Valid || grant.UserId != userId || | ||
| await ProtectedGrantBinding.CheckAsync(grant, userId, ProtectedGrantBinding.SessionFor(grantContext, userId), policy.StepUpWindowMinutes, |
There was a problem hiding this comment.
The awaited ProtectedGrantBinding.CheckAsync call in AdpReleaseService and the listed call sites can reject without guarded handling, propagating unhandled exceptions. Wrap the call in try/catch or apply an equivalent error-handling strategy that maps or logs the failure appropriately.
Kody rule violation: Handle async operations with proper error handling
Prompt for LLM
File Core/Resgrid.Services/AdpReleaseService.cs:
Line 30:
The awaited `ProtectedGrantBinding.CheckAsync` call in AdpReleaseService and the listed call sites can reject without guarded handling, propagating unhandled exceptions. Wrap the call in try/catch or apply an equivalent error-handling strategy that maps or logs the failure appropriately.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
|
|
||
| var now = _time.GetUtcNow().UtcDateTime; | ||
| if (await _challenges.CountPendingForUserAsync(binding.UserId, now, cancellationToken) >= Math.Max(1, PasskeyConfig.MaxOutstandingChallengesPerUser)) | ||
| return null; |
There was a problem hiding this comment.
AuthenticationChallengeService returns null from a Task-returning method, which can cause callers to receive a null Task and fail before awaiting a result. Return an explicit Task result, or change the contract to a nullable result type with a completed Task.
Kody rule violation: Avoid Returning Null in Non-Async Task Methods
return Task.FromResult<AuthenticationChallenge>(null);Prompt for LLM
File Core/Resgrid.Services/AuthenticationChallengeService.cs:
Line 34:
AuthenticationChallengeService returns null from a Task-returning method, which can cause callers to receive a null Task and fail before awaiting a result. Return an explicit Task result, or change the contract to a nullable result type with a completed Task.
Suggested Code:
return Task.FromResult<AuthenticationChallenge>(null);
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| catch (Exception ex) | ||
| { | ||
| // Authoritative state is unavailable: the ceremony is refused, never treated as approved (plan section 3 item 4). | ||
| Logging.LogException(ex, "Authentication challenge lookup failed; the ceremony was refused."); | ||
| return AuthenticationChallengeResult.Of(AuthenticationChallengeOutcome.Unavailable); |
There was a problem hiding this comment.
AuthenticationChallengeService catches every exception and converts it to AuthenticationChallengeOutcome.Unavailable, masking non-transient database failures and preventing them from surfacing for remediation. Catch only exceptions identified by IsTransientDatabaseException and rethrow non-transient failures after logging them.
Kody rule violation: Implement proper database error checking
catch (Exception ex) when (IsTransientDatabaseException(ex))
{
Logging.LogException(ex, "Transient authentication challenge lookup failure.");
return AuthenticationChallengeResult.Of(AuthenticationChallengeOutcome.Unavailable);
}
catch (Exception ex)
{
Logging.LogException(ex, "Non-transient authentication challenge lookup failure.");
throw;
}Prompt for LLM
File Core/Resgrid.Services/AuthenticationChallengeService.cs:
Line 70 to 74:
AuthenticationChallengeService catches every exception and converts it to AuthenticationChallengeOutcome.Unavailable, masking non-transient database failures and preventing them from surfacing for remediation. Catch only exceptions identified by IsTransientDatabaseException and rethrow non-transient failures after logging them.
Suggested Code:
catch (Exception ex) when (IsTransientDatabaseException(ex))
{
Logging.LogException(ex, "Transient authentication challenge lookup failure.");
return AuthenticationChallengeResult.Of(AuthenticationChallengeOutcome.Unavailable);
}
catch (Exception ex)
{
Logging.LogException(ex, "Non-transient authentication challenge lookup failure.");
throw;
}
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| public async Task RecordFailedAttemptAsync(FactorRecoveryTransaction transaction, CancellationToken cancellationToken = default) | ||
| { | ||
| try | ||
| { | ||
| await _transactions.RecordFailedAttemptAsync(transaction.FactorRecoveryTransactionId, cancellationToken); | ||
| } | ||
| catch (Exception ex) when (!(ex is OperationCanceledException)) | ||
| { | ||
| Logging.LogException(ex, "A failed factor recovery attempt could not be counted."); | ||
| } | ||
| } |
There was a problem hiding this comment.
RecordFailedAttemptAsync swallows every non-cancellation repository failure and returns success, so FactorRecoveryController continues using the same pending transaction when the attempt counter cannot be persisted, allowing unlimited replacement-authenticator guesses during a storage outage. Propagate a refusal or failure result and stop the recovery endpoint until the guarded attempt update succeeds.
public async Task RecordFailedAttemptAsync(FactorRecoveryTransaction transaction, CancellationToken cancellationToken = default)
{
await _transactions.RecordFailedAttemptAsync(transaction.FactorRecoveryTransactionId, cancellationToken);
}Prompt for LLM
File Core/Resgrid.Services/FactorRecoveryService.cs:
Line 99 to 109:
RecordFailedAttemptAsync swallows every non-cancellation repository failure and returns success, so FactorRecoveryController continues using the same pending transaction when the attempt counter cannot be persisted, allowing unlimited replacement-authenticator guesses during a storage outage. Propagate a refusal or failure result and stop the recovery endpoint until the guarded attempt update succeeds.
Suggested Code:
public async Task RecordFailedAttemptAsync(FactorRecoveryTransaction transaction, CancellationToken cancellationToken = default)
{
await _transactions.RecordFailedAttemptAsync(transaction.FactorRecoveryTransactionId, cancellationToken);
}
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| public async Task<MfaActivityReport> ReportAsync(string userId, string mfaActivityId, string reportingSessionId, SharedSessionRequestInfo request, | ||
| CancellationToken cancellationToken = default) | ||
| { | ||
| var activity = string.IsNullOrWhiteSpace(mfaActivityId) ? null : await _rows.GetAsync(mfaActivityId, cancellationToken); |
There was a problem hiding this comment.
MfaActivityService issues a repository query when userId or mfaActivityId is missing, allowing invalid identifiers to reach the data layer. Validate both identifiers first and return MfaActivityReportOutcome.NotFound without querying when either is blank.
Kody rule violation: Order validations before database queries
if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(mfaActivityId))
return new MfaActivityReport { Outcome = MfaActivityReportOutcome.NotFound };
var activity = await _rows.GetAsync(mfaActivityId, cancellationToken);Prompt for LLM
File Core/Resgrid.Services/MfaActivityService.cs:
Line 68:
MfaActivityService issues a repository query when userId or mfaActivityId is missing, allowing invalid identifiers to reach the data layer. Validate both identifiers first and return MfaActivityReportOutcome.NotFound without querying when either is blank.
Suggested Code:
if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(mfaActivityId))
return new MfaActivityReport { Outcome = MfaActivityReportOutcome.NotFound };
var activity = await _rows.GetAsync(mfaActivityId, cancellationToken);
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| private static bool NamesCredential(ProtectedDataGrant grant) => | ||
| grant.Version >= 2 && grant.MfaMethod != ProtectedDataGrantMfaMethods.Totp && grant.MfaMethod != ProtectedDataGrantMfaMethods.None; |
There was a problem hiding this comment.
NamesCredential dereferences the potentially null grant before validating it, which can cause a null reference failure in the protected-grant flow. Check grant before accessing its properties and use GrantVersionWithCredential for the version comparison.
Kody rule violation: Add null checks to prevent NullReferenceException
private static bool NamesCredential(ProtectedDataGrant grant) =>
grant != null && grant.Version >= GrantVersionWithCredential && grant.MfaMethod != ProtectedDataGrantMfaMethods.Totp && grant.MfaMethod != ProtectedDataGrantMfaMethods.None;Prompt for LLM
File Core/Resgrid.Services/ProtectedGrantBinding.cs:
Line 64 to 65:
`NamesCredential` dereferences the potentially null `grant` before validating it, which can cause a null reference failure in the protected-grant flow. Check `grant` before accessing its properties and use `GrantVersionWithCredential` for the version comparison.
Suggested Code:
private static bool NamesCredential(ProtectedDataGrant grant) =>
grant != null && grant.Version >= GrantVersionWithCredential && grant.MfaMethod != ProtectedDataGrantMfaMethods.Totp && grant.MfaMethod != ProtectedDataGrantMfaMethods.None;
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| private static bool IsAllowedOrigin(string origin, string rpId) | ||
| { | ||
| if (AndroidOriginPattern.IsMatch(origin)) | ||
| return true; |
There was a problem hiding this comment.
Android origins are accepted solely because they match a 43-character shape, without checking an allowlisted signing-certificate hash, so any Android application with its own signing key can present an android:apk-key-hash: origin and bypass per-application passkey binding. Require Android origins to be explicitly configured and validate the normalized origin against the configured per-client allowlist instead of returning true for every regex match.
private static bool IsAllowedOrigin(string origin, string rpId, IReadOnlySet<string> allowedAndroidOrigins)
{
if (AndroidOriginPattern.IsMatch(origin))
return allowedAndroidOrigins.Contains(origin);
// validate HTTPS web origins as before
}Prompt for LLM
File Core/Resgrid.Services/RelyingPartyRegistry.cs:
Line 131 to 134:
Android origins are accepted solely because they match a 43-character shape, without checking an allowlisted signing-certificate hash, so any Android application with its own signing key can present an `android:apk-key-hash:` origin and bypass per-application passkey binding. Require Android origins to be explicitly configured and validate the normalized origin against the configured per-client allowlist instead of returning true for every regex match.
Suggested Code:
private static bool IsAllowedOrigin(string origin, string rpId, IReadOnlySet<string> allowedAndroidOrigins)
{
if (AndroidOriginPattern.IsMatch(origin))
return allowedAndroidOrigins.Contains(origin);
// validate HTTPS web origins as before
}
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| { | ||
| try | ||
| { | ||
| if (await DeliverAsync(notice, cancellationToken)) |
There was a problem hiding this comment.
SecurityNoticeService performs delivery and related I/O once per notice inside a loop, serializing independent operations and increasing latency. Batch the work or safely parallelize it with Task.WhenAll, or add an aggregate delivery API.
Kody rule violation: Detect N+1 style queries and suggest batching
var deliveries = due.Select(notice => DeliverAsync(notice, cancellationToken));
foreach (var delivered in await Task.WhenAll(deliveries))
{
if (delivered)
sent++;
}Prompt for LLM
File Core/Resgrid.Services/SecurityNoticeService.cs:
Line 120:
SecurityNoticeService performs delivery and related I/O once per notice inside a loop, serializing independent operations and increasing latency. Batch the work or safely parallelize it with Task.WhenAll, or add an aggregate delivery API.
Suggested Code:
var deliveries = due.Select(notice => DeliverAsync(notice, cancellationToken));
foreach (var delivered in await Task.WhenAll(deliveries))
{
if (delivered)
sent++;
}
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| { | ||
| var reader = new CborReader(coseKey, CborConformanceMode.Lax); | ||
| var entries = reader.ReadStartMap(); | ||
| for (var i = 0; entries == null || i < entries; i++) |
There was a problem hiding this comment.
The loop uses an equality-based termination expression and does not explicitly access the nullable value, reducing termination clarity. Use pattern matching with entries is null || i < entries.Value for deterministic relational termination.
Kody rule violation: Avoid equality operators in loop termination conditions
for (int i = 0; entries is null || i < entries.Value; i++)Prompt for LLM
File Providers/Resgrid.Providers.Authentication/Fido2PasskeyProvider.cs:
Line 221:
The loop uses an equality-based termination expression and does not explicitly access the nullable value, reducing termination clarity. Use pattern matching with `entries is null || i < entries.Value` for deterministic relational termination.
Suggested Code:
for (int i = 0; entries is null || i < entries.Value; i++)
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| { | ||
| var label = reader.ReadInt64(); | ||
| if (label == 3 && reader.PeekState() is CborReaderState.UnsignedInteger or CborReaderState.NegativeInteger) | ||
| return (int)reader.ReadInt64(); |
There was a problem hiding this comment.
The CBOR algorithm value is converted from Int64 to int without checked arithmetic, so an out-of-range value can silently overflow. Use checked((int)reader.ReadInt64()).
Kody rule violation: Prevent Numeric Overflow in Calculations
return checked((int)reader.ReadInt64());Prompt for LLM
File Providers/Resgrid.Providers.Authentication/Fido2PasskeyProvider.cs:
Line 230:
The CBOR algorithm value is converted from Int64 to int without checked arithmetic, so an out-of-range value can silently overflow. Use `checked((int)reader.ReadInt64())`.
Suggested Code:
return checked((int)reader.ReadInt64());
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| public Task<bool> SessionEvent(string sessionId, string payload) => SendMessage(Topics.EventingTopic, new EventingMessage | ||
| { | ||
| Id = Guid.NewGuid(), Type = (int)EventingTypes.SessionEvent, TimeStamp = DateTime.UtcNow, ItemId = sessionId, Payload = payload | ||
| }.SerializeJson()); |
There was a problem hiding this comment.
The Rabbit/message-bus publish operation in RabbitTopicProvider and the listed call sites lacks guarded failure context, making session-event delivery failures difficult to diagnose. Wrap the operation in try/catch, log the operation and session identifier with structured fields, and rethrow or map the error appropriately.
Kody rule violation: Add try-catch blocks for external calls
public async Task<bool> SessionEvent(string sessionId, string payload)
{
try
{
return await SendMessage(Topics.EventingTopic, new EventingMessage
{
Id = Guid.NewGuid(), Type = (int)EventingTypes.SessionEvent, TimeStamp = DateTime.UtcNow, ItemId = sessionId, Payload = payload
}.SerializeJson());
}
catch (Exception exception)
{
_logger.Error(exception, "Failed to publish session event for session {SessionId}", sessionId);
throw;
}
}Prompt for LLM
File Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs:
Line 90 to 93:
The Rabbit/message-bus publish operation in RabbitTopicProvider and the listed call sites lacks guarded failure context, making session-event delivery failures difficult to diagnose. Wrap the operation in try/catch, log the operation and session identifier with structured fields, and rethrow or map the error appropriately.
Suggested Code:
public async Task<bool> SessionEvent(string sessionId, string payload)
{
try
{
return await SendMessage(Topics.EventingTopic, new EventingMessage
{
Id = Guid.NewGuid(), Type = (int)EventingTypes.SessionEvent, TimeStamp = DateTime.UtcNow, ItemId = sessionId, Payload = payload
}.SerializeJson());
}
catch (Exception exception)
{
_logger.Error(exception, "Failed to publish session event for session {SessionId}", sessionId);
throw;
}
}
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| Alter.Table("DepartmentSsoConfigs").AddColumn("FederatedMfaMappingJson").AsString(int.MaxValue).Nullable(); | ||
|
|
||
| if (!Schema.Table("DepartmentSsoConfigs").Column("FederatedMfaMappingVersion").Exists()) | ||
| Alter.Table("DepartmentSsoConfigs").AddColumn("FederatedMfaMappingVersion").AsInt64().NotNullable().WithDefaultValue(0); |
There was a problem hiding this comment.
Adding a non-null column with a default in one migration can lock or rewrite a large table, creating deployment risk across the listed migrations. Use an expand/contract strategy: add the column nullable, backfill existing rows in batches, add the default for new writes, and enforce NOT NULL later with an online migration and documented rollback plan.
Kody rule violation: Block risky database migrations (locking ops, downtime risk)
Prompt for LLM
File Providers/Resgrid.Providers.Migrations/Migrations/M0251_AddFederatedMfaMapping.cs:
Line 19:
Adding a non-null column with a default in one migration can lock or rewrite a large table, creating deployment risk across the listed migrations. Use an expand/contract strategy: add the column nullable, backfill existing rows in batches, add the default for new writes, and enforce NOT NULL later with an online migration and documented rollback plan.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| private const int Canceled = (int)AuthenticationChallengeState.Canceled; | ||
|
|
||
| private readonly IConnectionProvider _connections; | ||
| private readonly bool _postgres; |
There was a problem hiding this comment.
The _postgres field is initialized once and never reassigned, so its declaration should remain readonly to enforce immutability. Keep private readonly bool _postgres; or verify that the field is intentionally mutable throughout the type.
Kody rule violation: Use `readonly` or `const` for Immutable Data
Prompt for LLM
File Repositories/Resgrid.Repositories.DataRepository/AuthenticationChallengeRepository.cs:
Line 25:
The `_postgres` field is initialized once and never reassigned, so its declaration should remain readonly to enforce immutability. Keep `private readonly bool _postgres;` or verify that the field is intentionally mutable throughout the type.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| /// <summary>The time step Identity computes for <paramref name="utcNow"/> (rounded seconds, integer division).</summary> | ||
| public static long CurrentTimeStep(DateTime utcNow) | ||
| => Convert.ToInt64(Math.Round((utcNow - UnixEpoch).TotalSeconds)) / StepSeconds; |
There was a problem hiding this comment.
CurrentTimeStep rounds Unix seconds before integer division, advancing the TOTP step by up to 0.5 seconds and allowing the server to consume a future step before the actual 30-second boundary, which causes intermittent valid-code failures. Use floor/integer division of elapsed Unix seconds, such as (long)(utcNow - UnixEpoch).TotalSeconds / StepSeconds, to match RFC 6238 and Identity's time-step calculation.
public static long CurrentTimeStep(DateTime utcNow)
=> (long)(utcNow - UnixEpoch).TotalSeconds / StepSeconds;Prompt for LLM
File Repositories/Resgrid.Repositories.DataRepository/Stores/TotpCalculator.cs:
Line 23 to 25:
`CurrentTimeStep` rounds Unix seconds before integer division, advancing the TOTP step by up to 0.5 seconds and allowing the server to consume a future step before the actual 30-second boundary, which causes intermittent valid-code failures. Use floor/integer division of elapsed Unix seconds, such as `(long)(utcNow - UnixEpoch).TotalSeconds / StepSeconds`, to match RFC 6238 and Identity's time-step calculation.
Suggested Code:
public static long CurrentTimeStep(DateTime utcNow)
=> (long)(utcNow - UnixEpoch).TotalSeconds / StepSeconds;
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| await db.ExecuteAsync($"CREATE TABLE {Q("DepartmentSsoConfigs")} ({Q("DepartmentId")} int,{Q("IsEnabled")} {boolean})"); | ||
| await db.ExecuteAsync($"CREATE TABLE {Q("UserSessions")} ({Q("UserSessionId")} {text}(128),{Q("UserId")} {text}(128),{Q("DepartmentId")} int,{Q("State")} int,{Q("CreatedOn")} {date},{Q("LastActiveOn")} {date},{Q("ExpiresOn")} {date},{Q("AuthenticationGeneration")} bigint)"); | ||
| var now = new DateTime(2026,9,24,12,0,0); | ||
| var args = new { False = false, True = true, Now = now, Old = now.AddHours(-1), Future = now.AddHours(1) }; | ||
| await db.ExecuteAsync($"INSERT INTO {Q("DepartmentMembers")} ({Q("DepartmentMemberId")},{Q("DepartmentId")},{Q("UserId")},{Q("IsDeleted")},{Q("IsDisabled")},{Q("IsHidden")},{Q("PasswordLastSetOn")}) VALUES (1,709,'security-a',@False,@False,@True,@Old),(2,709,'security-missing',@False,@False,@False,NULL),(3,709,'security-disabled',@False,@True,@False,NULL),(4,710,'security-other',@False,@False,@False,NULL)",args); | ||
| await db.ExecuteAsync($"INSERT INTO {Q("AspNetUsers")} ({Q("Id")},{Q("TwoFactorEnabled")},{Q("AuthenticationGeneration")}) VALUES ('security-a',@True,4); INSERT INTO {Q("DepartmentSsoConfigs")} VALUES (709,@True),(709,@False),(710,@True); INSERT INTO {Q("DepartmentSecurityPolicies")} VALUES (709,@True,@False,30,2,90,12)",args); | ||
| await db.ExecuteAsync($"INSERT INTO {Q("AspNetUsers")} ({Q("Id")},{Q("TwoFactorEnabled")},{Q("AuthenticationGeneration")}) VALUES ('security-a',@True,4); INSERT INTO {Q("DepartmentSsoConfigs")} VALUES (709,@True),(709,@False),(710,@True); INSERT INTO {Q("DepartmentSecurityPolicies")} VALUES (709,@True,@False,30,2,90,12,@True,@False,@False,@False,@True,@True,@True,3)",args); |
There was a problem hiding this comment.
The related database inserts execute without a transaction, so a failure can leave the test database partially seeded. Enclose the inserts in a transaction, commit only after all succeed, and roll back on failure.
Kody rule violation: Handle transaction rollbacks properly
using var transaction = db.BeginTransaction();
try
{
await db.ExecuteAsync($"INSERT INTO {Q("AspNetUsers")} (...) VALUES (...); INSERT INTO {Q("DepartmentSsoConfigs")} VALUES (...); INSERT INTO {Q("DepartmentSecurityPolicies")} VALUES (...)", args, transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}Prompt for LLM
File Tests/Resgrid.Tests/AdminAssist/AdminAssistDatabaseTests.cs:
Line 355:
The related database inserts execute without a transaction, so a failure can leave the test database partially seeded. Enclose the inserts in a transaction, commit only after all succeed, and roll back on failure.
Suggested Code:
using var transaction = db.BeginTransaction();
try
{
await db.ExecuteAsync($"INSERT INTO {Q("AspNetUsers")} (...) VALUES (...); INSERT INTO {Q("DepartmentSsoConfigs")} VALUES (...); INSERT INTO {Q("DepartmentSecurityPolicies")} VALUES (...)", args, transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| [TestFixture] | ||
| public class RazorOutputEncodingTests | ||
| { | ||
| private static readonly Regex LocalizerInject = new Regex(@"@inject\s+I(?:String|Html|View)Localizer(?:<[\w.]+>)?\s+(\w+)", RegexOptions.Compiled); |
There was a problem hiding this comment.
The listed Regex instances, including LocalizerInject in Tests/Resgrid.Tests/Localization/RazorOutputEncodingTests.cs, omit a timeout and can be exposed to regular-expression denial-of-service attacks. Define an explicit timeout for every regex that processes untrusted or externally sourced input.
Kody rule violation: Specify Timeout for Regular Expressions
Prompt for LLM
File Tests/Resgrid.Tests/Localization/RazorOutputEncodingTests.cs:
Line 45:
The listed Regex instances, including `LocalizerInject` in `Tests/Resgrid.Tests/Localization/RazorOutputEncodingTests.cs`, omit a timeout and can be exposed to regular-expression denial-of-service attacks. Define an explicit timeout for every regex that processes untrusted or externally sourced input.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| private static string FormatItems(string value) | ||
| { | ||
| return string.Join(" ", FormatItem.Matches(value) | ||
| .Select(m => int.Parse(m.Groups[1].Value)) |
There was a problem hiding this comment.
The listed tests use int.Parse to convert string input, which can throw on invalid or unexpected formats. Use a TryParse-style API and validate the culture and format where applicable.
Kody rule violation: Use TryParse for string conversions
Prompt for LLM
File Tests/Resgrid.Tests/Localization/ResourceKeyParityTests.cs:
Line 64:
The listed tests use `int.Parse` to convert string input, which can throw on invalid or unexpected formats. Use a TryParse-style API and validate the culture and format where applicable.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| DataConfig.DatabaseType = type; | ||
| _database = Prefix + Guid.NewGuid().ToString("N"); | ||
| await using (var master = Connect(_master)) | ||
| await master.ExecuteAsync("CREATE DATABASE " + _database); |
There was a problem hiding this comment.
The listed database tests build SQL with unsanitized _database input, allowing SQL injection through the concatenated CREATE DATABASE statement. Use parameterized queries or a validated identifier-quoting mechanism for database names.
Kody rule violation: Prevent SQL Injection in Queries
Prompt for LLM
File Tests/Resgrid.Tests/Security/BrokerReplayDatabaseTests.cs:
Line 70:
The listed database tests build SQL with unsanitized `_database` input, allowing SQL injection through the concatenated `CREATE DATABASE` statement. Use parameterized queries or a validated identifier-quoting mechanism for database names.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| if (type == DatabaseTypes.Postgres) r.AddPostgres(); else r.AddSqlServer(); | ||
| r.WithGlobalConnectionString(_connection); | ||
| }).AddSingleton(source.Object).BuildServiceProvider(); | ||
| _runner.GetRequiredService<IMigrationRunner>().MigrateUp(); |
There was a problem hiding this comment.
The async setup method calls the migration runner's synchronous MigrateUp() API, blocking the execution thread during database work. Use the asynchronous MigrateUpAsync() API.
Kody rule violation: Use Awaitable Methods in Async Code
await _runner.GetRequiredService<IMigrationRunner>().MigrateUpAsync();Prompt for LLM
File Tests/Resgrid.Tests/Security/BrokerReplayDatabaseTests.cs:
Line 85:
The async setup method calls the migration runner's synchronous `MigrateUp()` API, blocking the execution thread during database work. Use the asynchronous `MigrateUpAsync()` API.
Suggested Code:
await _runner.GetRequiredService<IMigrationRunner>().MigrateUpAsync();
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| /// Set RESGRID_ADP_SQLSERVER_TEST_CONNECTION / RESGRID_ADP_POSTGRES_TEST_CONNECTION (server-level connections) to run. | ||
| /// </summary> | ||
| [TestFixture(DatabaseTypes.SqlServer), TestFixture(DatabaseTypes.Postgres), NonParallelizable] | ||
| public class BrokerReplayDatabaseTests(DatabaseTypes type) |
There was a problem hiding this comment.
BrokerReplayDatabaseTests uses an async-capable primary-constructor pattern for lifecycle initialization, which obscures synchronous construction and setup ordering. Keep the constructor synchronous and move asynchronous initialization into the setup method.
Kody rule violation: Avoid asynchronous operations in constructors
public class BrokerReplayDatabaseTests
{
public BrokerReplayDatabaseTests(DatabaseTypes type) { ... }Prompt for LLM
File Tests/Resgrid.Tests/Security/BrokerReplayDatabaseTests.cs:
Line 32:
BrokerReplayDatabaseTests uses an async-capable primary-constructor pattern for lifecycle initialization, which obscures synchronous construction and setup ordering. Keep the constructor synchronous and move asynchronous initialization into the setup method.
Suggested Code:
public class BrokerReplayDatabaseTests
{
public BrokerReplayDatabaseTests(DatabaseTypes type) { ... }
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| reached = true; | ||
| return Task.FromResult<ActionExecutedContext>(null); | ||
| }); | ||
| return reached && context.Result == null; |
There was a problem hiding this comment.
The listed tests block asynchronous operations with .Result or .Wait(), which can cause deadlocks and prevent efficient asynchronous execution. Convert the call paths to async and use await instead of blocking.
Kody rule violation: Avoid Blocking Calls to Async Methods
Prompt for LLM
File Tests/Resgrid.Tests/Security/DepartmentLockAuthFlowsTests.cs:
Line 63:
The listed tests block asynchronous operations with `.Result` or `.Wait()`, which can cause deadlocks and prevent efficient asynchronous execution. Convert the call paths to async and use `await` instead of blocking.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| reached = true; | ||
| return Task.FromResult<ActionExecutedContext>(null); | ||
| }); | ||
| return reached && context.Result == null; |
There was a problem hiding this comment.
DepartmentLockAuthFlowsTests and the listed MfaApprovalServiceTests, MfaPolicyEnforcementTests, MfaStepUpIssuanceTests, SsoBrokerServiceTests, and ConnectControllerSsoTests block on asynchronous operations instead of awaiting them. Use async/await end-to-end, avoid .Result and .Wait(), and configure awaits appropriately.
Kody rule violation: Await async operations properly
Prompt for LLM
File Tests/Resgrid.Tests/Security/DepartmentLockAuthFlowsTests.cs:
Line 63:
DepartmentLockAuthFlowsTests and the listed MfaApprovalServiceTests, MfaPolicyEnforcementTests, MfaStepUpIssuanceTests, SsoBrokerServiceTests, and ConnectControllerSsoTests block on asynchronous operations instead of awaiting them. Use async/await end-to-end, avoid `.Result` and `.Wait()`, and configure awaits appropriately.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| [TestCase("https://unit.example.org\n", "https://unit.example.org/auth/callback", TestName = "A trailing newline, trimmed like spaces")] | ||
| public void A_clean_origin_is_listed_as_the_apps_web_page(string origin, string expected) | ||
| { | ||
| LegacyAppCallbacks.RedirectUris($"unit={origin}").Should().ContainSingle(u => u.Web).Which.Uri.Should().Be(expected); |
There was a problem hiding this comment.
The listed code uses an inline lambda in Should().ContainSingle(u => u.Web), although the cited rule targets .bind() or arrow functions in JSX props and does not apply to this C# test expression. Apply the rule only to JSX props, or provide a specific concern for this LINQ predicate.
Kody rule violation: Avoid using .bind() or arrow functions in JSX props
Prompt for LLM
File Tests/Resgrid.Tests/Security/LegacyAppCallbacksTests.cs:
Line 122:
The listed code uses an inline lambda in `Should().ContainSingle(u => u.Web)`, although the cited rule targets `.bind()` or arrow functions in JSX props and does not apply to this C# test expression. Apply the rule only to JSX props, or provide a specific concern for this LINQ predicate.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
|
|
||
| // ---- Host ----------------------------------------------------------------------------------------------------- | ||
|
|
||
| public static async Task<LiveSignInServer> StartAsync(string url = "http://127.0.0.1:0") |
There was a problem hiding this comment.
LiveSignInServer.StartAsync starts the externally reachable test server over plaintext HTTP, leaving transport security and HSTS behavior untested. Require HTTPS with TLS 1.2 or later.
Kody rule violation: Enforce TLS 1.2+ and HSTS on all external endpoints
public static async Task<LiveSignInServer> StartAsync(string url = "https://127.0.0.1:0")Prompt for LLM
File Tests/Resgrid.Tests/Security/Live/LiveSignInServer.cs:
Line 593:
LiveSignInServer.StartAsync starts the externally reachable test server over plaintext HTTP, leaving transport security and HSTS behavior untested. Require HTTPS with TLS 1.2 or later.
Suggested Code:
public static async Task<LiveSignInServer> StartAsync(string url = "https://127.0.0.1:0")
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| /// </summary> | ||
| internal sealed class SoftPasskeyAuthenticator | ||
| { | ||
| private readonly ECDsa _key = ECDsa.Create(ECCurve.NamedCurves.nistP256); |
There was a problem hiding this comment.
The disposable ECDsa instance in SoftPasskeyAuthenticator and the listed call sites is never deterministically released, which can leak cryptographic resources. Implement IDisposable on the owning type and dispose the field during teardown.
Kody rule violation: Use using statements for disposable resources
private readonly ECDsa _key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
public void Dispose() => _key.Dispose();Prompt for LLM
File Tests/Resgrid.Tests/Security/SoftPasskeyAuthenticator.cs:
Line 19:
The disposable `ECDsa` instance in SoftPasskeyAuthenticator and the listed call sites is never deterministically released, which can leak cryptographic resources. Implement IDisposable on the owning type and dispose the field during teardown.
Suggested Code:
private readonly ECDsa _key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
public void Dispose() => _key.Dispose();
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| [Test] | ||
| public async Task Index_EncodesCategoryNamesInTheTreeData() | ||
| { | ||
| const string hostileName = "<img src=x onerror=alert(1)>"; |
There was a problem hiding this comment.
The listed files use a plain <img> element for app assets without the required Next.js Image component, explicit dimensions, and meaningful alt text. Replace the element with the appropriate image component and provide dimensions and accessible alternative text.
Kody rule violation: Use next/image with explicit dimensions and alt
Prompt for LLM
File Tests/Resgrid.Tests/Web/User/ContactsIndexEncodingTests.cs:
Line 41:
The listed files use a plain `<img>` element for app assets without the required Next.js Image component, explicit dimensions, and meaningful alt text. Replace the element with the appropriate image component and provide dimensions and accessible alternative text.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| response.writeHead(200, { 'Content-Type': 'text/html' }).end(page(url.pathname.substring(1))); | ||
| } else if (url.pathname === '/SharedSession/Status') { | ||
| const [code, json] = statusBody(); | ||
| setTimeout(() => response.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }).end(json), state.delay); |
There was a problem hiding this comment.
The delayed response timer remains active when the request closes, allowing a response write after the connection has ended. Retain the timeout handle and clear it from the request's close handler.
Kody rule violation: Clear timers on teardown/unmount
const timer = setTimeout(() => response.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }).end(json), state.delay);
request.on('close', () => clearTimeout(timer));Prompt for LLM
File Tests/Resgrid.Tests/Web/resgrid-shared-session.test.cjs:
Line 71:
The delayed response timer remains active when the request closes, allowing a response write after the connection has ended. Retain the timeout handle and clear it from the request's close handler.
Suggested Code:
const timer = setTimeout(() => response.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }).end(json), state.delay);
request.on('close', () => clearTimeout(timer));
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| _connections.Register(context.Context.ConnectionId, sessionId, context.Context.Abort); | ||
| await context.Hub.Groups.AddToGroupAsync(context.Context.ConnectionId, SessionEvents.GroupFor(sessionId)); | ||
| } | ||
|
|
||
| await next(context); |
There was a problem hiding this comment.
The connection is registered before the awaited SignalR group join, and AddToGroupAsync failures leave dead connections in SessionConnectionRegistry even though OnConnectedAsync never completed. Wrap registration, group joining, and the downstream callback in try/finally or equivalent cleanup logic, and unregister when setup or the callback fails after registration.
var registered = false;\ntry\n{\n\t_connections.Register(context.Context.ConnectionId, sessionId, context.Context.Abort);\n\tregistered = true;\n\tawait context.Hub.Groups.AddToGroupAsync(context.Context.ConnectionId, SessionEvents.GroupFor(sessionId));\n\tawait next(context);\n}\ncatch\n{\n\tif (registered)\n\t\t_connections.Unregister(context.Context.ConnectionId);\n\tthrow;\n}Prompt for LLM
File Web/Resgrid.Web.Eventing/Middleware/SessionValidationHubFilter.cs:
Line 49 to 53:
The connection is registered before the awaited SignalR group join, and AddToGroupAsync failures leave dead connections in SessionConnectionRegistry even though OnConnectedAsync never completed. Wrap registration, group joining, and the downstream callback in try/finally or equivalent cleanup logic, and unregister when setup or the callback fails after registration.
Suggested Code:
var registered = false;\ntry\n{\n\t_connections.Register(context.Context.ConnectionId, sessionId, context.Context.Abort);\n\tregistered = true;\n\tawait context.Hub.Groups.AddToGroupAsync(context.Context.ConnectionId, SessionEvents.GroupFor(sessionId));\n\tawait next(context);\n}\ncatch\n{\n\tif (registered)\n\t\t_connections.Unregister(context.Context.ConnectionId);\n\tthrow;\n}
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| var transaction = redeemed.Transaction; | ||
| return transaction.TransactionPurpose == SsoTransactionPurpose.Reauthentication | ||
| ? await ReauthenticatedAsync(transaction, cancellationToken) | ||
| : await LoginAsync(transaction, client, input.ClientId, cancellationToken); |
There was a problem hiding this comment.
Sso/Redeem routes every redeemed SSO purpose other than Reauthentication to LoginAsync, so session-bound StepUp and AdpStepUp transactions are treated as fresh SSO logins and create login MFA/completion artifacts with incorrect policy and audit semantics. Dispatch only Login transactions to LoginAsync, route Reauthentication to ReauthenticatedAsync, and reject or handle StepUp/AdpStepUp exclusively in their dedicated consumers.
var transaction = redeemed.Transaction;
return transaction.TransactionPurpose switch
{
SsoTransactionPurpose.Login => await LoginAsync(transaction, client, input.ClientId, cancellationToken),
SsoTransactionPurpose.Reauthentication => await ReauthenticatedAsync(transaction, cancellationToken),
_ => Refuse(SsoBrokerOutcome.InvalidRequest)
};Prompt for LLM
File Web/Resgrid.Web.Services/Controllers/v4/SsoController.cs:
Line 148 to 151:
Sso/Redeem routes every redeemed SSO purpose other than Reauthentication to LoginAsync, so session-bound StepUp and AdpStepUp transactions are treated as fresh SSO logins and create login MFA/completion artifacts with incorrect policy and audit semantics. Dispatch only Login transactions to LoginAsync, route Reauthentication to ReauthenticatedAsync, and reject or handle StepUp/AdpStepUp exclusively in their dedicated consumers.
Suggested Code:
var transaction = redeemed.Transaction;
return transaction.TransactionPurpose switch
{
SsoTransactionPurpose.Login => await LoginAsync(transaction, client, input.ClientId, cancellationToken),
SsoTransactionPurpose.Reauthentication => await ReauthenticatedAsync(transaction, cancellationToken),
_ => Refuse(SsoBrokerOutcome.InvalidRequest)
};
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| } | ||
| catch (Exception ex) when (!(ex is OperationCanceledException)) | ||
| { | ||
| Resgrid.Framework.Logging.LogException(ex, "Shared session operator activity update failed."); |
There was a problem hiding this comment.
SessionValidationMiddleware logs only a message string for the failure, omitting the exception and identifiers needed to correlate the operation. Emit a structured error log containing the operation name, exception, session ID, request trace ID, and user or actor ID, with the same context applied to the listed call sites.
Kody rule violation: Include error context in structured logs
Prompt for LLM
File Web/Resgrid.Web.Services/Middleware/SessionValidationMiddleware.cs:
Line 132:
SessionValidationMiddleware logs only a message string for the failure, omitting the exception and identifiers needed to correlate the operation. Emit a structured error log containing the operation name, exception, session ID, request trace ID, and user or actor ID, with the same context applied to the listed call sites.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| trees.Add(group); | ||
| } | ||
| } | ||
| trees.AddRange(BSTreeModel.ForDepartmentGroups(model.Groups)); |
There was a problem hiding this comment.
model.Groups can be null when passed to BSTreeModel.ForDepartmentGroups, causing a null-related failure in PersonnelController and the other listed call sites. Use Enumerable.Empty<DepartmentGroup>() as the default collection.
Kody rule violation: Add null checks before accessing properties
trees.AddRange(BSTreeModel.ForDepartmentGroups(model.Groups ?? Enumerable.Empty<DepartmentGroup>()));Prompt for LLM
File Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs:
Line 325:
`model.Groups` can be null when passed to `BSTreeModel.ForDepartmentGroups`, causing a null-related failure in PersonnelController and the other listed call sites. Use `Enumerable.Empty<DepartmentGroup>()` as the default collection.
Suggested Code:
trees.AddRange(BSTreeModel.ForDepartmentGroups(model.Groups ?? Enumerable.Empty<DepartmentGroup>()));
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| await _systemAuditsService.SaveSystemAuditAsync(new SystemAudit | ||
| { | ||
| System = (int)SystemAuditSystems.Website, | ||
| Type = (int)SystemAuditTypes.FederatedMfaMappingChanged, | ||
| UserId = UserId, | ||
| Username = UserName, | ||
| Successful = true, | ||
| IpAddress = IpAddressHelper.GetRequestIP(Request, true), | ||
| ServerName = Environment.MachineName, | ||
| Data = removing | ||
| ? $"Provider step-up mapping for SSO configuration {config.DepartmentSsoConfigId} removed (now version {saved?.FederatedMfaMappingVersion})." | ||
| : $"Provider step-up mapping for SSO configuration {config.DepartmentSsoConfigId} saved as version {saved?.FederatedMfaMappingVersion}; " + | ||
| "it counts once it passes its test." | ||
| }, cancellationToken); |
There was a problem hiding this comment.
The federated MFA mapping audit record in SecurityController uses legacy fields and omits tamper-evident metadata required for traceability. Populate UTC timestamp, actor role, action, resource, result, trace ID, IP, and user agent, then ensure the service writes to immutable/WORM storage and forwards the record to the SIEM.
Kody rule violation: Emit tamper-evident audit logs with required fields
await _systemAuditsService.SaveSystemAuditAsync(new SystemAudit
{
TimestampUtc = DateTime.UtcNow,
ActorUserId = UserId,
ActorRole = CurrentRole,
Action = removing ? "FEDERATED_MFA_MAPPING_REMOVE" : "FEDERATED_MFA_MAPPING_SAVE",
ResourceId = config.DepartmentSsoConfigId,
Result = "success",
TraceId = HttpContext.TraceIdentifier,
IpAddress = IpAddressHelper.GetRequestIP(Request, true),
UserAgent = Request.Headers["User-Agent"].ToString()
}, cancellationToken);Prompt for LLM
File Web/Resgrid.Web/Areas/User/Controllers/SecurityController.cs:
Line 1484 to 1497:
The federated MFA mapping audit record in SecurityController uses legacy fields and omits tamper-evident metadata required for traceability. Populate UTC timestamp, actor role, action, resource, result, trace ID, IP, and user agent, then ensure the service writes to immutable/WORM storage and forwards the record to the SIEM.
Suggested Code:
await _systemAuditsService.SaveSystemAuditAsync(new SystemAudit
{
TimestampUtc = DateTime.UtcNow,
ActorUserId = UserId,
ActorRole = CurrentRole,
Action = removing ? "FEDERATED_MFA_MAPPING_REMOVE" : "FEDERATED_MFA_MAPPING_SAVE",
ResourceId = config.DepartmentSsoConfigId,
Result = "success",
TraceId = HttpContext.TraceIdentifier,
IpAddress = IpAddressHelper.GetRequestIP(Request, true),
UserAgent = Request.Headers["User-Agent"].ToString()
}, cancellationToken);
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| Successful = true, | ||
| IpAddress = IpAddressHelper.GetRequestIP(Request, true), | ||
| ServerName = Environment.MachineName, | ||
| Data = $"2FA disabled via web. {Request.Headers["User-Agent"]}" | ||
| Data = $"2FA disabled via web; all sessions revoked. {Request.Headers["User-Agent"]}" | ||
| }, cancellationToken); | ||
| await NoticeAsync(user, SecurityNoticeKind.TotpDisabled, cancellationToken); | ||
|
|
||
| TempData["StatusMessage"] = "Two-factor authentication has been disabled."; | ||
| return RedirectToAction(nameof(Index)); | ||
| await _signInManager.ForgetTwoFactorClientAsync(); | ||
| await _userSessionService.RevokeAllAfterCredentialChangeAsync(user.Id, user.Id, | ||
| UserSessionRevocationReason.MfaChanged, now, cancellationToken); |
There was a problem hiding this comment.
Disable2FA awaits SecurityNoticeService before revoking all sessions and signing out, so a notice-delivery failure can leave the current and other old sessions authenticated after MFA is disabled. Make NoticeAsync best-effort by catching and logging its exception, or revoke sessions and sign out before awaiting the notice.
await _systemAuditsService.SaveSystemAuditAsync(new SystemAudit
{
System = (int)SystemAuditSystems.Website,
Type = (int)SystemAuditTypes.TwoFactorDisabled,
UserId = user.Id,
Username = user.UserName,
Successful = true,
IpAddress = IpAddressHelper.GetRequestIP(Request, true),
ServerName = Environment.MachineName,
Data = $"2FA disabled via web; all sessions revoked. {Request.Headers["User-Agent"]}"
}, cancellationToken);
await _signInManager.ForgetTwoFactorClientAsync();
await _userSessionService.RevokeAllAfterCredentialChangeAsync(user.Id, user.Id,
UserSessionRevocationReason.MfaChanged, now, cancellationToken);
try
{
await NoticeAsync(user, SecurityNoticeKind.TotpDisabled, cancellationToken);
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
Logging.LogException(ex, "Failed to queue MFA-disabled security notice.");
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);Prompt for LLM
File Web/Resgrid.Web/Areas/User/Controllers/TwoFactorController.cs:
Line 412 to 421:
Disable2FA awaits SecurityNoticeService before revoking all sessions and signing out, so a notice-delivery failure can leave the current and other old sessions authenticated after MFA is disabled. Make NoticeAsync best-effort by catching and logging its exception, or revoke sessions and sign out before awaiting the notice.
Suggested Code:
await _systemAuditsService.SaveSystemAuditAsync(new SystemAudit
{
System = (int)SystemAuditSystems.Website,
Type = (int)SystemAuditTypes.TwoFactorDisabled,
UserId = user.Id,
Username = user.UserName,
Successful = true,
IpAddress = IpAddressHelper.GetRequestIP(Request, true),
ServerName = Environment.MachineName,
Data = $"2FA disabled via web; all sessions revoked. {Request.Headers["User-Agent"]}"
}, cancellationToken);
await _signInManager.ForgetTwoFactorClientAsync();
await _userSessionService.RevokeAllAfterCredentialChangeAsync(user.Id, user.Id,
UserSessionRevocationReason.MfaChanged, now, cancellationToken);
try
{
await NoticeAsync(user, SecurityNoticeKind.TotpDisabled, cancellationToken);
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
Logging.LogException(ex, "Failed to queue MFA-disabled security notice.");
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| if (await _userManager.GetTwoFactorEnabledAsync(user)) | ||
| return RedirectToAction(nameof(Index)); | ||
|
|
||
| var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty); | ||
| var isValid = await _userManager.VerifyTwoFactorTokenAsync(user, | ||
| _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); | ||
| var stagedKey = await GetStagedAuthenticatorKeyAsync(user); | ||
| if (string.IsNullOrEmpty(stagedKey)) | ||
| return RedirectToAction(nameof(Enable2FA)); |
There was a problem hiding this comment.
The Enable2FA and ReplaceAuthenticator POST actions accept staged authenticator codes without rechecking the fresh first-factor requirement enforced by their GET actions, allowing credential-change authorization to outlive its intended window. Re-run HasFreshFirstFactorAsync at the start of each POST action and redirect to reauthentication when it is no longer valid.
if (await _userManager.GetTwoFactorEnabledAsync(user))
return RedirectToAction(nameof(Index));
if (!await HasFreshFirstFactorAsync(user, TwoFactorConfig.FirstFactorReauthWindowMinutes))
return RedirectToReauthenticate(Url.Action(nameof(Enable2FA)));
var stagedKey = await GetStagedAuthenticatorKeyAsync(user);Prompt for LLM
File Web/Resgrid.Web/Areas/User/Controllers/TwoFactorController.cs:
Line 177 to 182:
The Enable2FA and ReplaceAuthenticator POST actions accept staged authenticator codes without rechecking the fresh first-factor requirement enforced by their GET actions, allowing credential-change authorization to outlive its intended window. Re-run HasFreshFirstFactorAsync at the start of each POST action and redirect to reauthentication when it is no longer valid.
Suggested Code:
if (await _userManager.GetTwoFactorEnabledAsync(user))
return RedirectToAction(nameof(Index));
if (!await HasFreshFirstFactorAsync(user, TwoFactorConfig.FirstFactorReauthWindowMinutes))
return RedirectToReauthenticate(Url.Action(nameof(Enable2FA)));
var stagedKey = await GetStagedAuthenticatorKeyAsync(user);
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| Passkeys = (await _passkeys.GetActiveForUserAsync(user.Id, cancellationToken) ?? Array.Empty<UserPasskey>()) | ||
| .OrderBy(p => p.ClientApplication).ThenBy(p => p.CreatedOnUtc) | ||
| .Select(p => new PasskeyRowView | ||
| { | ||
| Id = p.UserPasskeyId, | ||
| DisplayName = p.DisplayName, | ||
| AppLabelKey = AppLabelKey((UserSessionClientApplication)p.ClientApplication), | ||
| CreatedOn = DateTime.SpecifyKind(p.CreatedOnUtc, DateTimeKind.Utc), | ||
| LastUsedOn = p.LastUsedOnUtc == null ? null : DateTime.SpecifyKind(p.LastUsedOnUtc.Value, DateTimeKind.Utc), | ||
| CreatedOnSharedInstallation = p.RegisteredInSharedMode | ||
| }).ToList() |
There was a problem hiding this comment.
The multi-stage passkey query combines retrieval, ordering, projection, and materialization in one expression, making each stage difficult to inspect independently. Split it into named intermediate expressions for activePasskeys, orderedPasskeys, and the final Passkeys projection.
Kody rule violation: Limit Lengthy LINQ Chains
var activePasskeys = await _passkeys.GetActiveForUserAsync(user.Id, cancellationToken) ?? Array.Empty<UserPasskey>();
var orderedPasskeys = activePasskeys.OrderBy(p => p.ClientApplication).ThenBy(p => p.CreatedOnUtc);
Passkeys = orderedPasskeys.Select(p => new PasskeyRowView { ... }).ToList()Prompt for LLM
File Web/Resgrid.Web/Areas/User/Controllers/TwoFactorController.cs:
Line 126 to 136:
The multi-stage passkey query combines retrieval, ordering, projection, and materialization in one expression, making each stage difficult to inspect independently. Split it into named intermediate expressions for activePasskeys, orderedPasskeys, and the final Passkeys projection.
Suggested Code:
var activePasskeys = await _passkeys.GetActiveForUserAsync(user.Id, cancellationToken) ?? Array.Empty<UserPasskey>();
var orderedPasskeys = activePasskeys.OrderBy(p => p.ClientApplication).ThenBy(p => p.CreatedOnUtc);
Passkeys = orderedPasskeys.Select(p => new PasskeyRowView { ... }).ToList()
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| @@ -366,12 +366,12 @@ | |||
| { | |||
| if (count == 0) | |||
| { | |||
| @Html.Raw("<li role='presentation' class='active'><a href='#unitsTab" + Model.Groups[i].DepartmentGroupId + "' aria-controls='home' role='tab' data-toggle='tab'>" + Model.Groups[i].Name + "</a></li>") | |||
| @Html.Raw("<li role='presentation' class='active'><a href='#unitsTab" + Model.Groups[i].DepartmentGroupId + "' aria-controls='home' role='tab' data-toggle='tab'>" + Html.Encode(Model.Groups[i].Name) + "</a></li>") | |||
There was a problem hiding this comment.
DepartmentGroupId is interpolated into raw HTML through Html.Raw without encoding, allowing model-derived values to inject markup or attributes. Encode DepartmentGroupId before inserting it into the attribute, and apply the same protection to the listed views.
Kody rule violation: Always sanitize user inputs
@Html.Raw("<li role='presentation' class='active'><a href='#unitsTab" + Html.Encode(Model.Groups[i].DepartmentGroupId) + "' aria-controls='home' role='tab' data-toggle='tab'>" + Html.Encode(Model.Groups[i].Name) + "</a></li>")Prompt for LLM
File Web/Resgrid.Web/Areas/User/Views/Dispatch/NewCall.cshtml:
Line 369:
DepartmentGroupId is interpolated into raw HTML through Html.Raw without encoding, allowing model-derived values to inject markup or attributes. Encode DepartmentGroupId before inserting it into the attribute, and apply the same protection to the listed views.
Suggested Code:
@Html.Raw("<li role='presentation' class='active'><a href='#unitsTab" + Html.Encode(Model.Groups[i].DepartmentGroupId) + "' aria-controls='home' role='tab' data-toggle='tab'>" + Html.Encode(Model.Groups[i].Name) + "</a></li>")
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| [HttpPost] | ||
| [AllowAnonymous] | ||
| [ValidateAntiForgeryToken] | ||
| public async Task<IActionResult> LoginMfaSetup(LoginMfaSetupViewModel model, CancellationToken cancellationToken) |
There was a problem hiding this comment.
LoginMfaSetup and the listed POST actions process the submitted model and may issue downstream operations before validating ModelState. Return the view immediately when ModelState.IsValid is false.
Kody rule violation: Always Validate `ModelState.IsValid` in Controllers
if (!ModelState.IsValid) return View(model);Prompt for LLM
File Web/Resgrid.Web/Controllers/AccountController.Recovery.cs:
Line 71:
LoginMfaSetup and the listed POST actions process the submitted model and may issue downstream operations before validating ModelState. Return the view immediately when `ModelState.IsValid` is false.
Suggested Code:
if (!ModelState.IsValid) return View(model);
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| // Authentication and session flows stay available during a department operation lock (ADP plan section 20.2): signing in | ||
| // and out, locking and unlocking a shared session, and verifying a second factor touch no department data. | ||
| [Resgrid.Web.Filters.AllowDuringDepartmentLock] | ||
| public partial class AccountController : Controller |
There was a problem hiding this comment.
The controller-wide AllowDuringDepartmentLock exemption bypasses the department operation-lock filter for every AccountController action, including ForcePasswordChange, which writes department membership state and defeats the filter's data-entry boundary. Remove the controller-level exemption and apply AllowDuringDepartmentLock only to the required sign-in, sign-out, and MFA/shared-session actions.
public partial class AccountController : Controller
{
// Apply [AllowDuringDepartmentLock] only to the individual authentication/MFA actions that require it.Prompt for LLM
File Web/Resgrid.Web/Controllers/AccountController.cs:
Line 39 to 42:
The controller-wide AllowDuringDepartmentLock exemption bypasses the department operation-lock filter for every AccountController action, including ForcePasswordChange, which writes department membership state and defeats the filter's data-entry boundary. Remove the controller-level exemption and apply AllowDuringDepartmentLock only to the required sign-in, sign-out, and MFA/shared-session actions.
Suggested Code:
public partial class AccountController : Controller
{
// Apply [AllowDuringDepartmentLock] only to the individual authentication/MFA actions that require it.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| { | ||
| <div class="checkbox"> | ||
| <label> | ||
| <input type="checkbox" name="RemovePasskeyIds" value="@passkey.Id" checked="@Model.RemovePasskeyIds.Contains(passkey.Id)" /> |
There was a problem hiding this comment.
The passkey-removal checkbox always renders a boolean checked attribute, including when the model value is false; HTML treats checked="False" as checked, so recovery submits every displayed passkey in RemovePasskeyIds. Render the attribute only when Model.RemovePasskeyIds.Contains(passkey.Id) is true.
<input type="checkbox" name="RemovePasskeyIds" value="@passkey.Id" @(Model.RemovePasskeyIds.Contains(passkey.Id) ? "checked=\"checked\"" : "") />Prompt for LLM
File Web/Resgrid.Web/Views/Account/Recovery.cshtml:
Line 58:
The passkey-removal checkbox always renders a boolean checked attribute, including when the model value is false; HTML treats `checked="False"` as checked, so recovery submits every displayed passkey in RemovePasskeyIds. Render the attribute only when `Model.RemovePasskeyIds.Contains(passkey.Id)` is true.
Suggested Code:
<input type="checkbox" name="RemovePasskeyIds" value="@passkey.Id" @(Model.RemovePasskeyIds.Contains(passkey.Id) ? "checked=\"checked\"" : "") />
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| <div class="row"> | ||
| <div class="col-md-6 col-md-offset-3"> | ||
| <div style="text-align: center;"> | ||
| <img src="~/images/Resgrid_JustText.png" style="width: 200px;" /> |
There was a problem hiding this comment.
The SsoLogOn view and listed views serve only a PNG without explicit dimensions or accessible text, which can reduce image performance and cause layout shifts. Serve AVIF/WebP with a PNG fallback and provide meaningful alt text, explicit width and height, loading="lazy", and decoding="async".
Kody rule violation: Serve responsive images with modern formats and lazy-load
<picture><source srcset="~/images/Resgrid_JustText.avif" type="image/avif" /><source srcset="~/images/Resgrid_JustText.webp" type="image/webp" /><img src="~/images/Resgrid_JustText.png" alt="Resgrid" width="200" height="40" loading="lazy" decoding="async" /></picture>Prompt for LLM
File Web/Resgrid.Web/Views/Account/SsoLogOn.cshtml:
Line 23:
The SsoLogOn view and listed views serve only a PNG without explicit dimensions or accessible text, which can reduce image performance and cause layout shifts. Serve AVIF/WebP with a PNG fallback and provide meaningful alt text, explicit width and height, `loading="lazy"`, and `decoding="async"`.
Suggested Code:
<picture><source srcset="~/images/Resgrid_JustText.avif" type="image/avif" /><source srcset="~/images/Resgrid_JustText.webp" type="image/webp" /><img src="~/images/Resgrid_JustText.png" alt="Resgrid" width="200" height="40" loading="lazy" decoding="async" /></picture>
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| }).then(function (status) { | ||
| if (status && status.shared && !status.locked) | ||
| back(); | ||
| }).catch(function () { }); |
There was a problem hiding this comment.
The empty catch in Web/Resgrid.Web/wwwroot/js/app/common/shared/resgrid.shared.locked.js silently swallows exceptions, obscuring failures and preventing recovery. Log the exception with relevant context and either rethrow it or handle it explicitly.
Kody rule violation: Avoid empty catch blocks
Prompt for LLM
File Web/Resgrid.Web/wwwroot/js/app/common/shared/resgrid.shared.locked.js:
Line 51:
The empty catch in `Web/Resgrid.Web/wwwroot/js/app/common/shared/resgrid.shared.locked.js` silently swallows exceptions, obscuring failures and preventing recovery. Log the exception with relevant context and either rethrow it or handle it explicitly.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| if (xhr.status === 403) { | ||
| if (xhr.status === 403 && xhr.responseJSON && xhr.responseJSON.error === 'step_up_required' && xhr.responseJSON.redirectUrl) { | ||
| // The department requires MFA this session has not completed: verify, then come back and switch. | ||
| window.location.href = xhr.responseJSON.redirectUrl; |
There was a problem hiding this comment.
Assigning xhr.responseJSON.redirectUrl directly to window.location.href permits arbitrary or untrusted redirections. Parse the URL and allow navigation only when its origin matches window.location.origin and its protocol is https:.
Kody rule violation: Avoid unprotected HTTP request redirections
const redirectUrl = new URL(xhr.responseJSON.redirectUrl, window.location.origin);
if (redirectUrl.origin === window.location.origin && redirectUrl.protocol === 'https:') {
window.location.href = redirectUrl.href;
}Prompt for LLM
File Web/Resgrid.Web/wwwroot/js/app/internal/profile/resgrid.profile.yourdepartments.js:
Line 58:
Assigning xhr.responseJSON.redirectUrl directly to window.location.href permits arbitrary or untrusted redirections. Parse the URL and allow navigation only when its origin matches window.location.origin and its protocol is `https:`.
Suggested Code:
const redirectUrl = new URL(xhr.responseJSON.redirectUrl, window.location.origin);
if (redirectUrl.origin === window.location.origin && redirectUrl.protocol === 'https:') {
window.location.href = redirectUrl.href;
}
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
|
Tip For best results, initiate chat on the files or code changes.
You are interacting with an AI system. |
|
@CodeRabbit Review |
|
No description provided.