Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Services/TrustedSclIdentityAuthority.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public static class TrustedSclIdentityAuthority
source,
liveModel.LogicalDevices.Select(ld => ld.MmsDomain),
device.IpAddress,
device.SclIedName);
device.SclIedName,
device.SclAccessPointName);
}

/// <summary>
Expand Down Expand Up @@ -87,7 +88,8 @@ public static class TrustedSclIdentityAuthority
continue;

var identity = TrustedSclIedIdentityMatcher.TryMatch(
document, domains, device.IpAddress, candidate.IedName);
document, domains, device.IpAddress, candidate.IedName,
candidate.AccessPointName);
if (identity is null ||
identity.Source != "TrustedSclExactDomainMatch" ||
identity.Confidence != LiveIedDiscoveryConfidenceLevel.High)
Expand Down Expand Up @@ -122,7 +124,7 @@ public static Iec61850DeviceIdentity ToConsumerIdentity(
{
IedName=verified.IedName,
Source=$"ARIEC61850 {verified.Source} ({verified.Confidence})",
LogicalDevices=domains.Distinct(StringComparer.OrdinalIgnoreCase).Select(domain =>
LogicalDevices=domains.Distinct(StringComparer.Ordinal).Select(domain =>
new Iec61850LogicalDeviceIdentity
{
Domain=domain,
Expand Down
25 changes: 22 additions & 3 deletions engines/ARIEC61850.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "ccf31f60095ea65f9095514f0fa0102060962049",
"sourcePullRequest": 177,
"purpose": "P0-A identity code-only trial: exact case-sensitive MMS domain identity, provisional heuristics, SHA-pinned SCL AP/endpoint/domain evidence. Engine #177 Windows CI pending final verification; no physical promotion.",
"previousTrialPin": {
"commit": "4467124775d8d9d76f3db194f9fbfd97144767a8",
"sourcePullRequest": 134,
Expand Down Expand Up @@ -270,5 +270,24 @@
"physicalQualificationRequired": true,
"mainlinePromotionForbidden": true,
"cyclicMmsProcessPollingForbidden": true
},
"previousIdentityTrialPin": {
"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."
},
"p0AIdentityTrial": {
"status": "code-trial-not-physical",
"baseEngineCommit": "51c2eefd61d6bcbe1116fda6b781b74bb9cf06da",
"exactCommit": "ccf31f60095ea65f9095514f0fa0102060962049",
"enginePullRequest": 177,
"consumerPullRequest": 624,
"engineCiRun": 39228439974,
"exactDomainCaseRequired": true,
"sclShaPinnedRequired": true,
"exactEndpointAndAccessPointRequired": true,
"discoveryWritesForbidden": true,
"physicalQualificationRequired": true,
"mainlinePromotionForbidden": true
}
}
36 changes: 36 additions & 0 deletions scripts/test-m217k-typed-status-trial.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,42 @@ function Test-M217KTypedStatusTrial {
$previous = '036e2c00d83a0815b6160147b14ea05975057965'
$candidate = '51c2eefd61d6bcbe1116fda6b781b74bb9cf06da'

# P0-A is a descendant of the exact M2.17-K code trial, never a replacement
# for the historical physical authority. Re-run the original unmodified
# M2.17-K guard against its preserved pin before admitting one exact child.
if ([int]$Lock.sourcePullRequest -eq 177) {
$identity = $Lock.p0AIdentityTrial
$saved = $Lock.previousIdentityTrialPin
if ($null -eq $identity -or $null -eq $saved) { return $false }
$historicalView = $Lock | Select-Object *
$historicalView.commit = $candidate
$historicalView.sourcePullRequest = 173
$historicalView.purpose = [string]$saved.purpose
$parentValid = Test-M217KTypedStatusTrial -Lock $historicalView
return (
$parentValid -and
[string]$Lock.repository -ceq 'masarray/ARIEC61850' -and
[string]$Lock.ref -ceq 'main' -and
[string]$saved.commit -ceq $candidate -and
[int]$saved.sourcePullRequest -eq 173 -and
[string]$saved.purpose -match '^M2.17-K isolated C264 acceptance trial' -and
[string]$identity.baseEngineCommit -ceq $candidate -and
[string]$identity.exactCommit -ceq 'ccf31f60095ea65f9095514f0fa0102060962049' -and
[string]$Lock.commit -ceq [string]$identity.exactCommit -and
[string]$Lock.purpose -match '^P0-A identity code-only trial' -and
[int]$identity.enginePullRequest -eq 177 -and
[int]$identity.consumerPullRequest -eq 624 -and
[long]$identity.engineCiRun -eq 39228439974 -and
[string]$identity.status -ceq 'code-trial-not-physical' -and
[bool]$identity.exactDomainCaseRequired -and
[bool]$identity.sclShaPinnedRequired -and
[bool]$identity.exactEndpointAndAccessPointRequired -and
[bool]$identity.discoveryWritesForbidden -and
[bool]$identity.physicalQualificationRequired -and
[bool]$identity.mainlinePromotionForbidden
)
}

# Audit the COMPLETE previously accepted P10.9V code-only ancestry,
# including physical R10 history, routing, subscriber integrity, canonical
# ingress, ZIV BSC, commissioning and typed control gates. A new head
Expand Down
11 changes: 8 additions & 3 deletions tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -474,9 +474,14 @@ public void EngineLock_PreservesPhysicalSclRepairBaselineAcrossCodeVerifiedInter
typedStatusTrial.GetProperty("previousConsumerPin").GetString());
Assert.Equal("51c2eefd61d6bcbe1116fda6b781b74bb9cf06da",
typedStatusTrial.GetProperty("exactCommit").GetString());
Assert.Equal(typedStatusTrial.GetProperty("exactCommit").GetString(),
var identityTrial = root.GetProperty("p0AIdentityTrial");
Assert.Equal(typedStatusTrial.GetProperty("exactCommit").GetString(), identityTrial.GetProperty("baseEngineCommit").GetString());
Assert.Equal(identityTrial.GetProperty("exactCommit").GetString(),
root.GetProperty("commit").GetString());
Assert.Equal(173, root.GetProperty("sourcePullRequest").GetInt32());
Assert.Equal("code-trial-not-physical", identityTrial.GetProperty("status").GetString());
Assert.True(identityTrial.GetProperty("physicalQualificationRequired").GetBoolean());
Assert.True(identityTrial.GetProperty("mainlinePromotionForbidden").GetBoolean());
Assert.Equal(177, root.GetProperty("sourcePullRequest").GetInt32());
Assert.Equal(618, typedStatusTrial.GetProperty("consumerPullRequest").GetInt32());
Assert.True(typedStatusTrial.GetProperty("strictFcAndTupleEvidenceRequired").GetBoolean());
Assert.True(typedStatusTrial.GetProperty("physicalQualificationRequired").GetBoolean());
Expand Down Expand Up @@ -520,7 +525,7 @@ public void EngineLock_PreservesPhysicalSclRepairBaselineAcrossCodeVerifiedInter
Assert.True(zivTrial.GetProperty("cyclicMmsProcessPollingForbidden").GetBoolean());
// The current C264 report trial is PR #173, while the previous
// P10.9V control authority remains the unchanged PR #169.
Assert.Equal(173, root.GetProperty("sourcePullRequest").GetInt32());
Assert.Equal(177, root.GetProperty("sourcePullRequest").GetInt32());
Assert.Equal(169, typedBscTrial.GetProperty("enginePullRequest").GetInt32());
Assert.True(genericTrial.GetProperty("typedSemanticGateRequired").GetBoolean());
Assert.True(genericTrial.GetProperty("associationScopedInventoryRequired").GetBoolean());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,13 @@ public void TestProductUsesExplicitlyApprovedEngineCommitNotPreviouslyPinnedDll(
using var lockDocument = JsonDocument.Parse(File.ReadAllText(
Path.Combine(root!.FullName, "engines", "ARIEC61850.lock.json")));
var lockRoot = lockDocument.RootElement;
var identityTrial = lockRoot.GetProperty("p0AIdentityTrial");
Assert.Equal(ExpectedEngineSha, identityTrial.GetProperty("baseEngineCommit").GetString());
Assert.Equal(ExpectedEngineSha,
lockRoot.GetProperty("previousIdentityTrialPin").GetProperty("commit").GetString());
Assert.Equal(identityTrial.GetProperty("exactCommit").GetString(),
lockRoot.GetProperty("commit").GetString());
Assert.Equal(173, lockRoot.GetProperty("sourcePullRequest").GetInt32());
Assert.Equal(177, lockRoot.GetProperty("sourcePullRequest").GetInt32());
var trial = lockRoot.GetProperty("m217KTypedStatusReportTrial");
Assert.Equal("036e2c00d83a0815b6160147b14ea05975057965",
trial.GetProperty("previousConsumerPin").GetString());
Expand Down
15 changes: 15 additions & 0 deletions tests/ARSAS.Tests/P10MainlinePolicyExecutionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public sealed class P10MainlinePolicyExecutionTests
[InlineData("p10-cyclic-fallback-denied", false)]
[InlineData("p10-history-tampered-denied", false)]
[InlineData("p10-dataset-lineage-tampered-denied", false)]
[InlineData("p0a-wrong-exact-sha-denied", false)]
[InlineData("p0a-wrong-parent-denied", false)]
[InlineData("p0a-wrong-ci-denied", false)]
[InlineData("p0a-wrong-pr-denied", false)]
[InlineData("p0a-physical-promote-denied", false)]
[InlineData("m217k-exact-sha-denied", false)]
[InlineData("m217k-engine-ci-denied", false)]
[InlineData("m217k-previous-pin-denied", false)]
Expand Down Expand Up @@ -124,6 +129,16 @@ public void Exact_CI_Gate_Fails_Closed_Without_False_Physical_Promotion(
switch (scenario)
{
case "p10-pr-main-denied": baseRef = "main"; break;
case "p0a-wrong-exact-sha-denied":
lockDoc["p0AIdentityTrial"]!["exactCommit"] = new string('0',40); break;
case "p0a-wrong-parent-denied":
lockDoc["p0AIdentityTrial"]!["baseEngineCommit"] = new string('0',40); break;
case "p0a-wrong-ci-denied":
lockDoc["p0AIdentityTrial"]!["engineCiRun"] = 0; break;
case "p0a-wrong-pr-denied":
lockDoc["p0AIdentityTrial"]!["enginePullRequest"] = 0; break;
case "p0a-physical-promote-denied":
lockDoc["p0AIdentityTrial"]!["mainlinePromotionForbidden"] = false; break;
case "m217k-exact-sha-denied":
lockDoc["m217KTypedStatusReportTrial"]!["exactCommit"] = new string('0', 40); break;
case "m217k-engine-ci-denied":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,14 @@ public void TrialLock_PreservesHistoricalPhysicalAuthorityAndExplicitlyRequiresP
typedStatusTrial.GetProperty("previousConsumerPin").GetString());
Assert.Equal("51c2eefd61d6bcbe1116fda6b781b74bb9cf06da",
typedStatusTrial.GetProperty("exactCommit").GetString());
Assert.Equal(typedStatusTrial.GetProperty("exactCommit").GetString(),
root.GetProperty("commit").GetString());
Assert.Equal(173, root.GetProperty("sourcePullRequest").GetInt32());
var identityTrial = root.GetProperty("p0AIdentityTrial");
Assert.Equal(typedStatusTrial.GetProperty("exactCommit").GetString(), identityTrial.GetProperty("baseEngineCommit").GetString());
Assert.Equal(typedStatusTrial.GetProperty("exactCommit").GetString(), root.GetProperty("previousIdentityTrialPin").GetProperty("commit").GetString());
Assert.Equal(identityTrial.GetProperty("exactCommit").GetString(), root.GetProperty("commit").GetString());
Assert.Equal("code-trial-not-physical", identityTrial.GetProperty("status").GetString());
Assert.True(identityTrial.GetProperty("physicalQualificationRequired").GetBoolean());
Assert.True(identityTrial.GetProperty("mainlinePromotionForbidden").GetBoolean());
Assert.Equal(177, root.GetProperty("sourcePullRequest").GetInt32());
Assert.Equal(618, typedStatusTrial.GetProperty("consumerPullRequest").GetInt32());
Assert.True(typedStatusTrial.GetProperty("strictFcAndTupleEvidenceRequired").GetBoolean());
Assert.True(typedStatusTrial.GetProperty("physicalQualificationRequired").GetBoolean());
Expand Down Expand Up @@ -168,7 +173,7 @@ public void TrialLock_PreservesHistoricalPhysicalAuthorityAndExplicitlyRequiresP
Assert.True(zivTrial.GetProperty("cyclicMmsProcessPollingForbidden").GetBoolean());
// The current C264 report trial is PR #173, while the previous
// P10.9V control authority remains the unchanged PR #169.
Assert.Equal(173, root.GetProperty("sourcePullRequest").GetInt32());
Assert.Equal(177, root.GetProperty("sourcePullRequest").GetInt32());
Assert.Equal(169, typedBscTrial.GetProperty("enginePullRequest").GetInt32());
Assert.Equal(540, genericTrial.GetProperty("issue").GetInt32());
Assert.Equal(38231327892L, genericTrial.GetProperty("engineCiRun").GetInt64());
Expand Down
51 changes: 47 additions & 4 deletions tests/ARSAS.Tests/TrustedSclIdentityConsumerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,47 @@ public async Task Missing_Trusted_SCL_And_Wrong_MMS_Endpoint_Does_Not_Guess()
temp.Device,Model("BCUGEF650")));
}


[Fact]
public async Task Scl_Identity_Requires_The_Selected_AccessPoint()
{
using var temp=TempScl.Create("STATIONBCU","CTRL","192.0.2.20");
temp.Device.SclAccessPointName="OTHER_AP";
Assert.Null(await TrustedSclIdentityAuthority.TryMatchAsync(
temp.Device,Model("STATIONBCUCTRL")));
}

[Fact]
public async Task Five_Live_Domains_With_Verified_Scl_Preserve_Exact_Ied_And_Original_Mms_Refs()
{
using var temp=TempScl.Create("SIEBCU","CTRL","192.0.2.20",
"MEAS","PROT","UD1","UD2");
var domains=new[] {"SIEBCUCTRL","SIEBCUMEAS","SIEBCUPROT","SIEBCUUD1","SIEBCUUD2"};
var model=Model(domains);
var verified=await TrustedSclIdentityAuthority.TryMatchAsync(temp.Device,model);
Assert.NotNull(verified);
Assert.Equal("SIEBCU",verified.IedName);
Assert.Equal(LiveIedDiscoveryConfidenceLevel.High,verified.Confidence);
var consumer=TrustedSclIdentityAuthority.ToConsumerIdentity(verified,domains);
Assert.Equal("SIEBCU",consumer.IedName);
Assert.Equal(domains,consumer.LogicalDevices.Select(d=>d.Domain).ToArray());
Assert.Equal("UD2",consumer.LogicalDevices.Last().Instance);
}


[Fact]
public async Task Verified_Identity_Preserves_Case_Distinct_Raw_Mms_Domains_In_Consumer()
{
using var temp=TempScl.Create("STATIONBCU","CTRL","192.0.2.20","ctrl");
var domains=new[] {"STATIONBCUCTRL","STATIONBCUctrl"};
var verified=await TrustedSclIdentityAuthority.TryMatchAsync(temp.Device,Model(domains));
Assert.NotNull(verified);
var projected=TrustedSclIdentityAuthority.ToConsumerIdentity(verified,domains);
Assert.Equal(domains,projected.LogicalDevices.Select(x=>x.Domain).ToArray());
Assert.Equal(new[] {"CTRL","ctrl"},
projected.LogicalDevices.Select(x=>x.Instance).ToArray());
}

[Fact]
public void Canonical_Save_And_RCB_Export_Must_Consume_Verified_Identity()
{
Expand All @@ -60,10 +101,10 @@ public void Canonical_Save_And_RCB_Export_Must_Consume_Verified_Identity()
Assert.Contains("TrustedSclIdentityAuthority.ToConsumerIdentity",runtime);
}

private static LiveIedModelDiscoveryDocument Model(string domain) => new()
private static LiveIedModelDiscoveryDocument Model(params string[] domains) => new()
{
Host="192.16.1.33",IedName="IED_192_16_1_33",
LogicalDevices=[new LiveIedLogicalDeviceModel{MmsDomain=domain,Inst=domain}]
LogicalDevices=domains.Select(domain=>new LiveIedLogicalDeviceModel{MmsDomain=domain,Inst=domain}).ToArray()
};

private sealed class TempScl : IDisposable
Expand All @@ -74,15 +115,17 @@ private TempScl(string folder,Iec61850MonitorDevice device)
{
_folder=folder;Device=device;
}
public static TempScl Create(string ied,string ld,string ip)
public static TempScl Create(string ied,string ld,string ip,params string[] otherLdInstances)
{
var dir=Path.Combine(Path.GetTempPath(),"arsas-ied-p10-"+Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
var path=Path.Combine(dir,"engineering.iid");
var remaining=string.Concat(otherLdInstances.Select(inst=>
$"""<LDevice inst="{inst}"><LN0 lnClass="LLN0" lnType="LLN0_t"/></LDevice>"""));
var xml=$"""
<SCL xmlns="http://www.iec.ch/61850/2003/SCL">
<Communication><SubNetwork name="Bus"><ConnectedAP iedName="{ied}" apName="S1"><Address><P type="IP">{ip}</P></Address></ConnectedAP></SubNetwork></Communication>
<IED name="{ied}"><AccessPoint name="S1"><Server><LDevice inst="{ld}"><LN0 lnClass="LLN0" lnType="LLN0_t"/></LDevice></Server></AccessPoint></IED>
<IED name="{ied}"><AccessPoint name="S1"><Server><LDevice inst="{ld}"><LN0 lnClass="LLN0" lnType="LLN0_t"/></LDevice>{remaining}</Server></AccessPoint></IED>
</SCL>
""";
File.WriteAllText(path,xml);
Expand Down
Loading