diff --git a/SaBusMgrStandardBase/SaBusMgrStandardBase.csproj b/SaBusMgrStandardBase/SaBusMgrStandardBase.csproj
index 7167e98..d7e806f 100644
--- a/SaBusMgrStandardBase/SaBusMgrStandardBase.csproj
+++ b/SaBusMgrStandardBase/SaBusMgrStandardBase.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/SaBusMgrTest_Control/Base/Helpers/PasswordHelper_Tester.cs b/SaBusMgrTest_Control/Base/Helpers/PasswordHelper_Tester.cs
new file mode 100644
index 0000000..7b1847c
--- /dev/null
+++ b/SaBusMgrTest_Control/Base/Helpers/PasswordHelper_Tester.cs
@@ -0,0 +1,77 @@
+using Sarand.BusinessMgr.Net.Standard.Base.Bus.Control;
+using Sarand.BusinessMgr.Net.Standard.Base.Helpers;
+using Sarand.Net.Standard.Base.Bus;
+using Sarand.Net.Standard.Base.Helpers;
+using Sarand.Net.Standard.Base.Security;
+
+namespace Sarand.BusinessMgr.Test.Base.Helpers
+{
+ [TestClass]
+ public class PasswordHelper_Tester
+ {
+ // --- Test Data ---
+
+ private const string Password = "Valid!Password42";
+ private const string WrongPassword = "Wrong!Password42";
+ private const string LegacyPassword = "Legacy!Password42";
+ private const string LegacyIdentityV3Hash = "AQAAAAIAACcQAAAAEAABAgMEBQYHCAkKCwwNDg9chprUQ95IQiyEv3ItRa7O6I5hI4LOsBdWYiBlkyOHwA==";
+
+ // --- Password Hashing ---
+
+ [TestMethod]
+ public void SbmPasswordHash_RoundTripsAndRejectsWrongPassword()
+ {
+ TestSecurityItem securityItem = new TestSecurityItem();
+ string passwordHash = SbmPasswordHelper.GetPasswordHash(securityItem, Password);
+ securityItem.SetPasswordHash(passwordHash);
+
+ Assert.IsTrue(SbmPasswordHelper.VerifyHashedPassword(securityItem, Password));
+ Assert.IsTrue(SbmPasswordHelper.VerifyHashedPassword(securityItem, Password, passwordHash));
+ Assert.IsFalse(SbmPasswordHelper.VerifyHashedPassword(securityItem, WrongPassword, passwordHash));
+ Assert.IsFalse(SbmPasswordHelper.PasswordRequiresRehash(securityItem, Password));
+ }
+
+ [TestMethod]
+ public void SbmcPasswordHash_RoundTripsAndRejectsWrongPassword()
+ {
+ TestUserAccount userAccount = new TestUserAccount();
+ string passwordHash = SbmcPasswordHelper.GetPasswordHash(userAccount, Password);
+ userAccount.PasswordHashed = passwordHash;
+
+ Assert.IsTrue(SbmcPasswordHelper.VerifyHashedPassword(userAccount, Password));
+ Assert.IsTrue(SbmcPasswordHelper.VerifyHashedPassword(userAccount, Password, passwordHash));
+ Assert.IsFalse(SbmcPasswordHelper.VerifyHashedPassword(userAccount, WrongPassword, passwordHash));
+ Assert.IsFalse(SbmcPasswordHelper.PasswordRequiresRehash(userAccount, Password));
+ }
+
+ [TestMethod]
+ public void LegacyIdentityV3Hash_RemainsValidAndRequiresRehash()
+ {
+ TestSecurityItem securityItem = new TestSecurityItem();
+ TestUserAccount userAccount = new TestUserAccount();
+ securityItem.SetPasswordHash(LegacyIdentityV3Hash);
+ userAccount.PasswordHashed = LegacyIdentityV3Hash;
+
+ Assert.IsTrue(SbmPasswordHelper.VerifyHashedPassword(securityItem, LegacyPassword, LegacyIdentityV3Hash));
+ Assert.IsTrue(SbmcPasswordHelper.VerifyHashedPassword(userAccount, LegacyPassword, LegacyIdentityV3Hash));
+ Assert.IsTrue(SbmPasswordHelper.PasswordRequiresRehash(securityItem, LegacyPassword));
+ Assert.IsTrue(SbmcPasswordHelper.PasswordRequiresRehash(userAccount, LegacyPassword));
+ }
+
+ // --- Test Support ---
+
+ private sealed class TestSecurityItem : SaSecurityItem
+ {
+ public TestSecurityItem() : base(new SysId(1), SaSecurity.ItemType.User) { }
+
+ public void SetPasswordHash(string passwordHash) { PasswordHash = passwordHash; }
+ }
+
+ private sealed class TestUserAccount : SbmcUserAccount
+ {
+ public TestUserAccount() : base(new SysId(1)) { }
+ }
+
+ // --- The End ---
+ }
+}
diff --git a/docs/component-reviews.md b/docs/component-reviews.md
index 9488f7a..6eaa7e1 100644
--- a/docs/component-reviews.md
+++ b/docs/component-reviews.md
@@ -7,9 +7,10 @@ table td {
}
-Last updated: 2026-09-12
+Last updated: 2026-09-13
| Component | Scope | Status | Date reviewed | Next steps | Notes |
|---|---|---|---|---|---|
-| `BusinessMgr.sln` | .NET 10 restore, Debug build, and tests | Migrated; passing with warnings and toolchain requirement | 2026-09-10 | Review vulnerable and framework-specific packages, perform application smoke tests, then release-validate before removing Common's temporary .NET 8 targets. | All eight projects target `net10.0`. The complete solution builds with Visual Studio 18 MSBuild; the .NET CLI cannot build `SaBusMgrTest_Portal` because its SSDT SQL unit-testing targets are Visual Studio-specific (`MSB4278`). Control tests pass 8 of 8 and Portal tests pass 13 of 13. |
-| `eng/validate.ps1` | Repository validation | Complete; passing | 2026-09-12 | Use as the standard validation entry point and extend it when repository-wide checks change. | Locates Visual Studio MSBuild for the SSDT project, builds serially, runs 8 Control and 13 Portal tests, and verifies changed-file line endings and diffs. |
+| `BusinessMgr.sln` | .NET 10 restore, Debug build, and tests | Migrated; passing with warnings and toolchain requirement | 2026-09-13 | Review remaining framework-specific packages, perform application smoke tests, then release-validate. | All eight projects target `net10.0`. The complete solution builds with Visual Studio 18 MSBuild; the .NET CLI cannot build `SaBusMgrTest_Portal` because its SSDT SQL unit-testing targets are Visual Studio-specific (`MSB4278`). Control tests pass 11 of 11 and Portal tests pass 13 of 13. The Identity-related `NU1903` warnings have been removed. |
+| `SaBusMgrStandardBase/Base/Helpers/SbmPasswordHelper.cs` and `SaBusinessMgrCoreWebControl/Control/Helpers/SbmcPasswordHelper.cs` | Password hashing | Dependency upgrade complete; legacy hashes compatible | 2026-09-13 | Preserve the compatibility tests when password-hashing defaults or account migration behavior changes. | Both helpers use `PasswordHasher` from `Microsoft.Extensions.Identity.Core` 10.0.12. Current hashes round-trip, wrong passwords fail, and legacy Identity V3 hashes remain valid while being identified for rehash. |
+| `eng/validate.ps1` | Repository validation | Complete; passing | 2026-09-13 | Use as the standard validation entry point and extend it when repository-wide checks change. | Locates Visual Studio MSBuild for the SSDT project, builds serially, runs 11 Control and 13 Portal tests, audits the Identity-package-owning project, and verifies changed-file line endings and diffs. |
diff --git a/docs/topics.md b/docs/topics.md
index eaf657d..e2d5927 100644
--- a/docs/topics.md
+++ b/docs/topics.md
@@ -7,14 +7,13 @@ table td {
}
-Last updated: 2026-09-12
+Last updated: 2026-09-13
## Prioritised Topics to Be Actioned
| Priority | Topic | Date noted | Next step | Notes |
|---|---|---|---|---|
-| P1 | Remove vulnerable transitive `System.Security.Cryptography.Xml` 8.0.2 dependency | 2026-09-10 | Upgrade or replace the legacy ASP.NET Core Identity 2.3.1 dependency, then confirm restore is clean. | The Common NPOI path is complete: NPOI 2.7.6 plus an explicit `System.Security.Cryptography.Xml` 10.0.12 reference passes the spreadsheet, Common, and BusinessMgr suites. BusinessMgr still resolves version 8.0.2 independently through `Microsoft.AspNetCore.DataProtection` 2.3.0 from ASP.NET Core Identity 2.3.1, producing the remaining `NU1903` advisories. |
-| P2 | Review framework-specific package versions after .NET 10 retargeting | 2026-09-10 | Review ASP.NET Core Identity and Razor runtime compilation, ODBC, and System.Drawing packages for supported .NET 10 versions and behavior before release validation. | Existing package versions compile and all tests pass, but several remain on 2.x or 6.x generations and should not be assumed to be fully supported merely because compilation succeeds. |
+| P2 | Review framework-specific package versions after .NET 10 retargeting | 2026-09-10 | Review Razor runtime compilation, ODBC, and System.Drawing packages for supported .NET 10 versions and behavior before release validation. | The legacy ASP.NET Core Identity package has been replaced with the .NET 10 Identity Core package. Other package versions compile and all tests pass, but several remain on 6.x generations and should not be assumed to be fully supported merely because compilation succeeds. |
## .NET 10 Dependencies and Sequence
@@ -39,3 +38,4 @@ Last updated: 2026-09-12
| Pin the BusinessMgr .NET 10 SDK | 2026-09-10 | 2026-09-10 | 2026-09-10 | Added `global.json` for SDK 10.0.401 with `latestPatch` roll-forward and prerelease SDKs disabled. Visual Studio MSBuild remains required for the SSDT-dependent Portal test project. |
| Migrate BusinessMgr to .NET 10 against compatible Common targets | 2026-09-10 | 2026-09-10 | 2026-09-10 | All eight BusinessMgr projects now target `net10.0`. Visual Studio 18 MSBuild restored and built the complete solution successfully with no compilation errors. Control tests passed 8 of 8 and Portal tests passed 13 of 13 on .NET 10. Package vulnerability and framework-specific package reviews are tracked separately. |
| Automate BusinessMgr validation | 2026-09-12 | 2026-09-12 | 2026-09-12 | Added `eng/validate.ps1` to locate Visual Studio MSBuild, build the SSDT-dependent solution serially, run both test assemblies, and verify diffs and changed-file line endings. The script passes with 8 of 8 Control and 13 of 13 Portal tests. |
+| Remove vulnerable transitive `System.Security.Cryptography.Xml` 8.0.2 dependency | 2026-09-10 | 2026-09-13 | 2026-09-13 | Replaced the broad legacy `Microsoft.AspNetCore.Identity` 2.3.1 reference with `Microsoft.Extensions.Identity.Core` 10.0.12, matching the only used API, `PasswordHasher`. Three focused tests cover both password helpers, wrong-password rejection, current hash round trips, legacy Identity V3 verification, and rehash detection. Restore and build no longer report `NU1903`; 11 Control and 13 Portal tests pass, and the package audit reports no vulnerable packages for `SaBusMgrStandardBase`. |
diff --git a/eng/README.md b/eng/README.md
index ade8d6a..580fd6f 100644
--- a/eng/README.md
+++ b/eng/README.md
@@ -6,4 +6,4 @@ Run the complete BusinessMgr validation:
.\eng\validate.ps1
```
-Use `-SkipRestore` when packages are already restored. The script suppresses .NET first-run setup and banners, locates Visual Studio MSBuild because the Portal test project requires Visual Studio SSDT targets, builds serially, derives test output paths from each project's target framework, runs both test assemblies, stores test output under the ignored `TestResults/Validation` folder, checks the Git diff, and verifies working-tree line endings without modifying files. Its isolated `DOTNET_CLI_HOME` stores only CLI state; NuGet packages reuse `NUGET_PACKAGES` when already configured or the user's standard `.nuget/packages` cache.
+Use `-SkipRestore` when packages are already restored. The script suppresses .NET first-run setup and banners, locates Visual Studio MSBuild because the Portal test project requires Visual Studio SSDT targets, builds serially, derives test output paths from each project's target framework, runs both test assemblies, audits the Identity-package-owning project for vulnerable packages, stores test output under the ignored `TestResults/Validation` folder, checks the Git diff, and verifies working-tree line endings without modifying files. The focused project audit avoids loading the Portal test project's Visual Studio-only SSDT targets through CLI MSBuild. The script's isolated `DOTNET_CLI_HOME` stores only CLI state; NuGet packages reuse `NUGET_PACKAGES` when already configured or the user's standard `.nuget/packages` cache.
diff --git a/eng/validate.ps1 b/eng/validate.ps1
index 563ad1f..6947e1c 100644
--- a/eng/validate.ps1
+++ b/eng/validate.ps1
@@ -124,6 +124,7 @@ try
$portalAssembly = Get-TestAssemblyPath 'SaBusMgrTest_Portal\SaBusMgrTest_Portal.csproj' 'SaBusMgrTest_Portal'
Invoke-External dotnet @('vstest', $controlAssembly, "--TestAdapterPath:$(Split-Path -Parent $controlAssembly)", "--ResultsDirectory:$testResultsPath")
Invoke-External dotnet @('vstest', $portalAssembly, "--TestAdapterPath:$(Split-Path -Parent $portalAssembly)", "--ResultsDirectory:$testResultsPath")
+ Invoke-External dotnet @('list', 'SaBusMgrStandardBase\SaBusMgrStandardBase.csproj', 'package', '--vulnerable', '--include-transitive')
Invoke-External git @('diff', '--check')
Test-LineEndings