Skip to content
Merged
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
11 changes: 6 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
# .NET 10.0 manifest digest resolved 2026-09-11.
FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:60a2b2230a0d052bc54c0d453e97e331219ed503c5411470ee226859420e693c AS build
WORKDIR /app

# Install CA certificates so HTTPS requests to CDNs (cdnjs) work
Expand All @@ -22,7 +23,8 @@ WORKDIR /app/src/RfcBuddy.Web
RUN dotnet publish -c Release -o /out

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
# .NET 10.0 manifest digest resolved 2026-09-11.
FROM mcr.microsoft.com/dotnet/aspnet:10.0@sha256:900c2dd83cc0cef53db0aaf786f12fe766ee075b6334750a664c9e77e7a7c0c5 AS runtime
WORKDIR /app
COPY --from=build /out ./

Expand All @@ -38,8 +40,7 @@ VOLUME /app/data
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENV \
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 \
LC_ALL=en_US.UTF-8 \
LANG=en_US.UTF-8
LC_ALL=C.UTF-8 \
LANG=C.UTF-8

ENTRYPOINT ["dotnet", "RfcBuddy.Web.dll"]
396 changes: 221 additions & 175 deletions docs/architecture.md

Large diffs are not rendered by default.

603 changes: 88 additions & 515 deletions docs/security-review.md

Large diffs are not rendered by default.

84 changes: 0 additions & 84 deletions packages.lock.json

This file was deleted.

6 changes: 3 additions & 3 deletions src/RfcBuddy.App.Tests/RfcBuddy.App.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="JunitXml.TestLogger" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageReference Include="MSTest.TestAdapter" Version="4.2.3" />
<PackageReference Include="MSTest.TestFramework" Version="4.2.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.0" />
<PackageReference Include="MSTest.TestAdapter" Version="4.4.0" />
<PackageReference Include="MSTest.TestFramework" Version="4.4.0" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 4 additions & 0 deletions src/RfcBuddy.App.Tests/Services/UserRegistryServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public void BootstrapFirstUserAsAdminAndManageAdminStatus()
Assert.IsTrue(registry.SetAdmin("user-1", false, "user-2"));
Assert.IsFalse(registry.IsAdmin("user-1"));

// Non-admin and unknown requesters cannot change administrative status.
Assert.IsFalse(registry.SetAdmin("user-1", true, "user-1"));
Assert.IsFalse(registry.SetAdmin("user-1", true, "missing-user"));

// Attempting to demote the last remaining admin (user-2) should fail
Assert.IsFalse(registry.SetAdmin("user-2", false, "user-1"));
Assert.IsTrue(registry.IsAdmin("user-2")); // Still admin
Expand Down
6 changes: 3 additions & 3 deletions src/RfcBuddy.App.Tests/Services/UserServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ public void GetAndSaveUserKeywordsRoundTrip()

userService.GetUserKeywords(out var ministry, out var general, out var ignore);

