549 lines
20 KiB
C#
549 lines
20 KiB
C#
// -----------------------------------------------------------------------------
|
|
// CrossModuleEvidenceLinkingTests.cs
|
|
// Sprint: SPRINT_8100_0012_0002 - Unified Evidence Model
|
|
// Task: EVID-8100-018 - Cross-module evidence linking integration tests
|
|
// Description: Integration tests verifying evidence linking across modules:
|
|
// - Same subject can have evidence from multiple modules
|
|
// - Evidence types from Scanner, Attestor, Policy, Excititor
|
|
// - Evidence chain/graph queries work correctly
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using FluentAssertions;
|
|
using StellaOps.Evidence.Core;
|
|
using StellaOps.Evidence.Persistence.Postgres.Tests.Fixtures;
|
|
using Xunit;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Evidence.Persistence.Postgres.Tests;
|
|
|
|
/// <summary>
|
|
/// Integration tests for cross-module evidence linking.
|
|
/// Verifies that the unified evidence model correctly links evidence
|
|
/// from different modules (Scanner, Attestor, Policy, Excititor) to the same subject.
|
|
/// </summary>
|
|
[Collection(EvidencePostgresTestCollection.Name)]
|
|
public sealed class CrossModuleEvidenceLinkingTests : IAsyncLifetime
|
|
{
|
|
private readonly EvidencePostgresContainerFixture _fixture;
|
|
private readonly ITestOutputHelper _output;
|
|
private readonly string _tenantId = Guid.NewGuid().ToString();
|
|
private PostgresEvidenceStore _store = null!;
|
|
|
|
public CrossModuleEvidenceLinkingTests(
|
|
EvidencePostgresContainerFixture fixture,
|
|
ITestOutputHelper output)
|
|
{
|
|
_fixture = fixture;
|
|
_output = output;
|
|
}
|
|
|
|
public Task InitializeAsync()
|
|
{
|
|
_store = _fixture.CreateStore(_tenantId);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
await _fixture.TruncateAllTablesAsync();
|
|
}
|
|
|
|
#region Multi-Module Evidence for Same Subject
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task SameSubject_MultipleEvidenceTypes_AllLinked()
|
|
{
|
|
// Arrange - A container image subject with evidence from multiple modules
|
|
var subjectNodeId = $"sha256:{Guid.NewGuid():N}"; // Container image digest
|
|
|
|
var scannerEvidence = CreateScannerEvidence(subjectNodeId);
|
|
var reachabilityEvidence = CreateReachabilityEvidence(subjectNodeId);
|
|
var policyEvidence = CreatePolicyEvidence(subjectNodeId);
|
|
var vexEvidence = CreateVexEvidence(subjectNodeId);
|
|
var provenanceEvidence = CreateProvenanceEvidence(subjectNodeId);
|
|
|
|
// Act - Store all evidence
|
|
await _store.StoreAsync(scannerEvidence);
|
|
await _store.StoreAsync(reachabilityEvidence);
|
|
await _store.StoreAsync(policyEvidence);
|
|
await _store.StoreAsync(vexEvidence);
|
|
await _store.StoreAsync(provenanceEvidence);
|
|
|
|
// Assert - All evidence linked to same subject
|
|
var allEvidence = await _store.GetBySubjectAsync(subjectNodeId);
|
|
|
|
allEvidence.Should().HaveCount(5);
|
|
allEvidence.Select(e => e.EvidenceType).Should().Contain(new[]
|
|
{
|
|
EvidenceType.Scan,
|
|
EvidenceType.Reachability,
|
|
EvidenceType.Policy,
|
|
EvidenceType.Vex,
|
|
EvidenceType.Provenance
|
|
});
|
|
|
|
_output.WriteLine($"Subject {subjectNodeId} has {allEvidence.Count} evidence records from different modules");
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task SameSubject_FilterByType_ReturnsCorrectEvidence()
|
|
{
|
|
// Arrange
|
|
var subjectNodeId = $"sha256:{Guid.NewGuid():N}";
|
|
|
|
await _store.StoreAsync(CreateScannerEvidence(subjectNodeId));
|
|
await _store.StoreAsync(CreateScannerEvidence(subjectNodeId)); // Another scan finding
|
|
await _store.StoreAsync(CreateReachabilityEvidence(subjectNodeId));
|
|
await _store.StoreAsync(CreatePolicyEvidence(subjectNodeId));
|
|
|
|
// Act - Filter by Scan type
|
|
var scanEvidence = await _store.GetBySubjectAsync(subjectNodeId, EvidenceType.Scan);
|
|
var policyEvidence = await _store.GetBySubjectAsync(subjectNodeId, EvidenceType.Policy);
|
|
|
|
// Assert
|
|
scanEvidence.Should().HaveCount(2);
|
|
policyEvidence.Should().HaveCount(1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Evidence Chain Scenarios
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task EvidenceChain_ScanToVexToPolicy_LinkedCorrectly()
|
|
{
|
|
// Scenario: Vulnerability scan → VEX assessment → Policy decision
|
|
// All evidence points to the same subject (vulnerability finding)
|
|
|
|
var vulnerabilitySubject = $"sha256:{Guid.NewGuid():N}";
|
|
|
|
// 1. Scanner finds vulnerability
|
|
var scanEvidence = CreateScannerEvidence(vulnerabilitySubject);
|
|
await _store.StoreAsync(scanEvidence);
|
|
|
|
// 2. VEX assessment received
|
|
var vexEvidence = CreateVexEvidence(vulnerabilitySubject, referencedEvidenceId: scanEvidence.EvidenceId);
|
|
await _store.StoreAsync(vexEvidence);
|
|
|
|
// 3. Policy engine makes decision
|
|
var policyEvidence = CreatePolicyEvidence(vulnerabilitySubject, referencedEvidenceId: vexEvidence.EvidenceId);
|
|
await _store.StoreAsync(policyEvidence);
|
|
|
|
// Assert - Chain is queryable
|
|
var allEvidence = await _store.GetBySubjectAsync(vulnerabilitySubject);
|
|
allEvidence.Should().HaveCount(3);
|
|
|
|
// Verify order by type represents the chain
|
|
var scan = allEvidence.First(e => e.EvidenceType == EvidenceType.Scan);
|
|
var vex = allEvidence.First(e => e.EvidenceType == EvidenceType.Vex);
|
|
var policy = allEvidence.First(e => e.EvidenceType == EvidenceType.Policy);
|
|
|
|
scan.Should().NotBeNull();
|
|
vex.Should().NotBeNull();
|
|
policy.Should().NotBeNull();
|
|
|
|
_output.WriteLine($"Evidence chain: Scan({scan.EvidenceId}) → VEX({vex.EvidenceId}) → Policy({policy.EvidenceId})");
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task EvidenceChain_ReachabilityToEpssToPolicy_LinkedCorrectly()
|
|
{
|
|
// Scenario: Reachability analysis + EPSS score → Policy decision
|
|
var subjectNodeId = $"sha256:{Guid.NewGuid():N}";
|
|
|
|
// 1. Reachability analysis
|
|
var reachability = CreateReachabilityEvidence(subjectNodeId);
|
|
await _store.StoreAsync(reachability);
|
|
|
|
// 2. EPSS score
|
|
var epss = CreateEpssEvidence(subjectNodeId);
|
|
await _store.StoreAsync(epss);
|
|
|
|
// 3. Policy decision based on both
|
|
var policy = CreatePolicyEvidence(subjectNodeId, referencedEvidenceIds: new[]
|
|
{
|
|
reachability.EvidenceId,
|
|
epss.EvidenceId
|
|
});
|
|
await _store.StoreAsync(policy);
|
|
|
|
// Assert
|
|
var allEvidence = await _store.GetBySubjectAsync(subjectNodeId);
|
|
allEvidence.Should().HaveCount(3);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Multi-Tenant Evidence Isolation
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task MultiTenant_SameSubject_IsolatedByTenant()
|
|
{
|
|
// Arrange - Two tenants with evidence for the same subject
|
|
var subjectNodeId = $"sha256:{Guid.NewGuid():N}";
|
|
var tenantA = Guid.NewGuid().ToString();
|
|
var tenantB = Guid.NewGuid().ToString();
|
|
|
|
var storeA = _fixture.CreateStore(tenantA);
|
|
var storeB = _fixture.CreateStore(tenantB);
|
|
|
|
var evidenceA = CreateScannerEvidence(subjectNodeId);
|
|
var evidenceB = CreateScannerEvidence(subjectNodeId);
|
|
|
|
// Act - Store in different tenant stores
|
|
await storeA.StoreAsync(evidenceA);
|
|
await storeB.StoreAsync(evidenceB);
|
|
|
|
// Assert - Each tenant only sees their own evidence
|
|
var retrievedA = await storeA.GetBySubjectAsync(subjectNodeId);
|
|
var retrievedB = await storeB.GetBySubjectAsync(subjectNodeId);
|
|
|
|
retrievedA.Should().HaveCount(1);
|
|
retrievedA[0].EvidenceId.Should().Be(evidenceA.EvidenceId);
|
|
|
|
retrievedB.Should().HaveCount(1);
|
|
retrievedB[0].EvidenceId.Should().Be(evidenceB.EvidenceId);
|
|
|
|
_output.WriteLine($"Tenant A evidence: {evidenceA.EvidenceId}");
|
|
_output.WriteLine($"Tenant B evidence: {evidenceB.EvidenceId}");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Evidence Graph Queries
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task EvidenceGraph_AllTypesForArtifact_ReturnsComplete()
|
|
{
|
|
// Arrange - Simulate a complete evidence graph for a container artifact
|
|
var artifactDigest = $"sha256:{Guid.NewGuid():N}";
|
|
|
|
var evidenceRecords = new[]
|
|
{
|
|
CreateArtifactEvidence(artifactDigest), // SBOM entry
|
|
CreateScannerEvidence(artifactDigest), // Vulnerability scan
|
|
CreateReachabilityEvidence(artifactDigest), // Reachability analysis
|
|
CreateEpssEvidence(artifactDigest), // EPSS score
|
|
CreateVexEvidence(artifactDigest), // VEX statement
|
|
CreatePolicyEvidence(artifactDigest), // Policy decision
|
|
CreateProvenanceEvidence(artifactDigest), // Build provenance
|
|
CreateExceptionEvidence(artifactDigest) // Exception applied
|
|
};
|
|
|
|
foreach (var record in evidenceRecords)
|
|
{
|
|
await _store.StoreAsync(record);
|
|
}
|
|
|
|
// Act - Query all evidence types
|
|
var allEvidence = await _store.GetBySubjectAsync(artifactDigest);
|
|
|
|
// Assert - Complete evidence graph
|
|
allEvidence.Should().HaveCount(8);
|
|
allEvidence.Select(e => e.EvidenceType).Distinct().Should().HaveCount(8);
|
|
|
|
// Log evidence graph
|
|
foreach (var evidence in allEvidence)
|
|
{
|
|
_output.WriteLine($" {evidence.EvidenceType}: {evidence.EvidenceId}");
|
|
}
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task EvidenceGraph_ExistsCheck_ForAllTypes()
|
|
{
|
|
// Arrange
|
|
var subjectNodeId = $"sha256:{Guid.NewGuid():N}";
|
|
|
|
await _store.StoreAsync(CreateScannerEvidence(subjectNodeId));
|
|
await _store.StoreAsync(CreateReachabilityEvidence(subjectNodeId));
|
|
// Note: No Policy evidence
|
|
|
|
// Act & Assert
|
|
(await _store.ExistsAsync(subjectNodeId, EvidenceType.Scan)).Should().BeTrue();
|
|
(await _store.ExistsAsync(subjectNodeId, EvidenceType.Reachability)).Should().BeTrue();
|
|
(await _store.ExistsAsync(subjectNodeId, EvidenceType.Policy)).Should().BeFalse();
|
|
(await _store.ExistsAsync(subjectNodeId, EvidenceType.Vex)).Should().BeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cross-Module Evidence Correlation
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Correlation_SameCorrelationId_FindsRelatedEvidence()
|
|
{
|
|
// Arrange - Evidence from different modules with same correlation ID
|
|
var subjectNodeId = $"sha256:{Guid.NewGuid():N}";
|
|
var correlationId = Guid.NewGuid().ToString();
|
|
|
|
var scanEvidence = CreateScannerEvidence(subjectNodeId, correlationId: correlationId);
|
|
var reachEvidence = CreateReachabilityEvidence(subjectNodeId, correlationId: correlationId);
|
|
var policyEvidence = CreatePolicyEvidence(subjectNodeId, correlationId: correlationId);
|
|
|
|
await _store.StoreAsync(scanEvidence);
|
|
await _store.StoreAsync(reachEvidence);
|
|
await _store.StoreAsync(policyEvidence);
|
|
|
|
// Act - Get all evidence for subject
|
|
var allEvidence = await _store.GetBySubjectAsync(subjectNodeId);
|
|
|
|
// Assert - All have same correlation ID
|
|
allEvidence.Should().HaveCount(3);
|
|
allEvidence.Should().OnlyContain(e => e.Provenance.CorrelationId == correlationId);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task Generators_MultiplePerSubject_AllPreserved()
|
|
{
|
|
// Arrange - Evidence from different generators
|
|
var subjectNodeId = $"sha256:{Guid.NewGuid():N}";
|
|
|
|
var trivyEvidence = CreateScannerEvidence(subjectNodeId, generator: "stellaops/scanner/trivy");
|
|
var grypeEvidence = CreateScannerEvidence(subjectNodeId, generator: "stellaops/scanner/grype");
|
|
var snykEvidence = CreateScannerEvidence(subjectNodeId, generator: "vendor/snyk");
|
|
|
|
await _store.StoreAsync(trivyEvidence);
|
|
await _store.StoreAsync(grypeEvidence);
|
|
await _store.StoreAsync(snykEvidence);
|
|
|
|
// Act
|
|
var scanEvidence = await _store.GetBySubjectAsync(subjectNodeId, EvidenceType.Scan);
|
|
|
|
// Assert
|
|
scanEvidence.Should().HaveCount(3);
|
|
scanEvidence.Select(e => e.Provenance.GeneratorId).Should()
|
|
.Contain(new[] { "stellaops/scanner/trivy", "stellaops/scanner/grype", "vendor/snyk" });
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Evidence Count and Statistics
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task CountBySubject_AfterMultiModuleInserts_ReturnsCorrectCount()
|
|
{
|
|
// Arrange
|
|
var subjectNodeId = $"sha256:{Guid.NewGuid():N}";
|
|
|
|
await _store.StoreAsync(CreateScannerEvidence(subjectNodeId));
|
|
await _store.StoreAsync(CreateReachabilityEvidence(subjectNodeId));
|
|
await _store.StoreAsync(CreatePolicyEvidence(subjectNodeId));
|
|
|
|
// Act
|
|
var count = await _store.CountBySubjectAsync(subjectNodeId);
|
|
|
|
// Assert
|
|
count.Should().Be(3);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task GetByType_AcrossSubjects_ReturnsAll()
|
|
{
|
|
// Arrange - Multiple subjects with same evidence type
|
|
var subject1 = $"sha256:{Guid.NewGuid():N}";
|
|
var subject2 = $"sha256:{Guid.NewGuid():N}";
|
|
var subject3 = $"sha256:{Guid.NewGuid():N}";
|
|
|
|
await _store.StoreAsync(CreateScannerEvidence(subject1));
|
|
await _store.StoreAsync(CreateScannerEvidence(subject2));
|
|
await _store.StoreAsync(CreateScannerEvidence(subject3));
|
|
await _store.StoreAsync(CreateReachabilityEvidence(subject1)); // Different type
|
|
|
|
// Act
|
|
var scanEvidence = await _store.GetByTypeAsync(EvidenceType.Scan);
|
|
|
|
// Assert
|
|
scanEvidence.Should().HaveCount(3);
|
|
scanEvidence.Select(e => e.SubjectNodeId).Should()
|
|
.Contain(new[] { subject1, subject2, subject3 });
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
private static EvidenceRecord CreateScannerEvidence(
|
|
string subjectNodeId,
|
|
string? correlationId = null,
|
|
string generator = "stellaops/scanner/trivy")
|
|
{
|
|
var payload = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
cve = $"CVE-2024-{Random.Shared.Next(1000, 9999)}",
|
|
severity = "HIGH",
|
|
affectedPackage = "example-lib@1.0.0"
|
|
});
|
|
|
|
var provenance = new EvidenceProvenance
|
|
{
|
|
GeneratorId = generator,
|
|
GeneratorVersion = "1.0.0",
|
|
GeneratedAt = DateTimeOffset.UtcNow,
|
|
CorrelationId = correlationId ?? Guid.NewGuid().ToString()
|
|
};
|
|
|
|
return EvidenceRecord.Create(subjectNodeId, EvidenceType.Scan, payload, provenance, "1.0.0");
|
|
}
|
|
|
|
private static EvidenceRecord CreateReachabilityEvidence(
|
|
string subjectNodeId,
|
|
string? correlationId = null)
|
|
{
|
|
var payload = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
reachable = true,
|
|
confidence = 0.95,
|
|
paths = new[] { "main.go:42", "handler.go:128" }
|
|
});
|
|
|
|
var provenance = new EvidenceProvenance
|
|
{
|
|
GeneratorId = "stellaops/scanner/reachability",
|
|
GeneratorVersion = "1.0.0",
|
|
GeneratedAt = DateTimeOffset.UtcNow,
|
|
CorrelationId = correlationId ?? Guid.NewGuid().ToString()
|
|
};
|
|
|
|
return EvidenceRecord.Create(subjectNodeId, EvidenceType.Reachability, payload, provenance, "1.0.0");
|
|
}
|
|
|
|
private static EvidenceRecord CreatePolicyEvidence(
|
|
string subjectNodeId,
|
|
string? referencedEvidenceId = null,
|
|
string[]? referencedEvidenceIds = null,
|
|
string? correlationId = null)
|
|
{
|
|
var refs = referencedEvidenceIds ?? (referencedEvidenceId is not null ? new[] { referencedEvidenceId } : null);
|
|
|
|
var payload = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
ruleId = "vuln-severity-block",
|
|
verdict = "BLOCK",
|
|
referencedEvidence = refs
|
|
});
|
|
|
|
var provenance = new EvidenceProvenance
|
|
{
|
|
GeneratorId = "stellaops/policy/opa",
|
|
GeneratorVersion = "1.0.0",
|
|
GeneratedAt = DateTimeOffset.UtcNow,
|
|
CorrelationId = correlationId ?? Guid.NewGuid().ToString()
|
|
};
|
|
|
|
return EvidenceRecord.Create(subjectNodeId, EvidenceType.Policy, payload, provenance, "1.0.0");
|
|
}
|
|
|
|
private static EvidenceRecord CreateVexEvidence(
|
|
string subjectNodeId,
|
|
string? referencedEvidenceId = null)
|
|
{
|
|
var payload = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
status = "not_affected",
|
|
justification = "vulnerable_code_not_in_execute_path",
|
|
referencedEvidence = referencedEvidenceId
|
|
});
|
|
|
|
var provenance = new EvidenceProvenance
|
|
{
|
|
GeneratorId = "stellaops/excititor/vex",
|
|
GeneratorVersion = "1.0.0",
|
|
GeneratedAt = DateTimeOffset.UtcNow
|
|
};
|
|
|
|
return EvidenceRecord.Create(subjectNodeId, EvidenceType.Vex, payload, provenance, "1.0.0");
|
|
}
|
|
|
|
private static EvidenceRecord CreateEpssEvidence(string subjectNodeId)
|
|
{
|
|
var payload = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
score = 0.0342,
|
|
percentile = 0.89,
|
|
modelDate = "2024-12-25"
|
|
});
|
|
|
|
var provenance = new EvidenceProvenance
|
|
{
|
|
GeneratorId = "stellaops/scanner/epss",
|
|
GeneratorVersion = "1.0.0",
|
|
GeneratedAt = DateTimeOffset.UtcNow
|
|
};
|
|
|
|
return EvidenceRecord.Create(subjectNodeId, EvidenceType.Epss, payload, provenance, "1.0.0");
|
|
}
|
|
|
|
private static EvidenceRecord CreateProvenanceEvidence(string subjectNodeId)
|
|
{
|
|
var payload = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
buildId = Guid.NewGuid().ToString(),
|
|
builder = "github-actions",
|
|
inputs = new[] { "go.mod", "main.go" }
|
|
});
|
|
|
|
var provenance = new EvidenceProvenance
|
|
{
|
|
GeneratorId = "stellaops/attestor/provenance",
|
|
GeneratorVersion = "1.0.0",
|
|
GeneratedAt = DateTimeOffset.UtcNow
|
|
};
|
|
|
|
return EvidenceRecord.Create(subjectNodeId, EvidenceType.Provenance, payload, provenance, "1.0.0");
|
|
}
|
|
|
|
private static EvidenceRecord CreateArtifactEvidence(string subjectNodeId)
|
|
{
|
|
var payload = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
purl = "pkg:golang/example.com/mylib@1.0.0",
|
|
digest = subjectNodeId,
|
|
sbomFormat = "SPDX-3.0.1"
|
|
});
|
|
|
|
var provenance = new EvidenceProvenance
|
|
{
|
|
GeneratorId = "stellaops/scanner/sbom",
|
|
GeneratorVersion = "1.0.0",
|
|
GeneratedAt = DateTimeOffset.UtcNow
|
|
};
|
|
|
|
return EvidenceRecord.Create(subjectNodeId, EvidenceType.Artifact, payload, provenance, "1.0.0");
|
|
}
|
|
|
|
private static EvidenceRecord CreateExceptionEvidence(string subjectNodeId)
|
|
{
|
|
var payload = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
exceptionId = Guid.NewGuid().ToString(),
|
|
reason = "Risk accepted per security review",
|
|
expiry = DateTimeOffset.UtcNow.AddDays(90)
|
|
});
|
|
|
|
var provenance = new EvidenceProvenance
|
|
{
|
|
GeneratorId = "stellaops/policy/exceptions",
|
|
GeneratorVersion = "1.0.0",
|
|
GeneratedAt = DateTimeOffset.UtcNow
|
|
};
|
|
|
|
return EvidenceRecord.Create(subjectNodeId, EvidenceType.Exception, payload, provenance, "1.0.0");
|
|
}
|
|
|
|
#endregion
|
|
}
|