diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index 8cc894215..9c1a467ad 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -2,9 +2,9 @@ "schemaVersion": 1, "repository": "masarray/ARIEC61850", "ref": "main", - "commit": "51c2eefd61d6bcbe1116fda6b781b74bb9cf06da", - "sourcePullRequest": 173, - "purpose": "M2.17-K isolated C264 acceptance trial: ARIEC typed ST enumerated Mod/Beh/Health/PhyHealth Report FCD projection with exact ST/quality/timestamp checks, preserving P10.9V control and fail-closed unknowns. ARIEC #173 Windows engine CI passed; integrated consumer CI/physical C264 replay pending. Do not promote or merge into main without acceptance.", + "commit": "1ed776a689c8707be5db09a6318c4a7613229f95", + "sourcePullRequest": 175, + "purpose": "P0-A/B isolated global interoperability identity + exact-target adaptive RCB planner trial. Engine PR #175 Windows CI 39216835641 passed 1145/1145 at immutable SHA. Full ARSAS integrated Windows CI and physical Siemens/BCUGE verification pending. Keep draft; do not merge or promote without end-to-end acceptance.", "previousTrialPin": { "commit": "4467124775d8d9d76f3db194f9fbfd97144767a8", "sourcePullRequest": 134, @@ -270,5 +270,22 @@ "physicalQualificationRequired": true, "mainlinePromotionForbidden": true, "cyclicMmsProcessPollingForbidden": true + }, + "p0GlobalInteroperabilityTrial": { + "status": "engine-windows-ci-passed-not-physical", + "sourcePullRequest": 175, + "consumerPullRequest": 622, + "baseEngineCommit": "51c2eefd61d6bcbe1116fda6b781b74bb9cf06da", + "exactCommit": "1ed776a689c8707be5db09a6318c4a7613229f95", + "engineCiRun": 39216835641, + "verifiedEngineTests": 1145, + "identityEvidence": "synthetic-five-ld", + "rcbEvidence": "synthetic-66-300-rcb-target-scoped-resumable", + "physicalQualificationRequired": true, + "mainlinePromotionForbidden": true, + "vendorBranchingForbidden": true, + "readOnlyDiscoveryRequired": true, + "rcbOwnershipProtectionRequired": true, + "cyclicMmsProcessPollingForbidden": true } } diff --git a/scripts/test-p0-global-interoperability-trial.ps1 b/scripts/test-p0-global-interoperability-trial.ps1 new file mode 100644 index 000000000..73a283227 --- /dev/null +++ b/scripts/test-p0-global-interoperability-trial.ps1 @@ -0,0 +1,33 @@ +# Private physical acceptance remains independent of a Windows-green engine code trial. +# This checker is intentionally a strict allowlist for one immutable P0 trial SHA. +function Test-P0GlobalInteroperabilityTrial { + [CmdletBinding()] + param([Parameter(Mandatory = $true)] $Lock) + + $trial = $Lock.p0GlobalInteroperabilityTrial + $ancestor = $Lock.m217KTypedStatusReportTrial + if ($null -eq $trial -or $null -eq $ancestor) { + return $false + } + + return ( + $trial.status -eq 'engine-windows-ci-passed-not-physical' -and + [int]$trial.sourcePullRequest -eq 175 -and + [int]$trial.consumerPullRequest -eq 622 -and + $trial.exactCommit -ceq '1ed776a689c8707be5db09a6318c4a7613229f95' -and + $trial.baseEngineCommit -ceq $ancestor.exactCommit -and + $ancestor.exactCommit -ceq '51c2eefd61d6bcbe1116fda6b781b74bb9cf06da' -and + [int64]$trial.engineCiRun -eq 39216835641 -and + [int]$trial.verifiedEngineTests -eq 1145 -and + $trial.identityEvidence -eq 'synthetic-five-ld' -and + $trial.rcbEvidence -eq 'synthetic-66-300-rcb-target-scoped-resumable' -and + [bool]$trial.physicalQualificationRequired -and + [bool]$trial.mainlinePromotionForbidden -and + [bool]$trial.vendorBranchingForbidden -and + [bool]$trial.readOnlyDiscoveryRequired -and + [bool]$trial.rcbOwnershipProtectionRequired -and + [bool]$trial.cyclicMmsProcessPollingForbidden -and + $Lock.commit -ceq $trial.exactCommit -and + [int]$Lock.sourcePullRequest -eq [int]$trial.sourcePullRequest + ) +} diff --git a/tests/ARSAS.Tests/P0GlobalIdentityConsumerRegressionTests.cs b/tests/ARSAS.Tests/P0GlobalIdentityConsumerRegressionTests.cs new file mode 100644 index 000000000..97d1011bf --- /dev/null +++ b/tests/ARSAS.Tests/P0GlobalIdentityConsumerRegressionTests.cs @@ -0,0 +1,80 @@ +using AR.Iec61850.Discovery; +using ArIED61850Tester.Models; +using ArIED61850Tester.Services; +using System.Security.Cryptography; + +namespace ARSAS.Tests; + +public sealed class P0GlobalIdentityConsumerRegressionTests +{ + private static readonly string[] Domains = + [ + "SIEBCUCTRL", "SIEBCUMEAS", "SIEBCUPROT", "SIEBCUUD1", "SIEBCUUD2" + ]; + + [Fact] + public void UnverifiedLiveDomains_PreserveIdentityCandidateWithoutWireConfidence() + { + var inferred = LiveIedIdentityResolver.Resolve(Domains, "192.0.2.20"); + + Assert.Equal("SIEBCU", inferred.IedName); + Assert.Equal(LiveIedDiscoveryConfidenceLevel.Medium, inferred.Confidence); + Assert.Equal("CTRL", inferred.LogicalDeviceAliases["SIEBCUCTRL"]); + Assert.Equal(5, inferred.LogicalDeviceAliases.Count); + } + + [Fact] + public async Task TrustedPinnedScl_ProjectsExactIdentityThroughConsumerWithoutChangingRawDomains() + { + var folder = Path.Combine(Path.GetTempPath(), "arsas-p0-identity-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(folder); + var path = Path.Combine(folder, "synthetic.iid"); + try + { + var ldXml = string.Join("", new[] { "CTRL", "MEAS", "PROT", "UD1", "UD2" } + .Select(inst => $"")); + var scl = $""" + +

