mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 05:00:07 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2109133aee | |||
| 7ac7a5e8ba | |||
| 014b3e1e83 | |||
| b948d32c9a | |||
| 40d3922fe0 | |||
| 37bd4c73f5 | |||
| 9f4c1efe26 | |||
| 13a34fb7f1 | |||
| 60a41e9778 | |||
| 9aa5e46e22 |
+1
-1
Submodule .claude/commands updated: 2f276c1f9c...3b0198f8fb
@@ -4,15 +4,16 @@
|
||||
|
||||
.DESCRIPTION
|
||||
CI's member of the URL registry (Shared Constants.cs, web lib/config.ts, MCP src/config.ts):
|
||||
every own host the workflows, the .github/scripts helpers and the local release build talk to,
|
||||
plus the Authenticode timestamp server, is defined here once. Assert-HostsFromRegistry.ps1
|
||||
(Workflow gates) fails on any of these hosts spelled out elsewhere under .github/.
|
||||
every own host the workflows and the .github/scripts helpers talk to, plus the Authenticode
|
||||
timestamp server, is defined here once. Assert-HostsFromRegistry.ps1 (Workflow gates) fails
|
||||
on any of these hosts spelled out elsewhere under .github/.
|
||||
|
||||
The storage URLs derive from the account name, so blob and table can never point at two
|
||||
different accounts.
|
||||
|
||||
A file in the repository rather than a GitHub variable: the local release build reads it too,
|
||||
and bearer tokens are sent to these URLs -- where a token goes stays a reviewed line in git.
|
||||
A file in the repository rather than a GitHub variable: the helpers read it themselves, so
|
||||
they behave the same in every job and in a local run, and bearer tokens are sent to these
|
||||
URLs -- where a token goes stays a reviewed line in git.
|
||||
|
||||
.PARAMETER ExportToGitHubEnv
|
||||
Write every entry to $GITHUB_ENV instead of returning it, so the following steps of the job
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
live here so they cannot drift between call sites.
|
||||
|
||||
Identity comes from whatever `az login` established: the federated GitHub OIDC principal
|
||||
in CI (azure/login, subject repo:okieselbach/AutopilotMonitor:ref:refs/heads/main), the
|
||||
operator's own account for the local build. No secret is read, stored or passed around.
|
||||
in CI (azure/login in a job of the `production` or `release` environment), the operator's
|
||||
own account when a helper runs locally. No secret is read, stored or passed around.
|
||||
Under GitHub Actions the token is masked in the run log before it is returned.
|
||||
|
||||
x-ms-version is MANDATORY here, not decoration: Entra-authorized requests are rejected
|
||||
|
||||
@@ -258,12 +258,14 @@ jobs:
|
||||
# run here -- never a restore or build. The build output arrives as data. The runner
|
||||
# image's Windows SDK signtool is used read-only, to verify third-party signatures.
|
||||
#
|
||||
# No `environment:` yet: the code-signing credential is still bound to the ref, and a job
|
||||
# carries exactly one OIDC subject, so an environment here would break the signing login.
|
||||
# Runs in the `release` environment: both identities trust its subject, and it admits main
|
||||
# only. No federated credential trusts the ref, so a job without an environment can neither
|
||||
# sign nor write storage, whatever its permissions say.
|
||||
release:
|
||||
name: Sign and publish
|
||||
needs: [reserve, build]
|
||||
runs-on: windows-latest
|
||||
environment: release
|
||||
permissions:
|
||||
contents: write # tag + GitHub Release
|
||||
id-token: write # code-signing and storage logins, keyless Sigstore signing
|
||||
|
||||
@@ -39,11 +39,11 @@ jobs:
|
||||
# need the embedded loader to carry a signature of its own.
|
||||
#
|
||||
# Trust zone: only this repository's scripts, pinned actions and the pinned signing tool run
|
||||
# here. No `environment:` yet: the code-signing credential is still bound to the ref, and a
|
||||
# job carries exactly one OIDC subject.
|
||||
# here. The `release` environment is the only subject the code-signing identity trusts.
|
||||
sign-loader:
|
||||
name: Sign bootstrap loader
|
||||
runs-on: windows-latest
|
||||
environment: release
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # code-signing login
|
||||
@@ -198,12 +198,13 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Trust zone: signs, verifies and publishes the MSI as data; never builds. No
|
||||
# `environment:` yet, for the same reason as sign-loader.
|
||||
# Trust zone: signs, verifies and publishes the MSI as data; never builds. Environment
|
||||
# `release`, like sign-loader.
|
||||
release:
|
||||
name: Sign and publish MSI
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
environment: release
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # code-signing and storage logins, keyless Sigstore signing
|
||||
|
||||
@@ -54,6 +54,8 @@ jobs:
|
||||
name: Publish to the download alias
|
||||
needs: gates
|
||||
runs-on: windows-latest
|
||||
# Both identities trust only the `release` environment, which admits main only.
|
||||
environment: release
|
||||
# Job-scoped: only the publish job federates to Azure, the gates job never holds an
|
||||
# OIDC token it does not need.
|
||||
permissions:
|
||||
@@ -74,9 +76,7 @@ jobs:
|
||||
}
|
||||
|
||||
# First, and only to fetch the Key Vault token: azure/login sets the default az
|
||||
# account and the storage login below has to be the one that stays. See D-223 --
|
||||
# both federated credentials are bound to the ref, so this job must never carry a
|
||||
# GitHub `environment:`.
|
||||
# account and the storage login below has to be the one that stays.
|
||||
- name: Login to Azure (code-signing identity)
|
||||
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0
|
||||
with:
|
||||
|
||||
+1
-1
Submodule internal updated: 144e8f6fd8...8ae9625ce3
@@ -18,6 +18,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Harness
|
||||
public string Message { get; set; } = default!;
|
||||
public int? HttpStatusCode { get; set; }
|
||||
public string? TpmVersion { get; set; }
|
||||
public DistressDeviceContext? Context { get; set; }
|
||||
}
|
||||
|
||||
public List<Call> Calls { get; } = new List<Call>();
|
||||
@@ -33,9 +34,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Harness
|
||||
{
|
||||
}
|
||||
|
||||
public override Task TrySendAsync(DistressErrorType errorType, string message, int? httpStatusCode = null, string? tpmVersion = null)
|
||||
public override Task TrySendAsync(DistressErrorType errorType, string message, int? httpStatusCode = null, DistressDetail? detail = null)
|
||||
{
|
||||
Calls.Add(new Call { ErrorType = errorType, Message = message, HttpStatusCode = httpStatusCode, TpmVersion = tpmVersion });
|
||||
Calls.Add(new Call { ErrorType = errorType, Message = message, HttpStatusCode = httpStatusCode, TpmVersion = detail?.TpmVersion, Context = detail?.Context });
|
||||
return Send?.Invoke() ?? Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Collections.Generic;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring
|
||||
{
|
||||
/// <summary>
|
||||
/// The ESP phase a report names, derived from the three ESP registry values. Both value shapes
|
||||
/// Windows writes are covered: flat status texts (older builds) and nested subcategory objects.
|
||||
/// </summary>
|
||||
public sealed class EspPhaseSnapshotTests
|
||||
{
|
||||
private const string DoneFlag = "{\"categorySucceeded\":true}";
|
||||
private const string FailedFlag = "{\"categorySucceeded\":false}";
|
||||
private const string NestedRunning =
|
||||
"{\"SecurityPoliciesSubcategory\":{\"subcategoryState\":\"succeeded\",\"subcategoryStatusText\":\"Complete\"}," +
|
||||
"\"AppsSubcategory\":{\"subcategoryState\":\"inProgress\",\"subcategoryStatusText\":\"Installing (1 of 3)\"}}";
|
||||
private const string NestedAllDoneNoFlag =
|
||||
"{\"SecurityPoliciesSubcategory\":{\"subcategoryState\":\"succeeded\"},\"AppsSubcategory\":{\"subcategoryState\":\"notRequired\"}}";
|
||||
private const string NestedFailedNoFlag =
|
||||
"{\"SecurityPoliciesSubcategory\":{\"subcategoryState\":\"succeeded\"},\"AppsSubcategory\":{\"subcategoryState\":\"failed\"}}";
|
||||
private const string FlatRunning = "{\"CertificatesSubcategory\":\"Certificates (0 of 1 applied)\",\"AppsSubcategory\":\"Identifying\"}";
|
||||
|
||||
private static string Derive(string? preparation, string? deviceSetup, string? accountSetup)
|
||||
=> EspPhaseSnapshot.Derive(new List<string> { preparation!, deviceSetup!, accountSetup! });
|
||||
|
||||
[Fact]
|
||||
public void No_value_is_none()
|
||||
{
|
||||
Assert.Equal("none", Derive(null, null, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Running_category_names_its_phase()
|
||||
{
|
||||
Assert.Equal("device_preparation", Derive(NestedRunning, null, null));
|
||||
Assert.Equal("device_setup", Derive(DoneFlag, NestedRunning, null));
|
||||
Assert.Equal("account_setup", Derive(DoneFlag, DoneFlag, NestedRunning));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Flat_status_texts_are_read_too()
|
||||
{
|
||||
Assert.Equal("device_setup", Derive(DoneFlag, FlatRunning, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Done_categories_name_the_last_one_reached()
|
||||
{
|
||||
Assert.Equal("device_preparation_done", Derive(DoneFlag, null, null));
|
||||
Assert.Equal("device_setup_done", Derive(DoneFlag, DoneFlag, null));
|
||||
Assert.Equal("esp_done", Derive(DoneFlag, DoneFlag, DoneFlag));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void All_subcategories_done_counts_as_done_without_the_flag()
|
||||
{
|
||||
// Some builds never set categorySucceeded although every subcategory finished.
|
||||
Assert.Equal("esp_done", Derive(DoneFlag, NestedAllDoneNoFlag, NestedAllDoneNoFlag));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_failure_wins_over_everything_after_it()
|
||||
{
|
||||
Assert.Equal("device_setup_failed", Derive(DoneFlag, FailedFlag, NestedRunning));
|
||||
Assert.Equal("account_setup_failed", Derive(DoneFlag, DoneFlag, NestedFailedNoFlag));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unparseable_value_counts_as_running()
|
||||
{
|
||||
Assert.Equal("device_setup", Derive(DoneFlag, "{not json", null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_on_this_machine_returns_a_known_token()
|
||||
{
|
||||
var phase = EspPhaseSnapshot.Read();
|
||||
Assert.Matches("^[a-z_]+$", phase);
|
||||
}
|
||||
}
|
||||
}
|
||||
+64
@@ -164,6 +164,70 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
|
||||
Assert.Null(persistence.LoadRegistrationFailure());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Success_after_a_failed_start_tells_reboot_certificate_and_where_the_failure_began()
|
||||
{
|
||||
using var tmp = new TempDirectory();
|
||||
var persistence = new SessionIdPersistence(tmp.Path);
|
||||
var at = new DateTime(2026, 10, 21, 7, 0, 0, DateTimeKind.Utc);
|
||||
var failedBoot = at.AddMinutes(-16);
|
||||
|
||||
BackendSessionRegistration.RecordFailedRun(
|
||||
persistence, SessionRegistrationResult.Failed("An error occurred while sending the request."),
|
||||
at, at.AddSeconds(90), true, true, "Succeeded", "fp-a", NewLogger(tmp.Path),
|
||||
new AutopilotMonitor.Agent.V2.Core.Monitoring.Transport.DistressDeviceContext("completed", "account_setup", failedBoot),
|
||||
certThumbprint: "6B98620E8BE952FEA7CD9200B899A9A8CCADD1E5");
|
||||
|
||||
// The record carries phase and OOBE flag; boot and thumbprint stay beside it.
|
||||
var summary = persistence.LoadRegistrationFailure()!;
|
||||
Assert.Equal("account_setup", summary.EspPhase);
|
||||
Assert.Equal("completed", summary.OobeState);
|
||||
|
||||
var prior = BackendSessionRegistration.ClearAfterSuccess(
|
||||
persistence, () => "fp-a", NewLogger(tmp.Path),
|
||||
currentBootUtc: at.AddHours(1), currentCertThumbprint: "6b98620e8be952fea7cd9200b899a9a8ccadd1e5");
|
||||
|
||||
Assert.NotNull(prior);
|
||||
Assert.Equal("account_setup", prior!.LastEspPhase);
|
||||
Assert.Equal("completed", prior.LastOobeState);
|
||||
Assert.True(prior.RebootedSince);
|
||||
Assert.False(prior.CertChanged);
|
||||
Assert.False(prior.NetworkChanged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Same_boot_and_unknown_sides_are_reported_as_such()
|
||||
{
|
||||
var boot = new DateTime(2026, 10, 21, 6, 40, 0, DateTimeKind.Utc);
|
||||
Assert.False(BackendSessionRegistration.RebootedSince(boot, boot.AddSeconds(30)));
|
||||
Assert.True(BackendSessionRegistration.RebootedSince(boot, boot.AddMinutes(5)));
|
||||
Assert.Null(BackendSessionRegistration.RebootedSince(null, boot));
|
||||
Assert.Null(BackendSessionRegistration.RebootedSince(boot, null));
|
||||
Assert.True(BackendSessionRegistration.CertificateChanged("AA", "BB"));
|
||||
Assert.Null(BackendSessionRegistration.CertificateChanged(null, "BB"));
|
||||
Assert.Null(BackendSessionRegistration.CertificateChanged("AA", ""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Record_from_an_older_agent_leaves_the_new_comparisons_unknown()
|
||||
{
|
||||
using var tmp = new TempDirectory();
|
||||
var persistence = new SessionIdPersistence(tmp.Path);
|
||||
var at = new DateTime(2026, 10, 21, 7, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
// Written without context, boot or thumbprint — the shape agents up to 2.0.1487 leave.
|
||||
BackendSessionRegistration.RecordFailedRun(
|
||||
persistence, SessionRegistrationResult.Failed("x"), at, at.AddSeconds(5), true, true, null, null, NewLogger(tmp.Path));
|
||||
|
||||
var prior = BackendSessionRegistration.ClearAfterSuccess(
|
||||
persistence, () => null, NewLogger(tmp.Path), at.AddHours(1), "AA");
|
||||
|
||||
Assert.NotNull(prior);
|
||||
Assert.Null(prior!.LastEspPhase);
|
||||
Assert.Null(prior.RebootedSince);
|
||||
Assert.Null(prior.CertChanged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Recording_without_persistence_is_a_no_op()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Transport;
|
||||
using AutopilotMonitor.Agent.V2.Core.Security;
|
||||
using AutopilotMonitor.Agent.V2.Core.Tests.Harness;
|
||||
using AutopilotMonitor.Agent.V2.Runtime;
|
||||
@@ -35,8 +36,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
|
||||
new WebException("Could not create SSL/TLS secure channel.",
|
||||
new Win32Exception(unchecked((int)0x80090326)), WebExceptionStatus.SecureChannelFailure, null);
|
||||
|
||||
private static Func<Task<(string TpmVersion, string SchannelEvents)>> Context(string tpm, string schannel) =>
|
||||
() => Task.FromResult((tpm, schannel));
|
||||
private static Func<Task<(string TpmVersion, string SchannelEvents, string TlsProbe)>> Context(
|
||||
string tpm, string schannel, string tlsProbe = null!) =>
|
||||
() => Task.FromResult((tpm, schannel, tlsProbe));
|
||||
|
||||
[Fact]
|
||||
public async Task Broken_tpm_report_completes_only_after_the_send()
|
||||
@@ -46,14 +48,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
|
||||
var reporter = new FakeDistressReporter { Send = () => sendGate.Task };
|
||||
|
||||
var report = BackendSessionRegistration.ReportProbeResultAsync(
|
||||
Probe(), TlsFailure(), reporter, NewLogger(tmp), Context("IFX 7.63.3353.0", "36888:40/1205"));
|
||||
Probe(), TlsFailure(), reporter, NewLogger(tmp), Context("IFX 7.63.3353.0", "36888:40/1205", "tls12,-,cc403"));
|
||||
|
||||
var call = Assert.Single(reporter.Calls);
|
||||
Assert.False(report.IsCompleted);
|
||||
Assert.Equal(DistressErrorType.TpmPssUnsupported, call.ErrorType);
|
||||
Assert.Equal("IFX 7.63.3353.0", call.TpmVersion);
|
||||
Assert.Equal(
|
||||
"pkcs1=ok pss256=fail(0x80290423) pss384=fail(0x80090029) tls=0x80090326 sch=36888:40/1205 prov=PCP key=2048",
|
||||
"pkcs1=ok pss256=fail(0x80290423) pss384=fail(0x80090029) tls=0x80090326 tlsp=tls12,-,cc403 sch=36888:40/1205 prov=PCP key=2048",
|
||||
call.Message);
|
||||
|
||||
sendGate.SetResult(true);
|
||||
@@ -72,7 +74,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
|
||||
|
||||
var call = Assert.Single(reporter.Calls);
|
||||
Assert.Null(call.TpmVersion);
|
||||
Assert.DoesNotContain("sch=", call.Message);
|
||||
Assert.Contains("sch=unreadable", call.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -115,14 +117,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
|
||||
await Task.Delay(500);
|
||||
});
|
||||
|
||||
var (tpm, schannel) = await BackendSessionRegistration.ReadDiagnosticContextAsync(
|
||||
var (tpm, schannel, tlsProbe) = await BackendSessionRegistration.ReadDiagnosticContextAsync(
|
||||
() => { fastReturned.Set(); return "IFX 7.63.3353.0"; },
|
||||
() => { slowRelease.Wait(TimeSpan.FromSeconds(30)); return "late"; },
|
||||
() => { slowRelease.Wait(TimeSpan.FromSeconds(30)); return "tls12,-,cc403"; },
|
||||
bound,
|
||||
NewLogger(tmp));
|
||||
|
||||
Assert.Equal("IFX 7.63.3353.0", tpm);
|
||||
Assert.Null(schannel);
|
||||
Assert.Equal(BackendSessionRegistration.SchannelReadTimeout, schannel);
|
||||
Assert.Equal(BackendSessionRegistration.SchannelReadTimeout, tlsProbe);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -135,14 +139,62 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
|
||||
{
|
||||
using var tmp = new TempDirectory();
|
||||
|
||||
var (tpm, schannel) = await BackendSessionRegistration.ReadDiagnosticContextAsync(
|
||||
var (tpm, schannel, tlsProbe) = await BackendSessionRegistration.ReadDiagnosticContextAsync(
|
||||
() => throw new InvalidOperationException("no WMI"),
|
||||
() => "36887:116",
|
||||
() => "tls13,-,cc403",
|
||||
Task.Delay(TimeSpan.FromSeconds(30)),
|
||||
NewLogger(tmp));
|
||||
|
||||
Assert.Null(tpm);
|
||||
Assert.Equal("36887:116", schannel);
|
||||
Assert.Equal("tls13,-,cc403", tlsProbe);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Schannel_read_that_throws_is_reported_as_unreadable_not_as_empty()
|
||||
{
|
||||
using var tmp = new TempDirectory();
|
||||
|
||||
var (_, schannel, _) = await BackendSessionRegistration.ReadDiagnosticContextAsync(
|
||||
() => "IFX 7.63.3353.0",
|
||||
() => throw new UnauthorizedAccessException("System log"),
|
||||
() => "tls12,-,cc403",
|
||||
Task.Delay(TimeSpan.FromSeconds(30)),
|
||||
NewLogger(tmp));
|
||||
|
||||
Assert.Equal(SchannelEventReader.Unreadable, schannel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tls_probe_that_throws_is_reported_as_error_not_left_out()
|
||||
{
|
||||
using var tmp = new TempDirectory();
|
||||
|
||||
var (_, _, tlsProbe) = await BackendSessionRegistration.ReadDiagnosticContextAsync(
|
||||
() => "IFX 7.63.3353.0",
|
||||
() => "none",
|
||||
() => throw new InvalidOperationException("socket"),
|
||||
Task.Delay(TimeSpan.FromSeconds(30)),
|
||||
NewLogger(tmp));
|
||||
|
||||
Assert.Equal("error", tlsProbe);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Report_carries_the_context_the_run_started_in()
|
||||
{
|
||||
using var tmp = new TempDirectory();
|
||||
var reporter = new FakeDistressReporter();
|
||||
var startContext = new DistressDeviceContext("in_progress", "device_setup",
|
||||
new DateTime(2026, 10, 21, 6, 40, 0, DateTimeKind.Utc));
|
||||
|
||||
await BackendSessionRegistration.ReportProbeResultAsync(
|
||||
Probe(), TlsFailure(), reporter, NewLogger(tmp), Context("IFX 7.63.3353.0", "none"), startContext);
|
||||
|
||||
var call = Assert.Single(reporter.Calls);
|
||||
Assert.Same(startContext, call.Context);
|
||||
Assert.Contains("sch=none", call.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-3
@@ -43,10 +43,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Security
|
||||
[Fact]
|
||||
public void Query_is_accepted_by_the_event_log_service()
|
||||
{
|
||||
// A malformed XPath would only surface as "no events" through ReadSince (it never
|
||||
// A malformed XPath would only surface as "unreadable" through ReadBetween (it never
|
||||
// throws), so the query itself is run here.
|
||||
using (var reader = new EventLogReader(new EventLogQuery(
|
||||
"System", PathType.LogName, SchannelEventReader.BuildXPath(DateTime.UtcNow.AddDays(-1))) { ReverseDirection = true }))
|
||||
"System", PathType.LogName, SchannelEventReader.BuildXPath(DateTime.UtcNow.AddDays(-1), DateTime.UtcNow)) { ReverseDirection = true }))
|
||||
{
|
||||
using (reader.ReadEvent()) { }
|
||||
}
|
||||
@@ -55,7 +55,20 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Security
|
||||
[Fact]
|
||||
public void No_events_after_now()
|
||||
{
|
||||
Assert.Null(SchannelEventReader.ReadSince(DateTime.UtcNow.AddHours(1)));
|
||||
Assert.Equal(SchannelEventReader.NoEvents,
|
||||
SchannelEventReader.ReadBetween(DateTime.UtcNow.AddHours(1), DateTime.UtcNow.AddHours(2)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Window_ends_where_the_diagnostics_begin()
|
||||
{
|
||||
var xpath = SchannelEventReader.BuildXPath(
|
||||
new DateTime(2026, 10, 21, 6, 40, 0, DateTimeKind.Utc),
|
||||
new DateTime(2026, 10, 21, 6, 55, 30, 250, DateTimeKind.Utc));
|
||||
|
||||
Assert.Contains(
|
||||
"TimeCreated[@SystemTime >= '2026-10-21T06:40:00.000Z' and @SystemTime <= '2026-10-21T06:55:30.250Z']",
|
||||
xpath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using AutopilotMonitor.Agent.V2.Core.Security;
|
||||
using AutopilotMonitor.Agent.V2.Core.Tests.Transport;
|
||||
using AutopilotMonitor.Agent.V2.Core.Transport;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Tests.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// The TLS probe's request and the reading of its answer: only an echo of the probe's own
|
||||
/// correlation id counts as the backend, the platform's "Client Certificate Required" page says
|
||||
/// the certificate did not arrive, and a device value can never split the request.
|
||||
/// </summary>
|
||||
public sealed class TlsHandshakeProbeTests
|
||||
{
|
||||
private const string SentId = "0123456789abcdef0123456789abcdef";
|
||||
|
||||
private static TlsProbeTarget Target(string serial = "SN-1") => new TlsProbeTarget
|
||||
{
|
||||
BaseUrl = "https://api.example.test",
|
||||
TenantId = "00000000-0000-0000-0000-000000000001",
|
||||
Manufacturer = "Contoso",
|
||||
Model = "Book 1",
|
||||
SerialNumber = serial,
|
||||
AgentVersion = "2.0.1500",
|
||||
};
|
||||
|
||||
private static byte[] Raw(string statusLine, string headers, string body) =>
|
||||
Encoding.UTF8.GetBytes(statusLine + "\r\n" + headers + "\r\n" + body);
|
||||
|
||||
[Fact]
|
||||
public void Request_asks_for_the_agent_config_with_the_agent_headers()
|
||||
{
|
||||
var request = TlsHandshakeProbe.BuildRequest(Target(), "api.example.test", SentId);
|
||||
|
||||
Assert.StartsWith(
|
||||
"GET /api/agent/config?tenantId=00000000-0000-0000-0000-000000000001 HTTP/1.1\r\nHost: api.example.test\r\n",
|
||||
request);
|
||||
Assert.Contains("\r\nX-Correlation-ID: " + SentId + "\r\n", request);
|
||||
Assert.Contains("\r\nX-Device-Manufacturer: Contoso\r\n", request);
|
||||
Assert.Contains("\r\nX-Device-Model: Book 1\r\n", request);
|
||||
Assert.Contains("\r\nX-Device-SerialNumber: SN-1\r\n", request);
|
||||
Assert.Contains("\r\nX-Agent-Version: 2.0.1500\r\n", request);
|
||||
Assert.EndsWith("\r\nConnection: close\r\n\r\n", request);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Device_value_with_a_line_break_cannot_add_a_header()
|
||||
{
|
||||
var request = TlsHandshakeProbe.BuildRequest(Target("SN\r\nX-Injected: 1"), "api.example.test", SentId);
|
||||
|
||||
Assert.DoesNotContain("\r\nX-Injected", request);
|
||||
Assert.Contains("\r\nX-Device-SerialNumber: SN??X-Injected: 1\r\n", request);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Echoed_correlation_id_means_the_backend_answered()
|
||||
{
|
||||
var raw = Raw("HTTP/1.1 401 Unauthorized", "X-Correlation-ID: " + SentId + "\r\nContent-Type: application/json\r\n", "{}");
|
||||
|
||||
Assert.Equal("b401", TlsHandshakeProbe.ClassifyAnswer(raw, SentId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Backend_success_is_read_as_backend()
|
||||
{
|
||||
var raw = Raw("HTTP/1.1 200 OK", "x-correlation-id: " + SentId + "\r\n", "{\"ok\":true}");
|
||||
|
||||
Assert.Equal("b200", TlsHandshakeProbe.ClassifyAnswer(raw, SentId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Platform_certificate_required_page_means_the_certificate_did_not_arrive()
|
||||
{
|
||||
var raw = Raw("HTTP/1.1 403 Forbidden", "Content-Type: text/html\r\n", InterceptionFixtures.CertificateRequiredPage);
|
||||
|
||||
Assert.Equal("cc403", TlsHandshakeProbe.ClassifyAnswer(raw, SentId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Certificate_required_page_with_an_echo_is_still_the_backend()
|
||||
{
|
||||
var raw = Raw("HTTP/1.1 403 Forbidden", "X-Correlation-ID: " + SentId + "\r\n", InterceptionFixtures.CertificateRequiredPage);
|
||||
|
||||
Assert.Equal("b403", TlsHandshakeProbe.ClassifyAnswer(raw, SentId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Echo_of_another_id_is_someone_else()
|
||||
{
|
||||
var raw = Raw("HTTP/1.1 403 Forbidden", "X-Correlation-ID: ffffffffffffffffffffffffffffffff\r\n", "<html>Access denied</html>");
|
||||
|
||||
Assert.Equal("x403", TlsHandshakeProbe.ClassifyAnswer(raw, SentId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stopped_app_page_is_reported_as_stopped()
|
||||
{
|
||||
var raw = Raw("HTTP/1.1 403 Forbidden", "Content-Type: text/html\r\n", InterceptionFixtures.StoppedAppPage);
|
||||
|
||||
Assert.Equal("st403", TlsHandshakeProbe.ClassifyAnswer(raw, SentId));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("garbage")]
|
||||
[InlineData("HTTP/1.1 abc\r\n\r\n")]
|
||||
public void No_parsable_answer_is_reported_as_no_response(string text)
|
||||
{
|
||||
Assert.Equal("io(noresponse)", TlsHandshakeProbe.ClassifyAnswer(Encoding.ASCII.GetBytes(text), SentId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Microsoft_issued_server_certificate_adds_nothing()
|
||||
{
|
||||
using var cert = ServerCertificateObserverTests.SelfSigned(
|
||||
"CN=Microsoft Azure RSA TLS Issuing CA 03, O=Microsoft Corporation, C=US");
|
||||
|
||||
Assert.Null(TlsHandshakeProbe.ForeignIssuer(cert));
|
||||
Assert.Null(TlsHandshakeProbe.ForeignIssuer(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inspection_issuer_is_named_in_a_compact_token()
|
||||
{
|
||||
using var cert = ServerCertificateObserverTests.SelfSigned("CN=Contoso Inspection CA (2026), O=Contoso Ltd");
|
||||
|
||||
Assert.Equal("Contoso_Inspection_CA__2026_", TlsHandshakeProbe.ForeignIssuer(cert));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Long_issuer_is_capped()
|
||||
{
|
||||
using var cert = ServerCertificateObserverTests.SelfSigned("CN=" + new string('A', 80) + ", O=Fabrikam");
|
||||
|
||||
Assert.Equal(new string('A', TlsHandshakeProbe.MaxIssuerLength), TlsHandshakeProbe.ForeignIssuer(cert));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SslProtocols.Tls12, "tls12")]
|
||||
[InlineData((SslProtocols)12288, "tls13")]
|
||||
[InlineData(SslProtocols.Tls11, "tls11")]
|
||||
[InlineData(SslProtocols.Tls, "tls10")]
|
||||
[InlineData(SslProtocols.None, "p0")]
|
||||
public void Protocol_token_names_the_negotiated_version(SslProtocols protocol, string expected)
|
||||
{
|
||||
Assert.Equal(expected, TlsHandshakeProbe.ProtocolToken(protocol));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Security
|
||||
[Fact]
|
||||
public void Message_carries_codes_handshake_and_schannel_events()
|
||||
{
|
||||
var message = BrokenPcp().ToDistressMessage("0x80090326", "36888:40/1205,36887:116");
|
||||
var message = BrokenPcp().ToDistressMessage("0x80090326", "36888:40/1205,36887:116", "tls12,-,cc403");
|
||||
|
||||
Assert.Equal(
|
||||
"pkcs1=ok pss256=fail(0x80290423) pss384=fail(0x80090029) tls=0x80090326 sch=36888:40/1205,36887:116 prov=PCP key=2048",
|
||||
"pkcs1=ok pss256=fail(0x80290423) pss384=fail(0x80090029) tls=0x80090326 tlsp=tls12,-,cc403 sch=36888:40/1205,36887:116 prov=PCP key=2048",
|
||||
message);
|
||||
}
|
||||
|
||||
|
||||
+31
-5
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Interop;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Transport;
|
||||
@@ -27,28 +28,50 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Transport
|
||||
logger: null!);
|
||||
|
||||
[Fact]
|
||||
public void Report_carries_os_build_and_setup_state_at_send_time()
|
||||
public void Without_a_context_the_report_samples_the_device_when_sent()
|
||||
{
|
||||
using (var reporter = NewReporter())
|
||||
{
|
||||
var report = reporter.BuildReport(DistressErrorType.AuthCertificateRejected, "m", 401, tpmVersion: null);
|
||||
var report = reporter.BuildReport(DistressErrorType.AuthCertificateRejected, "m", 401, detail: null);
|
||||
|
||||
var osBuild = DeviceInfoProvider.GetOsBuild();
|
||||
Assert.Equal(string.IsNullOrEmpty(osBuild) ? null : osBuild, report.OsBuild);
|
||||
Assert.Equal(OobeStateReader.Read(), report.OobeState);
|
||||
Assert.Equal(EspPhaseSnapshot.Read(), report.EspPhase);
|
||||
Assert.NotNull(report.DeviceBootUtc);
|
||||
Assert.True(report.DeviceBootUtc <= DateTime.UtcNow);
|
||||
Assert.Null(report.TpmVersion);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_given_context_wins_over_the_send_time_sample()
|
||||
{
|
||||
// A failed registration samples the device before its first attempt; the report must
|
||||
// carry that, not where the device stands minutes later.
|
||||
var boot = new DateTime(2026, 10, 21, 6, 40, 0, DateTimeKind.Utc);
|
||||
using (var reporter = NewReporter())
|
||||
{
|
||||
var report = reporter.BuildReport(DistressErrorType.TpmPssUnsupported, "m", null, new DistressDetail
|
||||
{
|
||||
Context = new DistressDeviceContext("in_progress", "device_setup", boot),
|
||||
});
|
||||
|
||||
Assert.Equal("in_progress", report.OobeState);
|
||||
Assert.Equal("device_setup", report.EspPhase);
|
||||
Assert.Equal(boot, report.DeviceBootUtc);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tpm_version_passes_through_capped_at_48()
|
||||
{
|
||||
using (var reporter = NewReporter())
|
||||
{
|
||||
Assert.Equal("IFX 7.63.3353.0",
|
||||
reporter.BuildReport(DistressErrorType.TpmPssUnsupported, "m", null, "IFX 7.63.3353.0").TpmVersion);
|
||||
reporter.BuildReport(DistressErrorType.TpmPssUnsupported, "m", null, new DistressDetail { TpmVersion = "IFX 7.63.3353.0" }).TpmVersion);
|
||||
Assert.Equal(48,
|
||||
reporter.BuildReport(DistressErrorType.TpmPssUnsupported, "m", null, new string('v', 60)).TpmVersion!.Length);
|
||||
reporter.BuildReport(DistressErrorType.TpmPssUnsupported, "m", null, new DistressDetail { TpmVersion = new string('v', 60) }).TpmVersion!.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +81,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Transport
|
||||
// Every field at the agent's own caps; the agent version as built (not cut to 32).
|
||||
using (var reporter = NewReporter(new string('A', 64), "2.0.1486+d95640c597cb90849803dd65afda19e2d7a39444"))
|
||||
{
|
||||
var report = reporter.BuildReport(DistressErrorType.ClientCertificateNotReceived, new string('m', 256), 403, new string('t', 48));
|
||||
var report = reporter.BuildReport(DistressErrorType.ClientCertificateNotReceived, new string('m', 256), 403,
|
||||
new DistressDetail { TpmVersion = new string('t', 48) });
|
||||
report.CertThumbprint = new string('F', 40);
|
||||
report.CertSubject = new string('S', 96);
|
||||
report.CertIssuer = new string('I', 96);
|
||||
@@ -66,6 +90,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Transport
|
||||
report.CertNotAfter = DateTime.UtcNow;
|
||||
report.OsBuild = "26300.97111";
|
||||
report.OobeState = "unknown_2147483647".Substring(0, 16);
|
||||
report.EspPhase = "device_preparation_failed";
|
||||
report.DeviceBootUtc = DateTime.UtcNow;
|
||||
|
||||
var json = DistressReporter.SerializeWithinCap(report);
|
||||
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
|
||||
{
|
||||
/// <summary>
|
||||
/// One-shot read of the Autopilot ESP phase from the values <see cref="ProvisioningStatusTracker"/>
|
||||
/// watches (<c>HKLM\SOFTWARE\Microsoft\Provisioning\AutopilotSettings</c>), for reports taken
|
||||
/// without the tracker — a failed registration has no session and no tracker yet.
|
||||
/// <para>
|
||||
/// <b>Contract:</b> observational only, never decision-engine input. Unlike the tracker it has
|
||||
/// no settle or fallback timers: a category whose <c>categorySucceeded</c> Windows never sets
|
||||
/// counts as done once all its subcategories read succeeded or notRequired.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class EspPhaseSnapshot
|
||||
{
|
||||
internal const string None = "none";
|
||||
internal const string Unavailable = "unavailable";
|
||||
|
||||
internal enum CategoryState { Absent, Running, Done, Failed }
|
||||
|
||||
// ESP order: device preparation, device setup, account setup.
|
||||
private static readonly (string ValueName, string Phase)[] Categories =
|
||||
{
|
||||
("DevicePreparationCategory.Status", "device_preparation"),
|
||||
("DeviceSetupCategory.Status", "device_setup"),
|
||||
("AccountSetupCategory.Status", "account_setup"),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// <c>device_preparation</c> / <c>device_setup</c> / <c>account_setup</c> while that
|
||||
/// category runs, <c><category>_failed</c>, <c>device_preparation_done</c> /
|
||||
/// <c>device_setup_done</c> when the next category has not appeared (yet, or skipped),
|
||||
/// <c>esp_done</c>, <c>none</c> without any ESP value, <c>unavailable</c> when the
|
||||
/// registry cannot be read. Never throws.
|
||||
/// </summary>
|
||||
public static string Read()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var key = Registry.LocalMachine.OpenSubKey(ProvisioningStatusTracker.ProvisioningStatusRegistryPath, writable: false))
|
||||
{
|
||||
if (key == null) return None;
|
||||
return Derive(Categories.Select(c => key.GetValue(c.ValueName) as string).ToList());
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Unavailable;
|
||||
}
|
||||
}
|
||||
|
||||
/// <param name="statusJson">The three category values in ESP order; null where absent.</param>
|
||||
internal static string Derive(IReadOnlyList<string> statusJson)
|
||||
{
|
||||
var states = Categories.Select((_, i) => Classify(i < statusJson.Count ? statusJson[i] : null)).ToList();
|
||||
|
||||
for (var i = 0; i < Categories.Length; i++)
|
||||
if (states[i] == CategoryState.Failed) return Categories[i].Phase + "_failed";
|
||||
for (var i = 0; i < Categories.Length; i++)
|
||||
if (states[i] == CategoryState.Running) return Categories[i].Phase;
|
||||
|
||||
var lastDone = states.FindLastIndex(s => s == CategoryState.Done);
|
||||
if (lastDone < 0) return None;
|
||||
return lastDone == Categories.Length - 1 ? "esp_done" : Categories[lastDone].Phase + "_done";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One category value: done (<c>categorySucceeded=true</c>, or every subcategory succeeded
|
||||
/// or notRequired), failed (<c>false</c>, or a failed subcategory), running (anything
|
||||
/// else that is present, also unparseable JSON), absent (no value).
|
||||
/// </summary>
|
||||
internal static CategoryState Classify(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return CategoryState.Absent;
|
||||
try
|
||||
{
|
||||
using (var doc = JsonDocument.Parse(json))
|
||||
{
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object) return CategoryState.Running;
|
||||
|
||||
var succeeded = ProvisioningStatusTracker.SafeGetBool(root, "categorySucceeded");
|
||||
if (succeeded == true) return CategoryState.Done;
|
||||
if (succeeded == false) return CategoryState.Failed;
|
||||
|
||||
var subcategories = ProvisioningStatusTracker.ParseSubcategories(root);
|
||||
if (subcategories.Any(s => string.Equals(s.State, "failed", StringComparison.OrdinalIgnoreCase)))
|
||||
return CategoryState.Failed;
|
||||
if (subcategories.Count > 0 && subcategories.All(s =>
|
||||
string.Equals(s.State, "succeeded", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(s.State, "notRequired", StringComparison.OrdinalIgnoreCase)))
|
||||
return CategoryState.Done;
|
||||
return CategoryState.Running;
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return CategoryState.Running;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Interop;
|
||||
using AutopilotMonitor.Agent.V2.Core.Termination;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Transport
|
||||
{
|
||||
/// <summary>
|
||||
/// Where the device stood when a failure was observed: Windows' OOBE flag, the Autopilot ESP
|
||||
/// phase and the last boot. Observational only. Sample it when the failing work starts — a
|
||||
/// failed registration retries for minutes, and the device moves on meanwhile.
|
||||
/// </summary>
|
||||
public sealed class DistressDeviceContext
|
||||
{
|
||||
public DistressDeviceContext(string oobeState, string espPhase, DateTime? deviceBootUtc)
|
||||
{
|
||||
OobeState = oobeState;
|
||||
EspPhase = espPhase;
|
||||
DeviceBootUtc = deviceBootUtc;
|
||||
}
|
||||
|
||||
/// <summary><see cref="OobeStateReader"/>: flips to <c>completed</c> at the desktop, not ESP progress.</summary>
|
||||
public string OobeState { get; }
|
||||
|
||||
/// <summary><see cref="EspPhaseSnapshot"/> token.</summary>
|
||||
public string EspPhase { get; }
|
||||
|
||||
/// <summary>Last boot (UTC) from the monotonic uptime counter; null when it could not be read.</summary>
|
||||
public DateTime? DeviceBootUtc { get; }
|
||||
|
||||
/// <summary>Samples all three now. Registry, WinRT and one kernel call; never throws.</summary>
|
||||
public static DistressDeviceContext Capture()
|
||||
{
|
||||
DateTime? boot;
|
||||
try { boot = ObservationCoverage.DeviceBootUtc(); }
|
||||
catch { boot = null; }
|
||||
return new DistressDeviceContext(OobeStateReader.Read(), EspPhaseSnapshot.Read(), boot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Optional parts of one distress report.</summary>
|
||||
public sealed class DistressDetail
|
||||
{
|
||||
/// <summary>TPM manufacturer id and firmware (TPM-PSS report only).</summary>
|
||||
public string TpmVersion { get; set; }
|
||||
|
||||
/// <summary>The device context of the failure; null samples it when the report is sent.</summary>
|
||||
public DistressDeviceContext Context { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Interop;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo;
|
||||
using AutopilotMonitor.Shared;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
@@ -127,14 +126,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Transport
|
||||
/// is bounded by the 5-second HTTP timeout.
|
||||
///
|
||||
/// All anti-flood checks are applied synchronously before the async HTTP call,
|
||||
/// so repeated calls are cheap when suppressed. Every report carries the OS build and the
|
||||
/// Windows setup state at send time; <paramref name="tpmVersion"/> only the TPM-PSS one.
|
||||
/// so repeated calls are cheap when suppressed. Every report carries the OS build and a
|
||||
/// <see cref="DistressDeviceContext"/> — <paramref name="detail"/>'s, sampled when the
|
||||
/// failure was observed, or one sampled at send time; the TPM version comes only with the
|
||||
/// TPM-PSS report.
|
||||
/// </summary>
|
||||
public virtual async Task TrySendAsync(
|
||||
DistressErrorType errorType,
|
||||
string message,
|
||||
int? httpStatusCode = null,
|
||||
string tpmVersion = null)
|
||||
DistressDetail detail = null)
|
||||
{
|
||||
var key = $"{errorType}:{httpStatusCode}";
|
||||
int currentReportCount;
|
||||
@@ -169,7 +170,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Transport
|
||||
var statusText = httpStatusCode.HasValue ? $" HTTP {httpStatusCode}" : string.Empty;
|
||||
_logger?.Warning($"[DistressChannel] Sending report {currentReportCount}/{MaxReportsPerSession}: {errorType}{statusText}");
|
||||
|
||||
var report = BuildReport(errorType, message, httpStatusCode, tpmVersion);
|
||||
var report = BuildReport(errorType, message, httpStatusCode, detail);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -203,9 +204,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Transport
|
||||
return value.Length <= maxLength ? value : value.Substring(0, maxLength);
|
||||
}
|
||||
|
||||
/// <summary>The report as sent: construction-time device and cert context plus the send-time OS build and setup state.</summary>
|
||||
internal DistressReport BuildReport(DistressErrorType errorType, string message, int? httpStatusCode, string tpmVersion)
|
||||
/// <summary>
|
||||
/// The report as sent: construction-time device and cert context, the send-time OS build,
|
||||
/// and the device context of <paramref name="detail"/> (sampled now when it has none).
|
||||
/// </summary>
|
||||
internal DistressReport BuildReport(DistressErrorType errorType, string message, int? httpStatusCode, DistressDetail detail)
|
||||
{
|
||||
var context = detail?.Context ?? DistressDeviceContext.Capture();
|
||||
return new DistressReport
|
||||
{
|
||||
TenantId = _tenantId,
|
||||
@@ -225,8 +230,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Transport
|
||||
CertNotBefore = _capturedCertNotBefore,
|
||||
CertNotAfter = _capturedCertNotAfter,
|
||||
OsBuild = NullIfEmpty(DeviceInfoProvider.GetOsBuild()),
|
||||
OobeState = OobeStateReader.Read(),
|
||||
TpmVersion = Truncate(tpmVersion, 48),
|
||||
OobeState = context.OobeState,
|
||||
EspPhase = context.EspPhase,
|
||||
DeviceBootUtc = context.DeviceBootUtc,
|
||||
TpmVersion = Truncate(detail?.TpmVersion, 48),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// One-shot read of the Schannel errors the System log recorded since a point in time:
|
||||
/// One-shot read of the Schannel errors the System log recorded in a time window:
|
||||
/// 36871 (creating the client credential failed), 36887 (fatal TLS alert received) and
|
||||
/// 36888 (fatal TLS alert sent). Only the terminal registration-failure path calls it.
|
||||
/// Another process's TLS failure in the same window shows up too, so the result is a hint.
|
||||
@@ -17,17 +17,25 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
|
||||
{
|
||||
internal const int MaxEvents = 2;
|
||||
|
||||
/// <summary>The log was read and holds no matching event — a finding, unlike <see cref="Unreadable"/>.</summary>
|
||||
internal const string NoEvents = "none";
|
||||
|
||||
/// <summary>The System log could not be queried; nothing is known about Schannel.</summary>
|
||||
internal const string Unreadable = "unreadable";
|
||||
|
||||
/// <summary>
|
||||
/// Newest first, at most <see cref="MaxEvents"/> events as <see cref="FormatToken"/> tokens
|
||||
/// joined by commas (<c>36888:40/1205,36887:116</c>); null when there is none or the log
|
||||
/// cannot be read. Never throws.
|
||||
/// joined by commas (<c>36888:40/1205,36887:116</c>); <see cref="NoEvents"/> when there is
|
||||
/// none, <see cref="Unreadable"/> when the log cannot be read. <paramref name="untilUtc"/>
|
||||
/// keeps out what the diagnostics after the failure log themselves (the TLS probe's own
|
||||
/// handshake). Never throws.
|
||||
/// </summary>
|
||||
public static string ReadSince(DateTime sinceUtc, AgentLogger logger = null)
|
||||
public static string ReadBetween(DateTime sinceUtc, DateTime untilUtc, AgentLogger logger = null)
|
||||
{
|
||||
var tokens = new List<string>();
|
||||
try
|
||||
{
|
||||
var query = new EventLogQuery("System", PathType.LogName, BuildXPath(sinceUtc)) { ReverseDirection = true };
|
||||
var query = new EventLogQuery("System", PathType.LogName, BuildXPath(sinceUtc, untilUtc)) { ReverseDirection = true };
|
||||
using (var reader = new EventLogReader(query))
|
||||
{
|
||||
EventRecord record;
|
||||
@@ -43,18 +51,20 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.Debug($"SchannelEventReader: System log not readable ({ex.Message}).");
|
||||
return null;
|
||||
return Unreadable;
|
||||
}
|
||||
return tokens.Count == 0 ? null : string.Join(",", tokens);
|
||||
return tokens.Count == 0 ? NoEvents : string.Join(",", tokens);
|
||||
}
|
||||
|
||||
internal static string BuildXPath(DateTime sinceUtc)
|
||||
internal static string BuildXPath(DateTime sinceUtc, DateTime untilUtc)
|
||||
{
|
||||
var since = sinceUtc.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture);
|
||||
return "*[System[Provider[@Name='Schannel'] and (EventID=36871 or EventID=36887 or EventID=36888)" +
|
||||
$" and TimeCreated[@SystemTime >= '{since}']]]";
|
||||
$" and TimeCreated[@SystemTime >= '{XPathTime(sinceUtc)}' and @SystemTime <= '{XPathTime(untilUtc)}']]]";
|
||||
}
|
||||
|
||||
private static string XPathTime(DateTime utc) =>
|
||||
utc.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// <c>36888:40/1205</c>: the event id and its numeric properties (TLS alert code, Schannel
|
||||
/// error state). Text properties (target name, certificate subjects) are left out.
|
||||
|
||||
@@ -29,8 +29,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
|
||||
private readonly string _registrationFailurePath;
|
||||
private readonly object _lockObject = new object();
|
||||
|
||||
// Agent-local key in registration-failure.json; RegistrationFailureSummary has no such member.
|
||||
// Agent-local keys in registration-failure.json; RegistrationFailureSummary has no such members.
|
||||
private const string LocalNetworkFingerprintKey = "NetworkFingerprint";
|
||||
private const string LocalDeviceBootKey = "DeviceBootUtc";
|
||||
private const string LocalCertThumbprintKey = "CertThumbprint";
|
||||
|
||||
public SessionIdPersistence(string dataDirectory)
|
||||
{
|
||||
@@ -180,13 +182,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
|
||||
/// <see cref="RegistrationFailureSummary.InterceptedRuns"/>), the first failure time is kept,
|
||||
/// everything else describes the latest run. A run that never registers exits and leaves
|
||||
/// nothing else behind — the emergency break reads this file so the backend learns why the
|
||||
/// session never appeared. <paramref name="networkFingerprint"/> (the latest run's network)
|
||||
/// is stored beside the summary, never in it, so it cannot reach the wire. Written through to
|
||||
/// disk (L.12): the process exits right after and the next boot decides on the record.
|
||||
/// session never appeared. <paramref name="networkFingerprint"/>, <paramref name="deviceBootUtc"/>
|
||||
/// and <paramref name="certThumbprint"/> (the latest run's network, boot and client
|
||||
/// certificate) are stored beside the summary, never in it, so they cannot reach the wire —
|
||||
/// the next successful start reports only whether they changed. Written through to disk
|
||||
/// (L.12): the process exits right after and the next boot decides on the record.
|
||||
/// Returns the merged summary, null when it could not be written. Never throws.
|
||||
/// </summary>
|
||||
public RegistrationFailureSummary RecordRegistrationFailure(
|
||||
RegistrationFailureSummary latestRun, AgentLogger logger = null, string networkFingerprint = null)
|
||||
RegistrationFailureSummary latestRun, AgentLogger logger = null, string networkFingerprint = null,
|
||||
DateTime? deviceBootUtc = null, string certThumbprint = null)
|
||||
{
|
||||
if (latestRun == null) return null;
|
||||
lock (_lockObject)
|
||||
@@ -206,8 +211,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
|
||||
NetworkLinkUpAtEnd = latestRun.NetworkLinkUpAtEnd,
|
||||
AttemptWindowSeconds = latestRun.AttemptWindowSeconds,
|
||||
ConfigFetchOutcome = latestRun.ConfigFetchOutcome,
|
||||
EspPhase = latestRun.EspPhase,
|
||||
OobeState = latestRun.OobeState,
|
||||
};
|
||||
WriteRegistrationFailure(merged, networkFingerprint);
|
||||
WriteRegistrationFailure(merged, networkFingerprint, deviceBootUtc, certThumbprint);
|
||||
logger?.Info($"SessionIdPersistence: recorded failed registration run #{merged.FailedRuns} ({merged.InterceptedRuns} intercepted).");
|
||||
return merged;
|
||||
}
|
||||
@@ -286,7 +293,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
|
||||
var json = JObject.Parse(File.ReadAllText(_registrationFailurePath));
|
||||
var summary = json.ToObject<RegistrationFailureSummary>();
|
||||
return summary != null && summary.FailedRuns > 0
|
||||
? new RegistrationFailureRecord(summary, (string)json[LocalNetworkFingerprintKey])
|
||||
? new RegistrationFailureRecord(
|
||||
summary,
|
||||
(string)json[LocalNetworkFingerprintKey],
|
||||
json[LocalDeviceBootKey]?.Type == JTokenType.Date ? json[LocalDeviceBootKey].ToObject<DateTime>().ToUniversalTime() : (DateTime?)null,
|
||||
(string)json[LocalCertThumbprintKey])
|
||||
: null;
|
||||
}
|
||||
catch
|
||||
@@ -297,14 +308,19 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The summary's own properties plus one agent-local key: the fingerprint sits next to the
|
||||
/// wire type, never inside it (the summary deserializes without it).
|
||||
/// The summary's own properties plus the agent-local keys: they sit next to the wire type,
|
||||
/// never inside it (the summary deserializes without them).
|
||||
/// </summary>
|
||||
private void WriteRegistrationFailure(RegistrationFailureSummary summary, string networkFingerprint)
|
||||
private void WriteRegistrationFailure(
|
||||
RegistrationFailureSummary summary, string networkFingerprint, DateTime? deviceBootUtc, string certThumbprint)
|
||||
{
|
||||
var json = JObject.FromObject(summary);
|
||||
if (!string.IsNullOrEmpty(networkFingerprint))
|
||||
json[LocalNetworkFingerprintKey] = networkFingerprint;
|
||||
if (deviceBootUtc.HasValue)
|
||||
json[LocalDeviceBootKey] = deviceBootUtc.Value.ToUniversalTime();
|
||||
if (!string.IsNullOrEmpty(certThumbprint))
|
||||
json[LocalCertThumbprintKey] = certThumbprint;
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(json.ToString(Formatting.None));
|
||||
using (var stream = new FileStream(
|
||||
@@ -357,20 +373,31 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
|
||||
|
||||
/// <summary>
|
||||
/// <c>registration-failure.json</c> as the agent keeps it: the <see cref="RegistrationFailureSummary"/>
|
||||
/// the emergency break may send, plus the network fingerprint of the latest failed run, which
|
||||
/// stays on the device — only the next successful start's comparison (changed or not) is reported.
|
||||
/// the emergency break may send, plus the network fingerprint, boot time and client-certificate
|
||||
/// thumbprint of the latest failed run, which stay on the device — only the next successful
|
||||
/// start's comparisons (changed or not) are reported.
|
||||
/// </summary>
|
||||
public sealed class RegistrationFailureRecord
|
||||
{
|
||||
public RegistrationFailureRecord(RegistrationFailureSummary summary, string networkFingerprint)
|
||||
public RegistrationFailureRecord(
|
||||
RegistrationFailureSummary summary, string networkFingerprint,
|
||||
DateTime? deviceBootUtc = null, string certThumbprint = null)
|
||||
{
|
||||
Summary = summary ?? throw new ArgumentNullException(nameof(summary));
|
||||
NetworkFingerprint = networkFingerprint;
|
||||
DeviceBootUtc = deviceBootUtc;
|
||||
CertThumbprint = certThumbprint;
|
||||
}
|
||||
|
||||
public RegistrationFailureSummary Summary { get; }
|
||||
|
||||
/// <summary>Fingerprint of the latest failed run's network; null when it was unknown.</summary>
|
||||
public string NetworkFingerprint { get; }
|
||||
|
||||
/// <summary>Boot time (UTC) of the latest failed run; null when it was unknown or the record predates it.</summary>
|
||||
public DateTime? DeviceBootUtc { get; }
|
||||
|
||||
/// <summary>Thumbprint of the client certificate the latest failed run used; null when none or unknown.</summary>
|
||||
public string CertThumbprint { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Transport;
|
||||
using AutopilotMonitor.Agent.V2.Core.Transport;
|
||||
using AutopilotMonitor.Shared;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Security
|
||||
{
|
||||
/// <summary>Who the TLS probe talks to and with which identity headers.</summary>
|
||||
internal sealed class TlsProbeTarget
|
||||
{
|
||||
public string BaseUrl { get; set; }
|
||||
public string TenantId { get; set; }
|
||||
public string Manufacturer { get; set; }
|
||||
public string Model { get; set; }
|
||||
public string SerialNumber { get; set; }
|
||||
public string AgentVersion { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic after a terminal <c>SecureChannelFailure</c>: one direct TLS connection to the
|
||||
/// backend on a raw <see cref="SslStream"/> with the client certificate (system-default
|
||||
/// protocols, default certificate selection, normal server validation) and one
|
||||
/// <c>GET /api/agent/config</c> with the agent's own headers — the platform asks for the
|
||||
/// certificate only once it has seen a path that needs one. The HTTP stack reports only
|
||||
/// <c>SecureChannelFailure</c>; this says which protocol was negotiated, whether the certificate
|
||||
/// went out and who answered, or the SSPI code of the failure. Failure path only, never a proxy.
|
||||
/// <para>
|
||||
/// Result token: <c>unreachable</c>, <c>hs(srvcert)</c> / <c>hs(0x…)</c> / <c>hs</c> (handshake
|
||||
/// failed), or <c><protocol>,<m|->,<answer></c> — protocol <c>tls12</c> /
|
||||
/// <c>tls13</c> / …, <c>m</c> when the session ended mutually authenticated, answer
|
||||
/// <c>b<status></c> (the backend answered: the certificate arrived), <c>cc<status></c>
|
||||
/// (the platform's "Client Certificate Required" page: it did not), <c>st<status></c>
|
||||
/// (stopped app), <c>x<status></c> (someone else), <c>io(0x…)</c> / <c>io</c> (the
|
||||
/// exchange failed, e.g. during renegotiation). A server certificate not issued by Microsoft
|
||||
/// (TLS inspection on the path) appends <c>,i=<issuer CN></c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class TlsHandshakeProbe
|
||||
{
|
||||
internal const string Unreachable = "unreachable";
|
||||
internal const int MaxResponseBytes = 16 * 1024;
|
||||
internal const int MaxIssuerLength = 32;
|
||||
|
||||
/// <summary>Runs the probe within <paramref name="timeout"/> per network step. Never throws.</summary>
|
||||
public static string Run(TlsProbeTarget target, X509Certificate2 clientCertificate, TimeSpan timeout, AgentLogger logger = null)
|
||||
{
|
||||
Uri baseUri;
|
||||
try { baseUri = new Uri(target.BaseUrl); }
|
||||
catch { return Unreachable; }
|
||||
var host = baseUri.Host;
|
||||
var port = baseUri.IsDefaultPort ? 443 : baseUri.Port;
|
||||
var timeoutMs = (int)timeout.TotalMilliseconds;
|
||||
|
||||
try
|
||||
{
|
||||
using (var tcp = new TcpClient())
|
||||
{
|
||||
var connect = tcp.ConnectAsync(host, port);
|
||||
// A connect that fails after the timeout must not surface as an unobserved
|
||||
// task exception: the agent writes crash artefacts for those.
|
||||
connect.ContinueWith(t => _ = t.Exception, TaskContinuationOptions.OnlyOnFaulted);
|
||||
try
|
||||
{
|
||||
if (!connect.Wait(timeout))
|
||||
return Unreachable;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Unreachable;
|
||||
}
|
||||
|
||||
var serverCertificateRejected = false;
|
||||
string foreignIssuer = null;
|
||||
using (var ssl = new SslStream(tcp.GetStream(), leaveInnerStreamOpen: false,
|
||||
userCertificateValidationCallback: (sender, certificate, chain, errors) =>
|
||||
{
|
||||
foreignIssuer = ForeignIssuer(certificate);
|
||||
serverCertificateRejected = errors != SslPolicyErrors.None;
|
||||
return !serverCertificateRejected;
|
||||
}))
|
||||
{
|
||||
ssl.ReadTimeout = timeoutMs;
|
||||
ssl.WriteTimeout = timeoutMs;
|
||||
var certificates = new X509CertificateCollection();
|
||||
if (clientCertificate != null) certificates.Add(clientCertificate);
|
||||
|
||||
try
|
||||
{
|
||||
ssl.AuthenticateAsClient(host, certificates, SslProtocols.None, checkCertificateRevocation: false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.Debug($"TlsHandshakeProbe: handshake failed: {ex.GetBaseException().Message}");
|
||||
return (serverCertificateRejected ? "hs(srvcert)" : "hs" + CodeSuffix(ex)) + IssuerSuffix(foreignIssuer);
|
||||
}
|
||||
|
||||
var protocol = ProtocolToken(ssl.SslProtocol);
|
||||
string answer;
|
||||
try
|
||||
{
|
||||
var correlationId = Guid.NewGuid().ToString("N");
|
||||
var request = Encoding.ASCII.GetBytes(BuildRequest(target, host, correlationId));
|
||||
ssl.Write(request, 0, request.Length);
|
||||
ssl.Flush();
|
||||
answer = ClassifyAnswer(ReadResponse(ssl), correlationId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.Debug($"TlsHandshakeProbe: exchange failed: {ex.GetBaseException().Message}");
|
||||
answer = "io" + CodeSuffix(ex);
|
||||
}
|
||||
|
||||
bool mutual;
|
||||
try { mutual = ssl.IsMutuallyAuthenticated; }
|
||||
catch { mutual = false; }
|
||||
return $"{protocol},{(mutual ? "m" : "-")},{answer}{IssuerSuffix(foreignIssuer)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.Debug($"TlsHandshakeProbe: failed: {ex.Message}");
|
||||
return Unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
internal static string BuildRequest(TlsProbeTarget target, string host, string correlationId)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("GET ").Append(Constants.ApiEndpoints.GetAgentConfig)
|
||||
.Append("?tenantId=").Append(Uri.EscapeDataString(target.TenantId ?? string.Empty)).Append(" HTTP/1.1\r\n");
|
||||
sb.Append("Host: ").Append(host).Append("\r\n");
|
||||
sb.Append("User-Agent: AutopilotMonitor.Agent/").Append(HeaderValue(target.AgentVersion)).Append(" tls-probe\r\n");
|
||||
sb.Append(ResponseOriginClassifier.CorrelationIdHeader).Append(": ").Append(correlationId).Append("\r\n");
|
||||
AppendHeader(sb, "X-Device-Manufacturer", target.Manufacturer);
|
||||
AppendHeader(sb, "X-Device-Model", target.Model);
|
||||
AppendHeader(sb, "X-Device-SerialNumber", target.SerialNumber);
|
||||
AppendHeader(sb, "X-Agent-Version", target.AgentVersion);
|
||||
sb.Append("Connection: close\r\n\r\n");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads until the server closes or <see cref="MaxResponseBytes"/> arrived; a read that
|
||||
/// times out after the answer began keeps what came (a server that ignores
|
||||
/// <c>Connection: close</c>).
|
||||
/// </summary>
|
||||
private static byte[] ReadResponse(Stream stream)
|
||||
{
|
||||
var buffer = new byte[4096];
|
||||
using (var collected = new MemoryStream())
|
||||
{
|
||||
while (collected.Length < MaxResponseBytes)
|
||||
{
|
||||
int read;
|
||||
try
|
||||
{
|
||||
read = stream.Read(buffer, 0, buffer.Length);
|
||||
}
|
||||
catch (IOException) when (collected.Length > 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (read <= 0) break;
|
||||
collected.Write(buffer, 0, read);
|
||||
}
|
||||
return collected.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The answer token for a raw HTTP response, through the shared <see cref="ResponseOriginClassifier"/>:
|
||||
/// an echo of <paramref name="correlationId"/> means the backend wrote it.
|
||||
/// </summary>
|
||||
internal static string ClassifyAnswer(byte[] raw, string correlationId)
|
||||
{
|
||||
if (!TryParse(raw, out var status, out var headers, out var body))
|
||||
return "io(noresponse)";
|
||||
|
||||
using (var response = new HttpResponseMessage((HttpStatusCode)status))
|
||||
{
|
||||
foreach (var header in headers)
|
||||
response.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
response.Content = new StringContent(body ?? string.Empty);
|
||||
|
||||
var classification = ResponseOriginClassifier.Classify(response, correlationId, body);
|
||||
switch (classification.Origin)
|
||||
{
|
||||
case ResponseOrigin.Backend:
|
||||
return "b" + status;
|
||||
case ResponseOrigin.PlatformStopped:
|
||||
return "st" + status;
|
||||
default:
|
||||
return classification.ForeignReason == ResponseClassification.ClientCertificateRequired
|
||||
? "cc" + status
|
||||
: "x" + status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryParse(byte[] raw, out int status, out List<KeyValuePair<string, string>> headers, out string body)
|
||||
{
|
||||
status = 0;
|
||||
headers = new List<KeyValuePair<string, string>>();
|
||||
body = null;
|
||||
if (raw == null || raw.Length == 0) return false;
|
||||
|
||||
var text = Encoding.UTF8.GetString(raw);
|
||||
var headEnd = text.IndexOf("\r\n\r\n", StringComparison.Ordinal);
|
||||
var head = headEnd >= 0 ? text.Substring(0, headEnd) : text;
|
||||
body = headEnd >= 0 ? text.Substring(headEnd + 4) : string.Empty;
|
||||
|
||||
var lines = head.Split(new[] { "\r\n" }, StringSplitOptions.None);
|
||||
var statusParts = lines[0].Split(' ');
|
||||
if (statusParts.Length < 2 || !statusParts[0].StartsWith("HTTP/", StringComparison.Ordinal)
|
||||
|| !int.TryParse(statusParts[1], out status))
|
||||
return false;
|
||||
|
||||
for (var i = 1; i < lines.Length; i++)
|
||||
{
|
||||
var colon = lines[i].IndexOf(':');
|
||||
if (colon > 0)
|
||||
headers.Add(new KeyValuePair<string, string>(lines[i].Substring(0, colon).Trim(), lines[i].Substring(colon + 1).Trim()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static string ProtocolToken(SslProtocols protocol)
|
||||
{
|
||||
switch ((int)protocol)
|
||||
{
|
||||
case 12288: return "tls13"; // SslProtocols.Tls13
|
||||
case 3072: return "tls12";
|
||||
case 768: return "tls11";
|
||||
case 192: return "tls10";
|
||||
default: return "p" + ((int)protocol).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The issuer CN of a server certificate that Microsoft did not issue (the backend's comes
|
||||
/// from a Microsoft CA), else null: <c>[A-Za-z0-9.-]</c>, others as <c>_</c>, at most
|
||||
/// <see cref="MaxIssuerLength"/>.
|
||||
/// </summary>
|
||||
internal static string ForeignIssuer(X509Certificate certificate)
|
||||
{
|
||||
if (certificate == null) return null;
|
||||
try
|
||||
{
|
||||
if (certificate.Issuer.IndexOf("O=Microsoft Corporation", StringComparison.Ordinal) >= 0)
|
||||
return null;
|
||||
string cn;
|
||||
using (var cert2 = new X509Certificate2(certificate))
|
||||
cn = cert2.GetNameInfo(X509NameType.SimpleName, forIssuer: true);
|
||||
if (string.IsNullOrEmpty(cn)) return "unknown";
|
||||
var sb = new StringBuilder(Math.Min(cn.Length, MaxIssuerLength));
|
||||
foreach (var c in cn)
|
||||
{
|
||||
if (sb.Length == MaxIssuerLength) break;
|
||||
sb.Append((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '-' ? c : '_');
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private static string IssuerSuffix(string foreignIssuer) =>
|
||||
foreignIssuer == null ? string.Empty : ",i=" + foreignIssuer;
|
||||
|
||||
private static string CodeSuffix(Exception ex)
|
||||
{
|
||||
var code = TransportErrorCodes.InnermostNativeCode(ex);
|
||||
return code == null ? string.Empty : "(" + code + ")";
|
||||
}
|
||||
|
||||
private static void AppendHeader(StringBuilder sb, string name, string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
sb.Append(name).Append(": ").Append(HeaderValue(value)).Append("\r\n");
|
||||
}
|
||||
|
||||
/// <summary>ASCII only, no line breaks — a header value must never split the request.</summary>
|
||||
private static string HeaderValue(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||||
var sb = new StringBuilder(value.Length);
|
||||
foreach (var c in value)
|
||||
sb.Append(c >= 0x20 && c < 0x7F ? c : '?');
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,19 +190,21 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
|
||||
|
||||
/// <summary>
|
||||
/// Language-neutral summary for the distress message, codes instead of the localized
|
||||
/// error texts: <c>pkcs1=ok pss256=fail(0x80290423) pss384=fail(0x80090029)
|
||||
/// tls=0x80090326 sch=36888:40/1205 prov=PCP key=2048</c>. <paramref name="tlsCode"/>
|
||||
/// is the native code of the failed handshake (<see cref="TransportErrorCodes"/>),
|
||||
/// <paramref name="schannelEvents"/> the System-log tokens (<see cref="SchannelEventReader"/>);
|
||||
/// both are left out when null. At most <see cref="MaxDistressMessageLength"/> chars.
|
||||
/// error texts: <c>pkcs1=ok pss256=fail(0x80070423) pss384=fail(0x80090029)
|
||||
/// tlsp=tls12,-,cc403 sch=none prov=PCP key=2048</c>. <paramref name="tlsCode"/> is the
|
||||
/// native code of the failed HTTP-stack handshake (<see cref="TransportErrorCodes"/>),
|
||||
/// <paramref name="tlsProbe"/> the <see cref="TlsHandshakeProbe"/> token,
|
||||
/// <paramref name="schannelEvents"/> the System-log finding (<see cref="SchannelEventReader"/>);
|
||||
/// each is left out when null. At most <see cref="MaxDistressMessageLength"/> chars.
|
||||
/// </summary>
|
||||
public string ToDistressMessage(string tlsCode = null, string schannelEvents = null)
|
||||
public string ToDistressMessage(string tlsCode = null, string schannelEvents = null, string tlsProbe = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("pkcs1=").Append(Pkcs1Sha256Works ? "ok" : "fail");
|
||||
AppendSignOutcome(sb, " pss256=", PssSha256Works, PssSha256ErrorCode);
|
||||
AppendSignOutcome(sb, " pss384=", PssSha384Works, PssSha384ErrorCode);
|
||||
if (!string.IsNullOrEmpty(tlsCode)) sb.Append(" tls=").Append(tlsCode);
|
||||
if (!string.IsNullOrEmpty(tlsProbe)) sb.Append(" tlsp=").Append(tlsProbe);
|
||||
if (!string.IsNullOrEmpty(schannelEvents)) sb.Append(" sch=").Append(schannelEvents);
|
||||
sb.Append(" prov=").Append(ShortProviderName(ProviderName));
|
||||
if (KeySizeBits > 0) sb.Append(" key=").Append(KeySizeBits);
|
||||
|
||||
@@ -149,7 +149,8 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
distressReporter: distressReporter,
|
||||
emergencyReporter: emergencyReporter,
|
||||
authFailureTracker: authFailureTracker,
|
||||
serverCertificates: serverCertificates);
|
||||
serverCertificates: serverCertificates,
|
||||
clientCertificateThumbprint: clientCertificate?.Thumbprint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -290,6 +291,12 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
/// </summary>
|
||||
public ServerCertificateObserver ServerCertificates { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Thumbprint of the client certificate this run uses; null without one. Kept beside a
|
||||
/// failed run's record so the next successful start can tell whether the certificate changed.
|
||||
/// </summary>
|
||||
public string ClientCertificateThumbprint { get; }
|
||||
|
||||
public BackendAuthBundle(
|
||||
BackendApiClient backendApiClient,
|
||||
NetworkMetrics networkMetrics,
|
||||
@@ -300,7 +307,8 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
DistressReporter distressReporter,
|
||||
EmergencyReporter emergencyReporter,
|
||||
AuthFailureTracker authFailureTracker,
|
||||
ServerCertificateObserver serverCertificates)
|
||||
ServerCertificateObserver serverCertificates,
|
||||
string clientCertificateThumbprint = null)
|
||||
{
|
||||
BackendApiClient = backendApiClient;
|
||||
NetworkMetrics = networkMetrics;
|
||||
@@ -312,6 +320,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
EmergencyReporter = emergencyReporter;
|
||||
AuthFailureTracker = authFailureTracker;
|
||||
ServerCertificates = serverCertificates;
|
||||
ClientCertificateThumbprint = clientCertificateThumbprint;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
// the device slept or was suspended while this run tried to register.
|
||||
var startedUtc = DateTime.UtcNow;
|
||||
var linkUpAtStart = TryIsNetworkLinkUp();
|
||||
// Where the device stands before the first attempt: the retries take minutes, and a
|
||||
// failure is reported (and recorded) with this, not with where it stands at the end.
|
||||
var startContext = DistressDeviceContext.Capture();
|
||||
|
||||
var registrationResult = SessionRegistrationHelper.RegisterWithRetryAsync(
|
||||
apiClient: auth.BackendApiClient,
|
||||
@@ -68,7 +71,18 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
logger: logger,
|
||||
authFailureTracker: auth.AuthFailureTracker,
|
||||
emergencyReporter: auth.EmergencyReporter,
|
||||
onTerminalTransportFailure: ex => DiagnoseTerminalTransportFailure(ex, auth, startedUtc, logger),
|
||||
onTerminalTransportFailure: ex => DiagnoseTerminalTransportFailure(
|
||||
ex, auth, startedUtc, startContext,
|
||||
new TlsProbeTarget
|
||||
{
|
||||
BaseUrl = agentConfig.ApiBaseUrl,
|
||||
TenantId = agentConfig.TenantId,
|
||||
Manufacturer = auth.Manufacturer,
|
||||
Model = auth.Model,
|
||||
SerialNumber = auth.SerialNumber,
|
||||
AgentVersion = agentVersion,
|
||||
},
|
||||
logger),
|
||||
// Reuse the single hardened hardware read from the auth bundle so the session row
|
||||
// matches the security headers the backend validated against (no second WMI query).
|
||||
deviceHardware: (auth.Manufacturer, auth.Model, auth.SerialNumber),
|
||||
@@ -78,12 +92,15 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
if (registrationResult.Outcome == SessionRegistrationOutcome.Succeeded)
|
||||
{
|
||||
return SessionRegistrationOutcomeResult.Continue(
|
||||
registrationResult, ClearAfterSuccess(sessionPersistence, NetworkFingerprint.TryCapture, logger));
|
||||
registrationResult,
|
||||
ClearAfterSuccess(sessionPersistence, NetworkFingerprint.TryCapture, logger,
|
||||
startContext.DeviceBootUtc, auth.ClientCertificateThumbprint));
|
||||
}
|
||||
|
||||
var record = RecordFailedRun(
|
||||
sessionPersistence, registrationResult, startedUtc, DateTime.UtcNow,
|
||||
linkUpAtStart, TryIsNetworkLinkUp(), configFetchOutcome, NetworkFingerprint.TryCapture(), logger);
|
||||
linkUpAtStart, TryIsNetworkLinkUp(), configFetchOutcome, NetworkFingerprint.TryCapture(), logger,
|
||||
startContext, auth.ClientCertificateThumbprint);
|
||||
|
||||
logger.Error(
|
||||
$"=== SESSION REGISTRATION FAILED ({registrationResult.Outcome}: {registrationResult.ErrorMessage}) — " +
|
||||
@@ -106,8 +123,10 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
/// it — for a session that never registered, the only account of why) and returns the record
|
||||
/// including this run; null without persistence or when it could not be written. An
|
||||
/// intercepted run also counts toward <see cref="RegistrationFailureSummary.InterceptedRuns"/>,
|
||||
/// which only a successful registration resets. <paramref name="networkFingerprint"/> is this
|
||||
/// run's network; it is kept beside the record and never leaves the device. Never throws.
|
||||
/// which only a successful registration resets. <paramref name="startContext"/> supplies the
|
||||
/// ESP phase and OOBE flag the run started in. <paramref name="networkFingerprint"/>, the
|
||||
/// boot time and <paramref name="certThumbprint"/> are kept beside the record and never leave
|
||||
/// the device. Never throws.
|
||||
/// </summary>
|
||||
internal static RegistrationFailureSummary RecordFailedRun(
|
||||
SessionIdPersistence sessionPersistence,
|
||||
@@ -118,7 +137,9 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
bool? linkUpAtEnd,
|
||||
string configFetchOutcome,
|
||||
string networkFingerprint,
|
||||
AgentLogger logger)
|
||||
AgentLogger logger,
|
||||
DistressDeviceContext startContext = null,
|
||||
string certThumbprint = null)
|
||||
{
|
||||
if (sessionPersistence == null || registrationResult == null) return null;
|
||||
try
|
||||
@@ -139,7 +160,9 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
NetworkLinkUpAtEnd = linkUpAtEnd,
|
||||
AttemptWindowSeconds = Math.Round(Math.Max(0, (endedUtc - startedUtc).TotalSeconds), 1),
|
||||
ConfigFetchOutcome = configFetchOutcome,
|
||||
}, logger, networkFingerprint);
|
||||
EspPhase = startContext?.EspPhase,
|
||||
OobeState = startContext?.OobeState,
|
||||
}, logger, networkFingerprint, startContext?.DeviceBootUtc, certThumbprint);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -152,14 +175,17 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
/// A successful registration clears <c>registration-failure.json</c>. When earlier starts of
|
||||
/// this session had failed, the cleared record comes back for this run's <c>agent_started</c>
|
||||
/// — until now the success erased it silently and late successes were uncountable. The
|
||||
/// network fingerprints are compared here (<paramref name="captureNetworkFingerprint"/> runs
|
||||
/// only when there is a record) and never leave the device. Null when there was no record.
|
||||
/// Never throws.
|
||||
/// network fingerprints, boot times (<paramref name="currentBootUtc"/>) and client-certificate
|
||||
/// thumbprints (<paramref name="currentCertThumbprint"/>) are compared here and never leave
|
||||
/// the device (<paramref name="captureNetworkFingerprint"/> runs only when there is a record).
|
||||
/// Null when there was no record. Never throws.
|
||||
/// </summary>
|
||||
internal static PriorRegistrationFailures ClearAfterSuccess(
|
||||
SessionIdPersistence sessionPersistence,
|
||||
Func<string> captureNetworkFingerprint,
|
||||
AgentLogger logger)
|
||||
AgentLogger logger,
|
||||
DateTime? currentBootUtc = null,
|
||||
string currentCertThumbprint = null)
|
||||
{
|
||||
if (sessionPersistence == null) return null;
|
||||
try
|
||||
@@ -172,10 +198,16 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
cleared.Summary.FailedRuns,
|
||||
cleared.Summary.InterceptedRuns,
|
||||
cleared.Summary.Outcome,
|
||||
NetworkFingerprint.Differs(cleared.NetworkFingerprint, captureNetworkFingerprint?.Invoke()));
|
||||
NetworkFingerprint.Differs(cleared.NetworkFingerprint, captureNetworkFingerprint?.Invoke()),
|
||||
cleared.Summary.EspPhase,
|
||||
cleared.Summary.OobeState,
|
||||
RebootedSince(cleared.DeviceBootUtc, currentBootUtc),
|
||||
CertificateChanged(cleared.CertThumbprint, currentCertThumbprint));
|
||||
logger?.Info(
|
||||
$"Session registered after {prior.FailedRuns} failed start(s) ({prior.InterceptedRuns} intercepted, " +
|
||||
$"last: {prior.LastOutcome}, network changed: {prior.NetworkChanged?.ToString() ?? "unknown"}).");
|
||||
$"last: {prior.LastOutcome}, network changed: {prior.NetworkChanged?.ToString() ?? "unknown"}, " +
|
||||
$"last ESP phase: {prior.LastEspPhase ?? "unknown"}, rebooted since: {prior.RebootedSince?.ToString() ?? "unknown"}, " +
|
||||
$"certificate changed: {prior.CertChanged?.ToString() ?? "unknown"}).");
|
||||
return prior;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -232,6 +264,27 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
return registrationResult.Outcome == SessionRegistrationOutcome.AuthFailed ? 6 : 7;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boot times derived from the uptime counter jitter by milliseconds to seconds between two
|
||||
/// reads of the same boot (NTP corrections shift them further), so only a gap above this
|
||||
/// counts as a reboot.
|
||||
/// </summary>
|
||||
internal static readonly TimeSpan RebootDetectionTolerance = TimeSpan.FromMinutes(2);
|
||||
|
||||
/// <summary>True when the two boot times are further apart than <see cref="RebootDetectionTolerance"/>; null when either is unknown.</summary>
|
||||
internal static bool? RebootedSince(DateTime? failedRunBootUtc, DateTime? currentBootUtc)
|
||||
{
|
||||
if (!failedRunBootUtc.HasValue || !currentBootUtc.HasValue) return null;
|
||||
return (currentBootUtc.Value - failedRunBootUtc.Value).Duration() > RebootDetectionTolerance;
|
||||
}
|
||||
|
||||
/// <summary>True when the thumbprints differ (case-insensitive); null when either is unknown.</summary>
|
||||
internal static bool? CertificateChanged(string failedRunThumbprint, string currentThumbprint)
|
||||
{
|
||||
if (string.IsNullOrEmpty(failedRunThumbprint) || string.IsNullOrEmpty(currentThumbprint)) return null;
|
||||
return !string.Equals(failedRunThumbprint, currentThumbprint, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool? TryIsNetworkLinkUp()
|
||||
{
|
||||
try { return NetworkInterface.GetIsNetworkAvailable(); }
|
||||
@@ -282,8 +335,11 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>How long the TPM and System-log reads for the TPM-PSS report may take together.</summary>
|
||||
internal static readonly TimeSpan DiagnosticContextBound = TimeSpan.FromSeconds(5);
|
||||
/// <summary>How long the TPM read, the System-log read and the TLS probe for the TPM-PSS report may take together.</summary>
|
||||
internal static readonly TimeSpan DiagnosticContextBound = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>Per network step of the TLS probe (connect, handshake, request, response).</summary>
|
||||
internal static readonly TimeSpan TlsProbeStepTimeout = TimeSpan.FromSeconds(4);
|
||||
|
||||
/// <summary>
|
||||
/// Off-hot-path diagnostic invoked once after all session-registration retries fail.
|
||||
@@ -301,8 +357,12 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
Exception ex,
|
||||
BackendAuthBundle auth,
|
||||
DateTime registrationStartedUtc,
|
||||
DistressDeviceContext startContext,
|
||||
TlsProbeTarget probeTarget,
|
||||
AgentLogger logger)
|
||||
{
|
||||
// The Schannel window closes here: the TLS probe below logs its own handshake.
|
||||
var failureDetectedUtc = DateTime.UtcNow;
|
||||
try
|
||||
{
|
||||
if (!IsSecureChannelFailure(ex))
|
||||
@@ -324,8 +384,10 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
probe, ex, auth.DistressReporter, logger,
|
||||
() => ReadDiagnosticContextAsync(
|
||||
() => TpmInfoReader.Read()?.Label,
|
||||
() => SchannelEventReader.ReadSince(registrationStartedUtc, logger),
|
||||
Task.Delay(DiagnosticContextBound), logger));
|
||||
() => SchannelEventReader.ReadBetween(registrationStartedUtc, failureDetectedUtc, logger),
|
||||
() => TlsHandshakeProbe.Run(probeTarget, cert, TlsProbeStepTimeout, logger),
|
||||
Task.Delay(DiagnosticContextBound), logger),
|
||||
startContext);
|
||||
}
|
||||
catch (Exception probeEx)
|
||||
{
|
||||
@@ -337,16 +399,17 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
/// <summary>
|
||||
/// Sends <see cref="DistressErrorType.TpmPssUnsupported"/> when the probe shows a TPM-backed
|
||||
/// key that signs PKCS#1 but not PSS, with the native code of the failed handshake, the
|
||||
/// Schannel events and the TPM firmware from <paramref name="readContext"/>. The returned
|
||||
/// task completes only after the send: the run exits right after this callback, and
|
||||
/// nothing else waits for it. Never throws.
|
||||
/// Schannel finding, the TLS probe and the TPM firmware from <paramref name="readContext"/>, and the
|
||||
/// <paramref name="startContext"/> the run began in. The returned task completes only after
|
||||
/// the send: the run exits right after this callback, and nothing else waits for it. Never throws.
|
||||
/// </summary>
|
||||
internal static async Task ReportProbeResultAsync(
|
||||
TpmPssProbeResult probe,
|
||||
Exception transportFailure,
|
||||
DistressReporter reporter,
|
||||
AgentLogger logger,
|
||||
Func<Task<(string TpmVersion, string SchannelEvents)>> readContext)
|
||||
Func<Task<(string TpmVersion, string SchannelEvents, string TlsProbe)>> readContext,
|
||||
DistressDeviceContext startContext = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -363,20 +426,21 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
"Schannel can withhold the cert from TLS client-auth, blocking mTLS to the backend. " +
|
||||
"Likely fix: update TPM firmware (common on older TPM 2.0 firmware).");
|
||||
|
||||
string tpmVersion = null, schannelEvents = null;
|
||||
string tpmVersion = null, schannelEvents = SchannelEventReader.Unreadable, tlsProbe = null;
|
||||
try
|
||||
{
|
||||
(tpmVersion, schannelEvents) = await readContext().ConfigureAwait(false);
|
||||
(tpmVersion, schannelEvents, tlsProbe) = await readContext().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// The context only enriches the report; it never holds it back.
|
||||
logger?.Debug($"TPM-PSS report: context read failed ({ex.Message}) — sending without it.");
|
||||
}
|
||||
logger?.Info($"TPM-PSS report: tlsProbe={tlsProbe ?? "n/a"} schannel={schannelEvents ?? "n/a"} tpm={tpmVersion ?? "n/a"}");
|
||||
await reporter.TrySendAsync(
|
||||
DistressErrorType.TpmPssUnsupported,
|
||||
probe.ToDistressMessage(tlsCode, schannelEvents),
|
||||
tpmVersion: tpmVersion).ConfigureAwait(false);
|
||||
probe.ToDistressMessage(tlsCode, schannelEvents, tlsProbe),
|
||||
detail: new DistressDetail { TpmVersion = tpmVersion, Context = startContext }).ConfigureAwait(false);
|
||||
}
|
||||
else if (probe.IsTpmPssBroken)
|
||||
{
|
||||
@@ -392,24 +456,35 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Schannel finding or TLS-probe token when that read did not finish within the bound.</summary>
|
||||
internal const string SchannelReadTimeout = "timeout";
|
||||
|
||||
/// <summary>
|
||||
/// Runs both reads in parallel until both finished or <paramref name="bound"/> completed
|
||||
/// (production: <see cref="DiagnosticContextBound"/>); a read that threw or had not
|
||||
/// finished by then counts as missing (null). Never throws.
|
||||
/// Runs the three reads in parallel until all finished or <paramref name="bound"/> completed
|
||||
/// (production: <see cref="DiagnosticContextBound"/>). A TPM read that threw or had not
|
||||
/// finished counts as missing (null); the Schannel finding then says
|
||||
/// <see cref="SchannelEventReader.Unreadable"/> or <see cref="SchannelReadTimeout"/>, so an
|
||||
/// empty finding is never mistaken for "no events"; the TLS probe says <c>error</c> or
|
||||
/// <see cref="SchannelReadTimeout"/>. Never throws.
|
||||
/// </summary>
|
||||
internal static async Task<(string TpmVersion, string SchannelEvents)> ReadDiagnosticContextAsync(
|
||||
internal static async Task<(string TpmVersion, string SchannelEvents, string TlsProbe)> ReadDiagnosticContextAsync(
|
||||
Func<string> readTpmVersion,
|
||||
Func<string> readSchannelEvents,
|
||||
Func<string> runTlsProbe,
|
||||
Task bound,
|
||||
AgentLogger logger)
|
||||
{
|
||||
var tpm = Task.Run(readTpmVersion);
|
||||
var schannel = Task.Run(readSchannelEvents);
|
||||
await Task.WhenAny(Task.WhenAll(tpm, schannel), bound).ConfigureAwait(false);
|
||||
return (ResultOrNull(tpm, "TPM", logger), ResultOrNull(schannel, "Schannel event", logger));
|
||||
var tlsProbe = Task.Run(runTlsProbe);
|
||||
await Task.WhenAny(Task.WhenAll(tpm, schannel, tlsProbe), bound).ConfigureAwait(false);
|
||||
return (
|
||||
ResultOr(tpm, "TPM", logger, faulted: null, unfinished: null),
|
||||
ResultOr(schannel, "Schannel event", logger, faulted: SchannelEventReader.Unreadable, unfinished: SchannelReadTimeout),
|
||||
ResultOr(tlsProbe, "TLS probe", logger, faulted: "error", unfinished: SchannelReadTimeout));
|
||||
}
|
||||
|
||||
private static string ResultOrNull(Task<string> read, string what, AgentLogger logger)
|
||||
private static string ResultOr(Task<string> read, string what, AgentLogger logger, string faulted, string unfinished)
|
||||
{
|
||||
if (read.Status == TaskStatus.RanToCompletion)
|
||||
return read.Result;
|
||||
@@ -419,7 +494,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
logger?.Debug(read.IsFaulted
|
||||
? $"TPM-PSS report: {what} read failed ({read.Exception?.GetBaseException().Message})."
|
||||
: $"TPM-PSS report: {what} read still running — sending without it.");
|
||||
return null;
|
||||
return read.IsFaulted ? faulted : unfinished;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -477,19 +552,37 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
/// <summary>
|
||||
/// What the failed starts before a successful registration left in the record it cleared,
|
||||
/// reported once on that run's <c>agent_started</c> (<c>priorRegistration*</c> keys): whether a
|
||||
/// further boot rescues intercepted or failed starts becomes countable. Only the fingerprint
|
||||
/// comparison is carried, never a fingerprint.
|
||||
/// further boot rescues intercepted or failed starts becomes countable. Only the comparisons of
|
||||
/// network, boot and client certificate are carried, never a fingerprint, boot time or thumbprint.
|
||||
/// </summary>
|
||||
internal sealed class PriorRegistrationFailures
|
||||
{
|
||||
public PriorRegistrationFailures(int failedRuns, int interceptedRuns, string lastOutcome, bool? networkChanged)
|
||||
public PriorRegistrationFailures(
|
||||
int failedRuns, int interceptedRuns, string lastOutcome, bool? networkChanged,
|
||||
string lastEspPhase = null, string lastOobeState = null, bool? rebootedSince = null, bool? certChanged = null)
|
||||
{
|
||||
FailedRuns = failedRuns;
|
||||
InterceptedRuns = interceptedRuns;
|
||||
LastOutcome = lastOutcome;
|
||||
NetworkChanged = networkChanged;
|
||||
LastEspPhase = lastEspPhase;
|
||||
LastOobeState = lastOobeState;
|
||||
RebootedSince = rebootedSince;
|
||||
CertChanged = certChanged;
|
||||
}
|
||||
|
||||
/// <summary>ESP phase the latest failed start began in; null for records that predate it.</summary>
|
||||
public string LastEspPhase { get; }
|
||||
|
||||
/// <summary>Windows' OOBE flag when the latest failed start began; null for records that predate it.</summary>
|
||||
public string LastOobeState { get; }
|
||||
|
||||
/// <summary>The device booted between the latest failed start and this one; null when either boot time is unknown.</summary>
|
||||
public bool? RebootedSince { get; }
|
||||
|
||||
/// <summary>This start uses a different client certificate than the latest failed one; null when either side is unknown.</summary>
|
||||
public bool? CertChanged { get; }
|
||||
|
||||
public int FailedRuns { get; }
|
||||
|
||||
public int InterceptedRuns { get; }
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using AutopilotMonitor.Agent.V2.Core.Configuration;
|
||||
using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Interop;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime;
|
||||
using AutopilotMonitor.Agent.V2.Core.Orchestration;
|
||||
@@ -87,6 +88,9 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
// this makes late starts provable: "completed" on a first_run means OOBE
|
||||
// was already over when the agent arrived (e.g. IME queue blocked).
|
||||
{ "oobeStateAtAgentStart", OobeStateReader.Read() },
|
||||
// The ESP phase from the ESP registry — unlike the OOBE flag, which flips only
|
||||
// at the desktop, it tells device setup from account setup from done.
|
||||
{ "espPhaseAtAgentStart", EspPhaseSnapshot.Read() },
|
||||
};
|
||||
|
||||
if (recovered)
|
||||
@@ -116,6 +120,16 @@ namespace AutopilotMonitor.Agent.V2.Runtime
|
||||
data["priorRegistrationLastOutcome"] = priorRegistrationFailures.LastOutcome;
|
||||
if (priorRegistrationFailures.NetworkChanged.HasValue)
|
||||
data["priorRegistrationNetworkChanged"] = priorRegistrationFailures.NetworkChanged.Value;
|
||||
// Where the latest failed start began, and what differs now: a reboot in
|
||||
// between, another client certificate. Comparisons only, no boot time or thumbprint.
|
||||
if (!string.IsNullOrEmpty(priorRegistrationFailures.LastEspPhase))
|
||||
data["priorRegistrationLastEspPhase"] = priorRegistrationFailures.LastEspPhase;
|
||||
if (!string.IsNullOrEmpty(priorRegistrationFailures.LastOobeState))
|
||||
data["priorRegistrationLastOobeState"] = priorRegistrationFailures.LastOobeState;
|
||||
if (priorRegistrationFailures.RebootedSince.HasValue)
|
||||
data["priorRegistrationRebootedSince"] = priorRegistrationFailures.RebootedSince.Value;
|
||||
if (priorRegistrationFailures.CertChanged.HasValue)
|
||||
data["priorRegistrationCertChanged"] = priorRegistrationFailures.CertChanged.Value;
|
||||
}
|
||||
|
||||
post.Emit(new EnrollmentEvent
|
||||
|
||||
@@ -558,6 +558,35 @@ public class DistressValidationTests
|
||||
input, ReportDistressFunction.MaxOobeStateLength, ReportDistressFunction.OobeStatePattern));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("device_setup", "device_setup")]
|
||||
[InlineData("account_setup_failed", "account_setup_failed")]
|
||||
[InlineData("esp_done", "esp_done")]
|
||||
[InlineData("device_preparation_failed", "device_preparation_failed")]
|
||||
[InlineData("Device_Setup", null)]
|
||||
[InlineData("device-setup", null)]
|
||||
[InlineData("device_setup2", null)]
|
||||
[InlineData("an_esp_phase_token_far_beyond_the_cap", null)]
|
||||
[InlineData(null, null)]
|
||||
public void SanitizeToken_EspPhase(string? input, string? expected)
|
||||
{
|
||||
Assert.Equal(expected, ReportDistressFunction.SanitizeToken(
|
||||
input, ReportDistressFunction.MaxEspPhaseLength, ReportDistressFunction.EspPhasePattern));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SanitizeDeviceBoot_KeepsBootsWithinThirtyDaysBeforeTheReport()
|
||||
{
|
||||
var report = FixedUtcNow;
|
||||
Assert.Equal(report.AddMinutes(-20), ReportDistressFunction.SanitizeDeviceBoot(report.AddMinutes(-20), report));
|
||||
Assert.Equal(report.AddDays(-30), ReportDistressFunction.SanitizeDeviceBoot(report.AddDays(-30), report));
|
||||
Assert.Equal(report.AddMinutes(4), ReportDistressFunction.SanitizeDeviceBoot(report.AddMinutes(4), report));
|
||||
Assert.Null(ReportDistressFunction.SanitizeDeviceBoot(report.AddDays(-31), report));
|
||||
Assert.Null(ReportDistressFunction.SanitizeDeviceBoot(report.AddMinutes(6), report));
|
||||
Assert.Null(ReportDistressFunction.SanitizeDeviceBoot(DateTime.MinValue, report));
|
||||
Assert.Null(ReportDistressFunction.SanitizeDeviceBoot(null, report));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SanitizeToken_OsBuildExactlyAtCap_Kept()
|
||||
{
|
||||
|
||||
@@ -175,6 +175,7 @@ public class GetTpmPssUnsupportedFunctionTests
|
||||
older.TpmVersion = "IFX 7.63.3353.0";
|
||||
older.OsBuild = "26100.1000";
|
||||
older.OobeState = "in_progress";
|
||||
older.EspPhase = "device_setup";
|
||||
var newer = MakeEntry("TpmPssUnsupported", "HP", "EliteBook", "SN1", T0.AddHours(1));
|
||||
newer.OsBuild = "26200.2000";
|
||||
newer.OobeState = "completed";
|
||||
@@ -187,6 +188,7 @@ public class GetTpmPssUnsupportedFunctionTests
|
||||
Assert.Equal("IFX 7.63.3353.0", item.TpmVersion);
|
||||
Assert.Equal("26200.2000", item.OsBuild);
|
||||
Assert.Equal("completed", item.OobeState);
|
||||
Assert.Equal("device_setup", item.EspPhase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -199,6 +201,7 @@ public class GetTpmPssUnsupportedFunctionTests
|
||||
Assert.Null(item.TpmVersion);
|
||||
Assert.Null(item.OsBuild);
|
||||
Assert.Null(item.OobeState);
|
||||
Assert.Null(item.EspPhase);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
+2
@@ -34,6 +34,8 @@ public class TableDistressReportRepositoryMappingTests
|
||||
OsBuild = "26200.6899",
|
||||
OobeState = "in_progress",
|
||||
TpmVersion = "IFX 7.63.3353.0",
|
||||
EspPhase = "account_setup",
|
||||
DeviceBootUtc = T0.AddMinutes(-20),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
|
||||
+4
@@ -58,6 +58,8 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
|
||||
["OsBuild"] = Truncate(entry.OsBuild, 32),
|
||||
["OobeState"] = Truncate(entry.OobeState, 16),
|
||||
["TpmVersion"] = Truncate(entry.TpmVersion, 48),
|
||||
["EspPhase"] = Truncate(entry.EspPhase, 32),
|
||||
["DeviceBootUtc"] = entry.DeviceBootUtc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,6 +144,8 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
|
||||
OsBuild = entity.GetString("OsBuild"),
|
||||
OobeState = entity.GetString("OobeState"),
|
||||
TpmVersion = entity.GetString("TpmVersion"),
|
||||
EspPhase = entity.GetString("EspPhase"),
|
||||
DeviceBootUtc = entity.GetDateTimeOffset("DeviceBootUtc")?.UtcDateTime,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ namespace AutopilotMonitor.Functions.Functions.Ingest
|
||||
internal const int MaxOsBuildLength = 32;
|
||||
internal const int MaxOobeStateLength = 16;
|
||||
internal const int MaxTpmVersionLength = 48;
|
||||
internal const int MaxEspPhaseLength = 32;
|
||||
internal static readonly TimeSpan MaxDeviceBootAge = TimeSpan.FromDays(30);
|
||||
internal static readonly TimeSpan MaxTimestampAge = TimeSpan.FromHours(24);
|
||||
internal static readonly TimeSpan MaxTimestampFuture = TimeSpan.FromMinutes(5);
|
||||
|
||||
@@ -81,6 +83,11 @@ namespace AutopilotMonitor.Functions.Functions.Ingest
|
||||
@"^[a-z0-9_]+\z",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
// EspPhaseSnapshot tokens: device_setup, account_setup_failed, esp_done, none, ...
|
||||
internal static readonly Regex EspPhasePattern = new Regex(
|
||||
@"^[a-z_]+\z",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
public ReportDistressFunction(
|
||||
ILogger<ReportDistressFunction> logger,
|
||||
TenantConfigurationService tenantConfigService,
|
||||
@@ -208,6 +215,8 @@ namespace AutopilotMonitor.Functions.Functions.Ingest
|
||||
var osBuild = SanitizeToken(report.OsBuild, MaxOsBuildLength, OsBuildPattern);
|
||||
var oobeState = SanitizeToken(report.OobeState, MaxOobeStateLength, OobeStatePattern);
|
||||
var tpmVersion = Sanitize(report.TpmVersion, MaxTpmVersionLength);
|
||||
var espPhase = SanitizeToken(report.EspPhase, MaxEspPhaseLength, EspPhasePattern);
|
||||
var deviceBootUtc = SanitizeDeviceBoot(report.DeviceBootUtc, report.Timestamp);
|
||||
|
||||
// Persist to Table Storage
|
||||
var entry = new DistressReportEntry
|
||||
@@ -233,6 +242,8 @@ namespace AutopilotMonitor.Functions.Functions.Ingest
|
||||
OsBuild = osBuild,
|
||||
OobeState = oobeState,
|
||||
TpmVersion = tpmVersion,
|
||||
EspPhase = espPhase,
|
||||
DeviceBootUtc = deviceBootUtc,
|
||||
};
|
||||
|
||||
await _repository.SaveDistressReportAsync(tenantId, entry);
|
||||
@@ -327,9 +338,10 @@ namespace AutopilotMonitor.Functions.Functions.Ingest
|
||||
|
||||
// Structured log (Warning, not Critical — data is unverified)
|
||||
_logger.LogWarning(
|
||||
"AgentDistress [{ErrorType}] tenant={TenantId} mfr={Manufacturer} model={Model} sn={SerialNumber} cloudPc={IsCloudPc} http={HttpStatusCode} ver={AgentVersion} os={OsBuild} oobe={OobeState} tpm={TpmVersion} certState={CertSourceState} thumbprint={CertThumbprint}: {Message}",
|
||||
"AgentDistress [{ErrorType}] tenant={TenantId} mfr={Manufacturer} model={Model} sn={SerialNumber} cloudPc={IsCloudPc} http={HttpStatusCode} ver={AgentVersion} os={OsBuild} esp={EspPhase} oobe={OobeState} boot={DeviceBootUtc} tpm={TpmVersion} certState={CertSourceState} thumbprint={CertThumbprint}: {Message}",
|
||||
report.ErrorType, tenantId, manufacturer, model, serialNumber, report.IsCloudPc,
|
||||
report.HttpStatusCode, agentVersion, osBuild ?? "n/a", oobeState ?? "n/a", tpmVersion ?? "n/a",
|
||||
report.HttpStatusCode, agentVersion, osBuild ?? "n/a", espPhase ?? "n/a", oobeState ?? "n/a",
|
||||
deviceBootUtc?.ToString("O") ?? "n/a", tpmVersion ?? "n/a",
|
||||
certSourceState ?? "n/a", certThumbprint ?? "n/a", message);
|
||||
|
||||
// Custom event for KQL queries:
|
||||
@@ -356,6 +368,8 @@ namespace AutopilotMonitor.Functions.Functions.Ingest
|
||||
["OsBuild"] = osBuild ?? string.Empty,
|
||||
["OobeState"] = oobeState ?? string.Empty,
|
||||
["TpmVersion"] = tpmVersion ?? string.Empty,
|
||||
["EspPhase"] = espPhase ?? string.Empty,
|
||||
["DeviceBootUtc"] = deviceBootUtc?.ToString("O") ?? string.Empty,
|
||||
});
|
||||
|
||||
return req.CreateResponse(HttpStatusCode.OK);
|
||||
@@ -398,6 +412,18 @@ namespace AutopilotMonitor.Functions.Functions.Ingest
|
||||
return pattern.IsMatch(trimmed) ? trimmed : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The device boot time as UTC when it lies within <see cref="MaxDeviceBootAge"/> before and
|
||||
/// <see cref="MaxTimestampFuture"/> after the report time, otherwise null.
|
||||
/// </summary>
|
||||
internal static DateTime? SanitizeDeviceBoot(DateTime? value, DateTime reportTimestamp)
|
||||
{
|
||||
if (!value.HasValue) return null;
|
||||
var boot = value.Value.Kind == DateTimeKind.Utc ? value.Value : DateTime.SpecifyKind(value.Value, DateTimeKind.Utc);
|
||||
var age = reportTimestamp - boot;
|
||||
return age >= -MaxTimestampFuture && age <= MaxDeviceBootAge ? boot : null;
|
||||
}
|
||||
|
||||
/// <summary>The tenant bell for the first TPM-PSS report of a device.</summary>
|
||||
internal static string BuildTpmPssBellMessage(string serialNumber, string? manufacturer, string? model)
|
||||
{
|
||||
|
||||
+2
-1
@@ -73,7 +73,7 @@ namespace AutopilotMonitor.Functions.Functions.Reports
|
||||
Success = true,
|
||||
Aggregated = aggregated,
|
||||
TotalRawReports = totalRawReports,
|
||||
DataQualityNotice = "This data is from pre-authentication distress reports and is UNVERIFIED. Devices reported here have a TPM that cannot sign with RSA-PSS, and their agent could not connect when it sent the report; connectedAt marks a device that uploaded with its certificate afterwards. Serial number, manufacturer, model, OS build, setup state and TPM version are self-reported by devices."
|
||||
DataQualityNotice = "This data is from pre-authentication distress reports and is UNVERIFIED. Devices reported here have a TPM that cannot sign with RSA-PSS, and their agent could not connect when it sent the report; connectedAt marks a device that uploaded with its certificate afterwards. Serial number, manufacturer, model, OS build, ESP phase, Windows OOBE flag and TPM version are self-reported by devices."
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -129,6 +129,7 @@ namespace AutopilotMonitor.Functions.Functions.Reports
|
||||
TpmVersion = newestFirst.Select(r => r.TpmVersion).FirstOrDefault(v => !string.IsNullOrEmpty(v)),
|
||||
OsBuild = newestFirst.Select(r => r.OsBuild).FirstOrDefault(v => !string.IsNullOrEmpty(v)),
|
||||
OobeState = newestFirst.Select(r => r.OobeState).FirstOrDefault(v => !string.IsNullOrEmpty(v)),
|
||||
EspPhase = newestFirst.Select(r => r.EspPhase).FirstOrDefault(v => !string.IsNullOrEmpty(v)),
|
||||
ConnectedAt = connectedAt,
|
||||
};
|
||||
})
|
||||
|
||||
+86
-110
@@ -1510,9 +1510,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1530,9 +1527,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1550,9 +1544,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1570,9 +1561,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1590,9 +1578,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1610,9 +1595,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1630,9 +1612,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1650,9 +1629,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1731,13 +1707,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.146.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz",
|
||||
"integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==",
|
||||
"version": "0.154.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz",
|
||||
"integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
"url": "https://github.com/sponsors/oxc-project"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-android-arm-eabi": {
|
||||
@@ -2080,9 +2056,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm-eabi": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz",
|
||||
"integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz",
|
||||
"integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2097,9 +2073,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz",
|
||||
"integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz",
|
||||
"integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2114,9 +2090,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz",
|
||||
"integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz",
|
||||
"integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2131,9 +2107,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz",
|
||||
"integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz",
|
||||
"integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2148,9 +2124,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz",
|
||||
"integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz",
|
||||
"integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2165,9 +2141,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz",
|
||||
"integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz",
|
||||
"integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2182,9 +2158,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz",
|
||||
"integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz",
|
||||
"integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2199,9 +2175,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz",
|
||||
"integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz",
|
||||
"integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2216,9 +2192,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz",
|
||||
"integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz",
|
||||
"integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -2233,9 +2209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz",
|
||||
"integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz",
|
||||
"integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -2250,9 +2226,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz",
|
||||
"integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz",
|
||||
"integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2267,9 +2243,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz",
|
||||
"integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz",
|
||||
"integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2284,9 +2260,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz",
|
||||
"integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz",
|
||||
"integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2301,9 +2277,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz",
|
||||
"integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz",
|
||||
"integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2318,9 +2294,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz",
|
||||
"integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz",
|
||||
"integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4146,9 +4122,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.13.7",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz",
|
||||
"integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==",
|
||||
"version": "4.13.13",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.13.tgz",
|
||||
"integrity": "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -4826,9 +4802,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"version": "3.3.20",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
|
||||
"integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -5196,9 +5172,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.26",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||
"version": "8.5.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz",
|
||||
"integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -5216,9 +5192,9 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.17",
|
||||
"nanoid": "^3.3.20",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
"source-map-js": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@@ -5354,13 +5330,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz",
|
||||
"integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz",
|
||||
"integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.146.0",
|
||||
"@oxc-project/types": "=0.154.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -5370,21 +5346,21 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm-eabi": "1.2.5",
|
||||
"@rolldown/binding-android-arm64": "1.2.5",
|
||||
"@rolldown/binding-darwin-arm64": "1.2.5",
|
||||
"@rolldown/binding-darwin-x64": "1.2.5",
|
||||
"@rolldown/binding-freebsd-x64": "1.2.5",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.5",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.2.5",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.2.5",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.5",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.2.5",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.2.5",
|
||||
"@rolldown/binding-linux-x64-musl": "1.2.5",
|
||||
"@rolldown/binding-openharmony-arm64": "1.2.5",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.2.5",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.5"
|
||||
"@rolldown/binding-android-arm-eabi": "1.2.14",
|
||||
"@rolldown/binding-android-arm64": "1.2.14",
|
||||
"@rolldown/binding-darwin-arm64": "1.2.14",
|
||||
"@rolldown/binding-darwin-x64": "1.2.14",
|
||||
"@rolldown/binding-freebsd-x64": "1.2.14",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.14",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.2.14",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.2.14",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.14",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.2.14",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.2.14",
|
||||
"@rolldown/binding-linux-x64-musl": "1.2.14",
|
||||
"@rolldown/binding-openharmony-arm64": "1.2.14",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.2.14",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
@@ -5973,16 +5949,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
|
||||
"integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz",
|
||||
"integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.33.0",
|
||||
"picomatch": "^4.0.5",
|
||||
"postcss": "^8.5.26",
|
||||
"rolldown": "~1.2.4",
|
||||
"picomatch": "^4.0.7",
|
||||
"postcss": "^8.5.29",
|
||||
"rolldown": "~1.2.12",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
@@ -5999,7 +5975,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.4.0 || ^0.5.0",
|
||||
"@vitejs/devtools": "^0.7.1",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
|
||||
@@ -1825,6 +1825,8 @@ export interface DistressReportEntry {
|
||||
osBuild?: string;
|
||||
oobeState?: string;
|
||||
tpmVersion?: string;
|
||||
espPhase?: string;
|
||||
deviceBootUtc?: string;
|
||||
}
|
||||
|
||||
/** Success body of GET global/distress-reports: all pre-auth distress reports (Global Admin). */
|
||||
@@ -5729,8 +5731,10 @@ export interface TpmPssUnsupportedItem {
|
||||
tpmVersion?: string;
|
||||
/** OS build of the most recent report; null from agents that predate it. */
|
||||
osBuild?: string;
|
||||
/** Windows setup state of the most recent report (in_progress, completed, …); null from agents that predate it. */
|
||||
/** Windows' OOBE flag of the most recent report (in_progress, completed, …; not ESP progress); null from agents that predate it. */
|
||||
oobeState?: string;
|
||||
/** Autopilot ESP phase of the most recent report that carries one (device_setup, account_setup, esp_done, …); null from agents that predate it. */
|
||||
espPhase?: string;
|
||||
/** Last upload of the newest certificate-authenticated session of this serial when that upload came after LastSeen: the device connected after its last report. Null while it has not, and when the lookup failed. */
|
||||
connectedAt?: string;
|
||||
}
|
||||
|
||||
@@ -50,5 +50,7 @@ namespace AutopilotMonitor.Shared.DataAccess
|
||||
public string? OsBuild { get; set; }
|
||||
public string? OobeState { get; set; }
|
||||
public string? TpmVersion { get; set; }
|
||||
public string? EspPhase { get; set; }
|
||||
public DateTime? DeviceBootUtc { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,12 +190,29 @@ namespace AutopilotMonitor.Shared.Models
|
||||
public string? OsBuild { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Windows setup (OOBE) state when the report was sent: <c>not_started</c>,
|
||||
/// <c>in_progress</c>, <c>completed</c>, <c>unknown_<n></c> or <c>unavailable</c>.
|
||||
/// Kept server-side only as <c>[a-z0-9_]</c>, at most 16 chars. Null from older agents.
|
||||
/// Windows' own OOBE flag (<c>SystemSetupInfo.OutOfBoxExperienceState</c>: <c>not_started</c>,
|
||||
/// <c>in_progress</c>, <c>completed</c>, <c>unknown_<n></c>, <c>unavailable</c>) — it
|
||||
/// flips to <c>completed</c> at the desktop and says nothing about ESP progress (see
|
||||
/// <see cref="EspPhase"/>). Sampled with <see cref="EspPhase"/>. Kept server-side only as
|
||||
/// <c>[a-z0-9_]</c>, at most 16 chars. Null from older agents.
|
||||
/// </summary>
|
||||
public string? OobeState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Autopilot ESP phase from the ESP registry: <c>device_preparation</c>,
|
||||
/// <c>device_setup</c>, <c>account_setup</c> (running), <c><phase>_failed</c>,
|
||||
/// <c>device_setup_done</c>, <c>esp_done</c>, <c>none</c> or <c>unavailable</c>. A failed
|
||||
/// registration samples it before its first attempt, other reports when sent. Kept
|
||||
/// server-side only as <c>[a-z_]</c>, at most 32 chars. Null from older agents.
|
||||
/// </summary>
|
||||
public string? EspPhase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the device last booted (UTC), sampled with <see cref="EspPhase"/>. Kept server-side
|
||||
/// only within 30 days before and 5 minutes after <see cref="Timestamp"/>. Null from older agents.
|
||||
/// </summary>
|
||||
public DateTime? DeviceBootUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// TPM manufacturer id and firmware version (e.g. <c>IFX 7.63.3353.0</c>), sent with
|
||||
/// <see cref="DistressErrorType.TpmPssUnsupported"/> only. Sanitized, at most 48 chars.
|
||||
|
||||
@@ -47,5 +47,9 @@ namespace AutopilotMonitor.Shared.Models
|
||||
|
||||
/// <summary>Outcome of the remote-config fetch in the latest failed run (e.g. <c>Succeeded</c>, <c>UsedDefaults</c>).</summary>
|
||||
public string? ConfigFetchOutcome { get; set; }
|
||||
/// <summary>Autopilot ESP phase when the latest failed run started registering (see <c>DistressReport.EspPhase</c>).</summary>
|
||||
public string? EspPhase { get; set; }
|
||||
/// <summary>Windows' OOBE flag when the latest failed run started registering (see <c>DistressReport.OobeState</c>).</summary>
|
||||
public string? OobeState { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,9 +226,12 @@ namespace AutopilotMonitor.Shared.Models
|
||||
/// <summary>OS build of the most recent report; null from agents that predate it.</summary>
|
||||
public string? OsBuild { get; set; }
|
||||
|
||||
/// <summary>Windows setup state of the most recent report (<c>in_progress</c>, <c>completed</c>, …); null from agents that predate it.</summary>
|
||||
/// <summary>Windows' OOBE flag of the most recent report (<c>in_progress</c>, <c>completed</c>, …; not ESP progress); null from agents that predate it.</summary>
|
||||
public string? OobeState { get; set; }
|
||||
|
||||
/// <summary>Autopilot ESP phase of the most recent report that carries one (<c>device_setup</c>, <c>account_setup</c>, <c>esp_done</c>, …); null from agents that predate it.</summary>
|
||||
public string? EspPhase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last upload of the newest certificate-authenticated session of this serial when that
|
||||
/// upload came after <see cref="LastSeen"/>: the device connected after its last report.
|
||||
|
||||
@@ -346,20 +346,35 @@ export function DistressReportsSection({
|
||||
<dd className="text-gray-900 dark:text-gray-100 mt-0.5">{selectedReport.agentVersion}</dd>
|
||||
</div>
|
||||
)}
|
||||
{(selectedReport.osBuild || selectedReport.oobeState || selectedReport.tpmVersion) && (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<dt className="font-medium text-gray-500 dark:text-gray-400">OS Build</dt>
|
||||
<dd className="font-mono text-gray-900 dark:text-gray-100 mt-0.5">{selectedReport.osBuild ?? "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-gray-500 dark:text-gray-400">Setup State</dt>
|
||||
<dd className="font-mono text-gray-900 dark:text-gray-100 mt-0.5">{selectedReport.oobeState ?? "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-gray-500 dark:text-gray-400">TPM</dt>
|
||||
<dd className="font-mono text-gray-900 dark:text-gray-100 mt-0.5">{selectedReport.tpmVersion ?? "-"}</dd>
|
||||
{(selectedReport.osBuild || selectedReport.oobeState || selectedReport.tpmVersion || selectedReport.espPhase) && (
|
||||
<div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<dt className="font-medium text-gray-500 dark:text-gray-400">OS Build</dt>
|
||||
<dd className="font-mono text-gray-900 dark:text-gray-100 mt-0.5">{selectedReport.osBuild ?? "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-gray-500 dark:text-gray-400">TPM</dt>
|
||||
<dd className="font-mono text-gray-900 dark:text-gray-100 mt-0.5">{selectedReport.tpmVersion ?? "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-gray-500 dark:text-gray-400">Device Boot</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100 mt-0.5 text-xs">
|
||||
{selectedReport.deviceBootUtc ? new Date(selectedReport.deviceBootUtc).toLocaleString() : "-"}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-gray-500 dark:text-gray-400">ESP Phase</dt>
|
||||
<dd className="font-mono text-gray-900 dark:text-gray-100 mt-0.5">{selectedReport.espPhase ?? "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-gray-500 dark:text-gray-400">Windows OOBE Flag</dt>
|
||||
<dd className="font-mono text-gray-900 dark:text-gray-100 mt-0.5">{selectedReport.oobeState ?? "-"}</dd>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||
ESP phase, OOBE flag and boot are taken when the failure began (a failed registration: before its first attempt). The OOBE flag turns "completed" at the desktop; it does not track ESP progress.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedReport.isCloudPc && (
|
||||
|
||||
@@ -9035,6 +9035,22 @@
|
||||
"name": "string"
|
||||
},
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "espPhase",
|
||||
"type": {
|
||||
"kind": "primitive",
|
||||
"name": "string"
|
||||
},
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"name": "deviceBootUtc",
|
||||
"type": {
|
||||
"kind": "primitive",
|
||||
"name": "string"
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -26231,7 +26247,16 @@
|
||||
"name": "string"
|
||||
},
|
||||
"optional": true,
|
||||
"doc": "Windows setup state of the most recent report (in_progress, completed, …); null from agents that predate it."
|
||||
"doc": "Windows' OOBE flag of the most recent report (in_progress, completed, …; not ESP progress); null from agents that predate it."
|
||||
},
|
||||
{
|
||||
"name": "espPhase",
|
||||
"type": {
|
||||
"kind": "primitive",
|
||||
"name": "string"
|
||||
},
|
||||
"optional": true,
|
||||
"doc": "Autopilot ESP phase of the most recent report that carries one (device_setup, account_setup, esp_done, …); null from agents that predate it."
|
||||
},
|
||||
{
|
||||
"name": "connectedAt",
|
||||
|
||||
@@ -1825,6 +1825,8 @@ export interface DistressReportEntry {
|
||||
osBuild?: string;
|
||||
oobeState?: string;
|
||||
tpmVersion?: string;
|
||||
espPhase?: string;
|
||||
deviceBootUtc?: string;
|
||||
}
|
||||
|
||||
/** Success body of GET global/distress-reports: all pre-auth distress reports (Global Admin). */
|
||||
@@ -5729,8 +5731,10 @@ export interface TpmPssUnsupportedItem {
|
||||
tpmVersion?: string;
|
||||
/** OS build of the most recent report; null from agents that predate it. */
|
||||
osBuild?: string;
|
||||
/** Windows setup state of the most recent report (in_progress, completed, …); null from agents that predate it. */
|
||||
/** Windows' OOBE flag of the most recent report (in_progress, completed, …; not ESP progress); null from agents that predate it. */
|
||||
oobeState?: string;
|
||||
/** Autopilot ESP phase of the most recent report that carries one (device_setup, account_setup, esp_done, …); null from agents that predate it. */
|
||||
espPhase?: string;
|
||||
/** Last upload of the newest certificate-authenticated session of this serial when that upload came after LastSeen: the device connected after its last report. Null while it has not, and when the lookup failed. */
|
||||
connectedAt?: string;
|
||||
}
|
||||
|
||||
+78
-114
@@ -1428,9 +1428,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1447,9 +1444,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1466,9 +1460,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1485,9 +1476,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1696,9 +1684,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1716,9 +1701,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1736,9 +1718,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1756,9 +1735,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1776,9 +1752,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1796,9 +1769,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1816,9 +1786,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1836,9 +1803,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1917,13 +1881,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.146.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz",
|
||||
"integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==",
|
||||
"version": "0.154.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz",
|
||||
"integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
"url": "https://github.com/sponsors/oxc-project"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-android-arm-eabi": {
|
||||
@@ -2249,9 +2213,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm-eabi": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz",
|
||||
"integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz",
|
||||
"integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2266,9 +2230,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz",
|
||||
"integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz",
|
||||
"integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2283,9 +2247,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz",
|
||||
"integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz",
|
||||
"integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2300,9 +2264,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz",
|
||||
"integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz",
|
||||
"integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2317,9 +2281,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz",
|
||||
"integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz",
|
||||
"integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2334,9 +2298,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz",
|
||||
"integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz",
|
||||
"integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2351,9 +2315,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz",
|
||||
"integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz",
|
||||
"integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2368,9 +2332,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz",
|
||||
"integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz",
|
||||
"integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2385,9 +2349,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz",
|
||||
"integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz",
|
||||
"integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -2402,9 +2366,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz",
|
||||
"integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz",
|
||||
"integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -2419,9 +2383,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz",
|
||||
"integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz",
|
||||
"integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2436,9 +2400,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz",
|
||||
"integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz",
|
||||
"integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2453,9 +2417,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz",
|
||||
"integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz",
|
||||
"integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2470,9 +2434,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz",
|
||||
"integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz",
|
||||
"integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2487,9 +2451,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz",
|
||||
"integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz",
|
||||
"integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8148,13 +8112,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz",
|
||||
"integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz",
|
||||
"integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.146.0",
|
||||
"@oxc-project/types": "=0.154.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -8164,21 +8128,21 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm-eabi": "1.2.5",
|
||||
"@rolldown/binding-android-arm64": "1.2.5",
|
||||
"@rolldown/binding-darwin-arm64": "1.2.5",
|
||||
"@rolldown/binding-darwin-x64": "1.2.5",
|
||||
"@rolldown/binding-freebsd-x64": "1.2.5",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.5",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.2.5",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.2.5",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.5",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.2.5",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.2.5",
|
||||
"@rolldown/binding-linux-x64-musl": "1.2.5",
|
||||
"@rolldown/binding-openharmony-arm64": "1.2.5",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.2.5",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.5"
|
||||
"@rolldown/binding-android-arm-eabi": "1.2.14",
|
||||
"@rolldown/binding-android-arm64": "1.2.14",
|
||||
"@rolldown/binding-darwin-arm64": "1.2.14",
|
||||
"@rolldown/binding-darwin-x64": "1.2.14",
|
||||
"@rolldown/binding-freebsd-x64": "1.2.14",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.14",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.2.14",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.2.14",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.14",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.2.14",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.2.14",
|
||||
"@rolldown/binding-linux-x64-musl": "1.2.14",
|
||||
"@rolldown/binding-openharmony-arm64": "1.2.14",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.2.14",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
@@ -9393,16 +9357,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
|
||||
"integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz",
|
||||
"integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.33.0",
|
||||
"picomatch": "^4.0.5",
|
||||
"postcss": "^8.5.26",
|
||||
"rolldown": "~1.2.4",
|
||||
"picomatch": "^4.0.7",
|
||||
"postcss": "^8.5.29",
|
||||
"rolldown": "~1.2.12",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
@@ -9419,7 +9383,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.4.0 || ^0.5.0",
|
||||
"@vitejs/devtools": "^0.7.1",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
@@ -9471,9 +9435,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
||||
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
Reference in New Issue
Block a user