CollectionAssert.AreEqual(new List<string> { "ministry" }, ministry);
CollectionAssert.AreEqual(new List<string> { "general1", "general2" }, general);
CollectionAssert.AreEqual(new List<string> { "ignore" }, ignore);
Assert.AreSequenceEqual(new List<string> { "ministry" }, ministry);
Assert.AreSequenceEqual(new List<string> { "general1", "general2" }, general);
Assert.AreSequenceEqual(new List<string> { "ignore" }, ignore);
}
finally
{
Expand Down
113 changes: 49 additions & 64 deletions src/RfcBuddy.App.Tests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,34 @@
},
"Microsoft.NET.Test.Sdk": {
"type": "Direct",
"requested": "[18.7.0, )",
"resolved": "18.7.0",
"contentHash": "49xH9j4UzCh2hMohJp53g3wUTvyycECw7CtVht4gfCz5ykudB1uBcF6D0TtgJPjCtP76UPW53bQElKdCeX+dUg==",
"requested": "[18.10.0, )",
"resolved": "18.10.0",
"contentHash": "0CZZ688UBvpQFB9HKo7wrNJNVe+AOM4UtipgwpER3TTM+d2DDM6QyKkIJZTR3mpbn9/zFbZEtAgVIz4SPTjQZQ==",
"dependencies": {
"Microsoft.CodeCoverage": "18.7.0",
"Microsoft.TestPlatform.TestHost": "18.7.0"
"Microsoft.CodeCoverage": "18.10.0",
"Microsoft.TestPlatform.TestHost": "18.10.0"
}
},
"MSTest.TestAdapter": {
"type": "Direct",
"requested": "[4.2.3, )",
"resolved": "4.2.3",
"contentHash": "oVV/luk0bBghnVvLaw8MwFlD7It0Cx9P2nKobeqIafmTQqFFWY69Wo801dxjeNaLzO/o9WQ84WSK84gXJuubhg==",
"requested": "[4.4.0, )",
"resolved": "4.4.0",
"contentHash": "xZdlCr2r/6XFOo67jcemJsfm46tTgXdqtnISFIuDUAJf5Yxk07XpTC9eMUErLdlYD8QM9zw07eOPre7+nTayUg==",
"dependencies": {
"MSTest.TestFramework": "4.2.3",
"Microsoft.Testing.Extensions.VSTestBridge": "2.2.3",
"Microsoft.Testing.Platform.MSBuild": "2.2.3"
"MSTest.TestFramework": "4.4.0",
"Microsoft.TestPlatform.ObjectModel": "18.9.0",
"Microsoft.Testing.Extensions.Telemetry": "2.4.0",
"Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.4.0",
"Microsoft.Testing.Platform.MSBuild": "2.4.0"
}
},
"MSTest.TestFramework": {
"type": "Direct",
"requested": "[4.2.3, )",
"resolved": "4.2.3",
"contentHash": "9zzij59YLh+tf+FRLNqhzHjmdspR91bol+jdQxLlxxTGMAML6LDbvuyXKMGdcrE84+QpKkk6KjTVBC5PBGrDeA==",
"requested": "[4.4.0, )",
"resolved": "4.4.0",
"contentHash": "sfJzj3ntdU/GahJd2n+WQLgKqbV0RRMJVXtjRwRQLhblevlzLSDAOocOKANhfMaCEQ5dMt6348ztbVq/Ap60xA==",
"dependencies": {
"MSTest.Analyzers": "4.2.3"
"MSTest.Analyzers": "4.4.0"
}
},
"DocX": {
Expand Down Expand Up @@ -73,99 +75,82 @@
},
"Microsoft.CodeCoverage": {
"type": "Transitive",
"resolved": "18.7.0",
"contentHash": "+wFfx9s7D9wegM0RziXMj2kvYDT4qcqXXtyjiQwSZOGQ2wwcOAJQcD6eQXk02jt0MvRNawtp8TJxTrV+wD8X1g=="
"resolved": "18.10.0",
"contentHash": "zo1hqV+Nz6FlIwdc+aPfrvGzqfKAPPaZ6OPj1W8C0QQE2xH951AZwTUytL3K7XX8brVBUFd7MhrLDl3CtK3U/A=="
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.9",
"contentHash": "qGhRPd3VxfLV9UqatVOiD9mAeUbj2KiMwGFYC5uXlzExiZQoe4X/hdmzGIU7BQjNLTqCnnbTHVyBglG3668/HA==",
"resolved": "10.0.12",
"contentHash": "8xaGcvS/qZ1otoxPQCEJkNva389CVL/plNcvIETZhQTETYdRkYDPEYhUMoAGONo4FU45ufdfE0j29AfWVVj0wA==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.9"
"Microsoft.Extensions.Primitives": "10.0.12"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "10.0.9",
"contentHash": "g41l/30G3K4B/d/L8kjux0+30e27c8D0FVQ/PFCpbekgfDpj9mnDhieP67EqXWvl1EWNeZh2rpR4F5B/jcDOHA=="
"resolved": "10.0.12",
"contentHash": "9/qymSh7hVDMGTGwrLz8MRp5zRyXy9adGDOs4HwRdnLil3oZGYuWeZjbmHgCQ9BL1qBroVfgUK3U/nb61617Cw=="
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "10.0.9",
"contentHash": "9S/DFt4cohlMPpzIxjG6kk0L8MuN2vDm9pbMCulxtJzzk82oJHVLBd8vuQxaPskaYQwKqmFmbannf5eoChgjYg==",
"resolved": "10.0.12",
"contentHash": "+24lC4plfbEDNfLAdTV/SWKS7dW+16X4HdydO3R++134kSNTzcbYA4KpR1Hdh6uWisB8Za3AzwyOn+K+NxWIug==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9"
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.9",
"contentHash": "fmEbAUFsaIKirgLt/lYhuFRBwhcSJN31jjHgCdbQxJiWOum6EdLjkbgGuukSP9z/a+9LibaxII/kF+GwOXgC4g=="
"resolved": "10.0.12",
"contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ=="
},
"Microsoft.Testing.Extensions.Telemetry": {
"type": "Transitive",
"resolved": "2.2.3",
"contentHash": "mLdW+JOR3kXYGTdgR/qc/UZBA0r+eCR2k6bUxTcuDj5w9WdIQ7Lol5MBUU7YOSGd9bs9bvhSYWAptgz0YtQqCA==",
"resolved": "2.4.0",
"contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==",
"dependencies": {
"Microsoft.ApplicationInsights": "2.23.0",
"Microsoft.Testing.Platform": "2.2.3"
"Microsoft.Testing.Platform": "[2.4.0, 3.0.0)"
}
},
"Microsoft.Testing.Extensions.TrxReport.Abstractions": {
"type": "Transitive",
"resolved": "2.2.3",
"contentHash": "hntvxJEkmUAx6C2xXc/PO38DqEQl4rimzOgSvTR1hAMruMid7R4RcXOrzzF33J66gKaN7jRaQ0TMW/nNfaV9jw==",
"resolved": "2.4.0",
"contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==",
"dependencies": {
"Microsoft.Testing.Platform": "2.2.3"
}
},
"Microsoft.Testing.Extensions.VSTestBridge": {
"type": "Transitive",
"resolved": "2.2.3",
"contentHash": "7WlJISO8QKUK+d+WhgnANwy4ACwUrvICnviY/mthPwjZ2gVeDaSUAeBnMy2cxfzZgm8VATtGUDbYzUxsgV2CyQ==",
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "18.3.0",
"Microsoft.Testing.Extensions.Telemetry": "2.2.3",
"Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.3",
"Microsoft.Testing.Platform": "2.2.3"
"Microsoft.Testing.Platform": "[2.4.0, 3.0.0)"
}
},
"Microsoft.Testing.Platform": {
"type": "Transitive",
"resolved": "2.2.3",
"contentHash": "LhM1/Qoi8Ams5QcD4r3f09CSOono9iQr3NEJQItFtyzWB55nWTgEOsVqXqMWWWIwk3nkPqc+XfnlJmp8xUI5fg=="
"resolved": "2.4.0",
"contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ=="
},
"Microsoft.Testing.Platform.MSBuild": {
"type": "Transitive",
"resolved": "2.2.3",
"contentHash": "Q22jJYJLx4srTinsAuoCskqmzjrBJC8YeGJMHHIcrf1dQeHoEZ7wsqDzTlENkMoke2qfufF7U+9u58nlZunH/Q==",
"resolved": "2.4.0",
"contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==",
"dependencies": {
"Microsoft.Testing.Platform": "2.2.3"
"Microsoft.Testing.Platform": "[2.4.0, 3.0.0)"
}
},
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
"resolved": "18.7.0",
"contentHash": "6rmgU4q3/WOpOPcncI0YW0Q/QpcQtwR2TTEXDR5+4TfSimPBAk6Z/BgKLeGgp1SOun0ROVUCCafXhRLwsHaPpA=="
"resolved": "18.10.0",
"contentHash": "aXA7DDbnt6MYu3eLBEDksNdl09QxJ/PpMzrFcSxqEOk3RvRGJzhKI5mezVCEOcvveRhjfw/mvn6xVXC/rTaOog=="
},
"Microsoft.TestPlatform.TestHost": {
"type": "Transitive",
"resolved": "18.7.0",
"contentHash": "kYwfmebCs8992zaxEDkvG7S+YEouTeKfYVKUFEkwh1W2dIoOaevBt80XSKVXCUFEhusjOIm1sFfHBnoJgygrRA==",
"resolved": "18.10.0",
"contentHash": "Spu6C7bgz1PmWN+Y7wS0PfGCw9Kwv5RvJbCwyQaEWjcwpMire+qYMrcZ7qk8agjy6nd0sdHLc3ugCSrGuSbZdw==",
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "18.7.0",
"Newtonsoft.Json": "13.0.3"
"Microsoft.TestPlatform.ObjectModel": "18.10.0"
}
},
"MSTest.Analyzers": {
"type": "Transitive",
"resolved": "4.2.3",
"contentHash": "dxOZt8/LWuiox7rugInJoIa5Mmu3pBmXdfaoZOx/mxx8+sUFFpjBXPlWXQXGeWzpkVPNC3x1Jf7rt2h2Zjyvvg=="
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
"resolved": "4.4.0",
"contentHash": "8g2KL2LjsXXVywvQb0uWQBANOONVnRf6cTSbON9ar6gIw08oSBY7Z3PJlC8sHbqOVvCr7srrcWiKzb12SUoV2w=="
},
"SkiaSharp": {
"type": "Transitive",
Expand Down Expand Up @@ -197,8 +182,8 @@
"DocX": "[5.2.0, )",
"ExcelDataReader": "[3.9.0, )",
"ExcelDataReader.DataSet": "[3.9.0, )",
"Microsoft.Extensions.Configuration.Abstractions": "[10.0.9, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.9, )"
"Microsoft.Extensions.Configuration.Abstractions": "[10.0.12, )",
"Microsoft.Extensions.Logging.Abstractions": "[10.0.12, )"
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/RfcBuddy.App/RfcBuddy.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
<PackageReference Include="DocX" Version="5.2.0" />
<PackageReference Include="ExcelDataReader" Version="3.9.0" />
<PackageReference Include="ExcelDataReader.DataSet" Version="3.9.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.12" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.12" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/RfcBuddy.App/Services/ExcelService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public class ExcelService(IAppSettingsService appSettingsService) : IRfcService
private const int colRisk = 7;

private const string excelFileName = "ServiceNow-365-Day-Changes.xlsx";
private static readonly TimeSpan sourceRequestTimeout = TimeSpan.FromSeconds(30);

private string ExcelFile => Path.Join(_appSettings.DataFolder, excelFileName);

Expand All @@ -87,6 +88,7 @@ public async Task GetLatestChanges()
handler.Credentials = new NetworkCredential(_appSettings.SourceUser, _appSettings.SourcePassword);
}
using HttpClient client = new(handler);
client.Timeout = sourceRequestTimeout;
using var responseStream = await client.GetStreamAsync(_appSettings.SourceUrl).ConfigureAwait(true);
using var fileStream = new FileStream(ExcelFile, FileMode.Create);
await responseStream.CopyToAsync(fileStream).ConfigureAwait(true);
Expand Down
6 changes: 6 additions & 0 deletions src/RfcBuddy.App/Services/UserRegistryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ public bool SetAdmin(string targetUserId, bool isAdmin, string requestingAdminUs
try
{
List<UserRecord> users = LoadStore();
if (string.IsNullOrWhiteSpace(requestingAdminUserId)
|| !users.Any(x => x.IsAdmin && string.Equals(x.UserId, requestingAdminUserId, StringComparison.OrdinalIgnoreCase)))
{
return false;
}

UserRecord? target = users.FirstOrDefault(x => string.Equals(x.UserId, targetUserId, StringComparison.OrdinalIgnoreCase));
if (target is null)
{
Expand Down
Loading
Loading