192.0.2.20

+ {ldXml} +
+ """; + await File.WriteAllTextAsync(path, scl); + var device = new Iec61850MonitorDevice + { + IpAddress = "192.0.2.20", + SclSourcePath = path, + SclSourceSha256 = Convert.ToHexString(SHA256.HashData(await File.ReadAllBytesAsync(path))), + SclIedName = "SIEBCU", + SclAccessPointName = "P1" + }; + var live = new LiveIedModelDiscoveryDocument + { + Host = "192.0.2.20", + IedName = "192.0.2.20", + LogicalDevices = Domains.Select(domain => new LiveIedLogicalDeviceModel + { + MmsDomain = domain, + Inst = domain + }).ToArray() + }; + + var verified = await TrustedSclIdentityAuthority.TryMatchAsync(device, live); + Assert.NotNull(verified); + Assert.Equal("SIEBCU", verified.IedName); + Assert.Equal(LiveIedDiscoveryConfidenceLevel.High, verified.Confidence); + var projection = TrustedSclIdentityAuthority.ToConsumerIdentity(verified, Domains); + Assert.Equal("SIEBCU", projection.IedName); + Assert.Equal(5, projection.LogicalDevices.Count); + Assert.Contains(projection.LogicalDevices, + ld => ld.Domain == "SIEBCUPROT" && ld.Instance == "PROT"); + + device.IpAddress = "192.0.2.21"; + Assert.Null(await TrustedSclIdentityAuthority.TryMatchAsync(device, live)); + } + finally + { + Directory.Delete(folder, recursive: true); + } + } +}