diff --git a/build/azure-devops/templates/agents/run-opentelemetry-collector.yml b/build/azure-devops/templates/agents/run-opentelemetry-collector.yml index d418fa493..70df031dd 100644 --- a/build/azure-devops/templates/agents/run-opentelemetry-collector.yml +++ b/build/azure-devops/templates/agents/run-opentelemetry-collector.yml @@ -34,8 +34,8 @@ steps: displayName: 'Show OpenTelemetry configuration' - script: | echo Mounting volumes: ${{ parameters.volumes }} - docker run -d -p 8888:8888 -p 8889:8889 --name ${{ parameters.containerName }} $(networkArgument) --volume ${{ parameters.volumes }} otel/opentelemetry-collector --config /etc/otel-collector-config.yaml + docker run -d -p 8888:8888 -p 8889:8889 --name ${{ parameters.containerName }} $(networkArgument) --volume ${{ parameters.volumes }} otel/opentelemetry-collector:0.103.0 --config /etc/otel-collector-config.yaml sleep 10 docker logs ${{ parameters.containerName }} displayName: Run OpenTelemetry Collector as ${{ parameters.containerName }} container - failOnStderr: false \ No newline at end of file + failOnStderr: false diff --git a/changelog/content/experimental/unreleased.md b/changelog/content/experimental/unreleased.md index 46f4bf549..2b4af7d2f 100644 --- a/changelog/content/experimental/unreleased.md +++ b/changelog/content/experimental/unreleased.md @@ -6,8 +6,8 @@ version: #### Scraper -None. - +- {{% tag added %}} Provide support for Azure Firewall ([docs](https://docs.promitor.io/scraping/providers/azure-firewall/)) + #### Resource Discovery -None. +- {{% tag added %}} Provide support for Azure Firewall ([docs](https://docs.promitor.io/scraping/providers/azure-firewall/)) diff --git a/config/promitor/resource-discovery/resource-discovery-declaration.yaml b/config/promitor/resource-discovery/resource-discovery-declaration.yaml index 95446ff57..b1392bc73 100644 --- a/config/promitor/resource-discovery/resource-discovery-declaration.yaml +++ b/config/promitor/resource-discovery/resource-discovery-declaration.yaml @@ -15,6 +15,8 @@ resourceDiscoveryGroups: type: AppPlan - name: automation-accounts type: AutomationAccount +- name: azure-firewall + type: AzureFirewall - name: cdn-landscape type: Cdn - name: container-instances diff --git a/config/promitor/scraper/metrics.yaml b/config/promitor/scraper/metrics.yaml index 14f084c3e..d1c8b8afb 100644 --- a/config/promitor/scraper/metrics.yaml +++ b/config/promitor/scraper/metrics.yaml @@ -172,6 +172,17 @@ metrics: - name: automation-accounts resources: - accountName: promitor-testing-resource-eu-automation-1 + - name: azure_firewall_application_rule_hits + description: number of times Application rules were hit + resourceType: AzureFirewall + azureMetricConfiguration: + metricName: ApplicationRuleHit + aggregation: + type: Count + dimension: + name: Status + resourceDiscoveryGroups: + - name: azure-firewall - name: promitor_demo_frontdoor_backend_health_per_backend_pool description: "Health percentage for a backend in Azure Front Door" resourceType: FrontDoor diff --git a/src/Promitor.Agents.ResourceDiscovery/Graph/ResourceDiscoveryFactory.cs b/src/Promitor.Agents.ResourceDiscovery/Graph/ResourceDiscoveryFactory.cs index 1c0afdc37..d9ea48e8e 100644 --- a/src/Promitor.Agents.ResourceDiscovery/Graph/ResourceDiscoveryFactory.cs +++ b/src/Promitor.Agents.ResourceDiscovery/Graph/ResourceDiscoveryFactory.cs @@ -20,6 +20,8 @@ public static ResourceDiscoveryQuery UseResourceDiscoveryFor(ResourceType resour return new AppPlanDiscoveryQuery(); case ResourceType.AutomationAccount: return new AutomationAccountResourceDiscoveryQuery(); + case ResourceType.AzureFirewall: + return new AzureFirewallDiscoveryQuery(); case ResourceType.Cdn: return new CdnDiscoveryQuery(); case ResourceType.ContainerInstance: diff --git a/src/Promitor.Agents.ResourceDiscovery/Graph/ResourceTypes/AzureFirewallDiscoveryQuery.cs b/src/Promitor.Agents.ResourceDiscovery/Graph/ResourceTypes/AzureFirewallDiscoveryQuery.cs new file mode 100644 index 000000000..a69f638fa --- /dev/null +++ b/src/Promitor.Agents.ResourceDiscovery/Graph/ResourceTypes/AzureFirewallDiscoveryQuery.cs @@ -0,0 +1,23 @@ +using GuardNet; +using Newtonsoft.Json.Linq; +using Promitor.Core.Contracts; +using Promitor.Core.Contracts.ResourceTypes; + +namespace Promitor.Agents.ResourceDiscovery.Graph.ResourceTypes +{ + public class AzureFirewallDiscoveryQuery : ResourceDiscoveryQuery + { + public override string[] ResourceTypes => new[] { "microsoft.network/azurefirewalls" }; + public override string[] ProjectedFieldNames => new[] { "subscriptionId", "resourceGroup", "name" }; + + public override AzureResourceDefinition ParseResults(JToken resultRowEntry) + { + Guard.NotNull(resultRowEntry, nameof(resultRowEntry)); + + var azureFirewallName = resultRowEntry[2]?.ToString(); + + var resource = new AzureFirewallResourceDefinition(resultRowEntry[0]?.ToString(), resultRowEntry[1]?.ToString(), azureFirewallName); + return resource; + } + } +} diff --git a/src/Promitor.Agents.Scraper/Validation/Factories/MetricValidatorFactory.cs b/src/Promitor.Agents.Scraper/Validation/Factories/MetricValidatorFactory.cs index 2b6b4ec5b..770936bf5 100644 --- a/src/Promitor.Agents.Scraper/Validation/Factories/MetricValidatorFactory.cs +++ b/src/Promitor.Agents.Scraper/Validation/Factories/MetricValidatorFactory.cs @@ -21,6 +21,8 @@ internal static IMetricValidator GetValidatorFor(ResourceType resourceType) return new AppPlanMetricValidator(); case ResourceType.AutomationAccount: return new AutomationAccountMetricValidator(); + case ResourceType.AzureFirewall: + return new AzureFirewallMetricValidator(); case ResourceType.BlobStorage: return new BlobStorageMetricValidator(); case ResourceType.Cdn: diff --git a/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/AzureFirewallMetricValidator.cs b/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/AzureFirewallMetricValidator.cs new file mode 100644 index 000000000..8ec36c763 --- /dev/null +++ b/src/Promitor.Agents.Scraper/Validation/MetricDefinitions/ResourceTypes/AzureFirewallMetricValidator.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using GuardNet; +using Promitor.Core.Scraping.Configuration.Model.Metrics; +using Promitor.Agents.Scraper.Validation.MetricDefinitions.Interfaces; +using Promitor.Core.Contracts.ResourceTypes; + +namespace Promitor.Agents.Scraper.Validation.MetricDefinitions.ResourceTypes +{ + internal class AzureFirewallMetricValidator : IMetricValidator + { + public IEnumerable Validate(MetricDefinition metricDefinition) + { + Guard.NotNull(metricDefinition, nameof(metricDefinition)); + + foreach (var resourceDefinition in metricDefinition.Resources.Cast()) + { + if (string.IsNullOrWhiteSpace(resourceDefinition.AzureFirewallName)) + { + yield return "No firewall name is configured"; + } + } + } + } +} diff --git a/src/Promitor.Core.Contracts/ResourceType.cs b/src/Promitor.Core.Contracts/ResourceType.cs index 4aa4709b5..3be659419 100644 --- a/src/Promitor.Core.Contracts/ResourceType.cs +++ b/src/Promitor.Core.Contracts/ResourceType.cs @@ -55,5 +55,6 @@ public enum ResourceType PublicIpAddress = 50, TrafficManager = 51, PowerBiDedicated = 52, + AzureFirewall = 53, } -} \ No newline at end of file +} diff --git a/src/Promitor.Core.Contracts/ResourceTypes/AzureFirewallResourceDefinition.cs b/src/Promitor.Core.Contracts/ResourceTypes/AzureFirewallResourceDefinition.cs new file mode 100644 index 000000000..622c24f04 --- /dev/null +++ b/src/Promitor.Core.Contracts/ResourceTypes/AzureFirewallResourceDefinition.cs @@ -0,0 +1,13 @@ +namespace Promitor.Core.Contracts.ResourceTypes +{ + public class AzureFirewallResourceDefinition : AzureResourceDefinition + { + public AzureFirewallResourceDefinition(string subscriptionId, string resourceGroupName, string azureFirewallName) + : base(ResourceType.AzureFirewall, subscriptionId, resourceGroupName, azureFirewallName) + { + AzureFirewallName = azureFirewallName; + } + + public string AzureFirewallName { get; } + } +} diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureResourceDeserializerFactory.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureResourceDeserializerFactory.cs index 3a563ce9c..aeb7a5c72 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureResourceDeserializerFactory.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Core/AzureResourceDeserializerFactory.cs @@ -36,6 +36,9 @@ public IDeserializer GetDeserializerFor(ResourceType case ResourceType.AutomationAccount: var automationLogger = _loggerFactory.CreateLogger(); return new AutomationAccountDeserializer(automationLogger); + case ResourceType.AzureFirewall: + var azureFirewallLogger = _loggerFactory.CreateLogger(); + return new AzureFirewallDeserializer(azureFirewallLogger); case ResourceType.BlobStorage: var blobStorageLogger = _loggerFactory.CreateLogger(); return new BlobStorageDeserializer(blobStorageLogger); diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Mapping/V1MappingProfile.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Mapping/V1MappingProfile.cs index 9ddea7e2c..fae8007a2 100644 --- a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Mapping/V1MappingProfile.cs +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Mapping/V1MappingProfile.cs @@ -30,6 +30,7 @@ public V1MappingProfile() CreateMap(); CreateMap(); CreateMap(); + CreateMap(); CreateMap(); CreateMap(); CreateMap(); @@ -89,6 +90,7 @@ public V1MappingProfile() .Include() .Include() .Include() + .Include() .Include() .Include() .Include() diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/ResourceTypes/AzureFirewallResourceV1.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/ResourceTypes/AzureFirewallResourceV1.cs new file mode 100644 index 000000000..4987c05c6 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Model/ResourceTypes/AzureFirewallResourceV1.cs @@ -0,0 +1,13 @@ +namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes +{ + /// + /// Contains the configuration required to scrape an Azure firewall. + /// + public class AzureFirewallResourceV1 : AzureResourceDefinitionV1 + { + /// + /// The name of the Azure firewall to get metrics for. + /// + public string AzureFirewallName { get; set; } + } +} diff --git a/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/AzureFirewallDeserializer.cs b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/AzureFirewallDeserializer.cs new file mode 100644 index 000000000..3fb649d62 --- /dev/null +++ b/src/Promitor.Core.Scraping/Configuration/Serialization/v1/Providers/AzureFirewallDeserializer.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Logging; +using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; + +namespace Promitor.Core.Scraping.Configuration.Serialization.v1.Providers +{ + public class AzureFirewallDeserializer : ResourceDeserializer + { + public AzureFirewallDeserializer(ILogger logger) : base(logger) + { + Map(resource => resource.AzureFirewallName) + .IsRequired(); + } + } +} diff --git a/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs b/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs index 93db0a520..e918d5e76 100644 --- a/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs +++ b/src/Promitor.Core.Scraping/Factories/MetricScraperFactory.cs @@ -47,6 +47,8 @@ public IScraper CreateScraper(ResourceType metricDefin return new AppPlanScraper(scraperConfiguration); case ResourceType.AutomationAccount: return new AutomationAccountScraper(scraperConfiguration); + case ResourceType.AzureFirewall: + return new AzureFirewallScraper(scraperConfiguration); case ResourceType.BlobStorage: return new BlobStorageScraper(scraperConfiguration); case ResourceType.Cdn: diff --git a/src/Promitor.Core.Scraping/ResourceTypes/AzureFirewallScraper.cs b/src/Promitor.Core.Scraping/ResourceTypes/AzureFirewallScraper.cs new file mode 100644 index 000000000..f1f032dbd --- /dev/null +++ b/src/Promitor.Core.Scraping/ResourceTypes/AzureFirewallScraper.cs @@ -0,0 +1,21 @@ +using Promitor.Core.Contracts; +using Promitor.Core.Contracts.ResourceTypes; +using Promitor.Core.Scraping.Configuration.Model.Metrics; + +namespace Promitor.Core.Scraping.ResourceTypes +{ + internal class AzureFirewallScraper : AzureMonitorScraper + { + private const string ResourceUriTemplate = "subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Network/azureFirewalls/{2}"; + + public AzureFirewallScraper(ScraperConfiguration scraperConfiguration) + : base(scraperConfiguration) + { + } + + protected override string BuildResourceUri(string subscriptionId, ScrapeDefinition scrapeDefinition, AzureFirewallResourceDefinition resource) + { + return string.Format(ResourceUriTemplate, subscriptionId, scrapeDefinition.ResourceGroupName, resource.AzureFirewallName); + } + } +} diff --git a/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs b/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs index 7decdf171..b0f8b2621 100644 --- a/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs +++ b/src/Promitor.Tests.Unit/Builders/Metrics/v1/MetricsDeclarationBuilder.cs @@ -167,6 +167,24 @@ public MetricsDeclarationBuilder WithAutomationAccountMetric(string metricName = return this; } + public MetricsDeclarationBuilder WithAzureFirewallMetric(string metricName = "promitor-AzureFirewall", + string metricDescription = "Description for a metric", + string azureFirewallName = "promitor-AzureFirewall", + string azureMetricName = "ApplicationRuleHit", + string resourceDiscoveryGroupName = "", + int? azureMetricLimit = null, + bool omitResource = false) + { + var resource = new AzureFirewallResourceV1 + { + AzureFirewallName = azureFirewallName + }; + + CreateAndAddMetricDefinition(ResourceType.AzureFirewall, metricName, metricDescription, resourceDiscoveryGroupName, omitResource, azureMetricName, azureMetricLimit, resource); + + return this; + } + public MetricsDeclarationBuilder WithBlobStorageMetric(string metricName = "promitor", string metricDescription = "Description for a metric", string accountName = "promitor-account", diff --git a/src/Promitor.Tests.Unit/Serialization/v1/Providers/AzureFirewallDeserializerTests.cs b/src/Promitor.Tests.Unit/Serialization/v1/Providers/AzureFirewallDeserializerTests.cs new file mode 100644 index 000000000..11f80812f --- /dev/null +++ b/src/Promitor.Tests.Unit/Serialization/v1/Providers/AzureFirewallDeserializerTests.cs @@ -0,0 +1,45 @@ + +using System.ComponentModel; +using Promitor.Core.Scraping.Configuration.Serialization; +using Promitor.Core.Scraping.Configuration.Serialization.v1.Model; +using Promitor.Core.Scraping.Configuration.Serialization.v1.Model.ResourceTypes; +using Promitor.Core.Scraping.Configuration.Serialization.v1.Providers; +using Xunit; + +namespace Promitor.Tests.Unit.Serialization.v1.Providers +{ + [Category("Unit")] + public class AzureFirewallDeserializerTests : ResourceDeserializerTest + { + private readonly AzureFirewallDeserializer _deserializer; + + public AzureFirewallDeserializerTests() + { + _deserializer = new AzureFirewallDeserializer(Logger); + } + + [Fact] + public void Deserialize_AzureFirewallNameSupplied_SetsAzureFirewallName() + { + YamlAssert.PropertySet( + _deserializer, + "azureFirewallName: promitor-firewall-name", + "promitor-firewall-name", + r => r.AzureFirewallName); + } + + [Fact] + public void Deserialize_AzureFirewallNameNotSupplied_Null() + { + YamlAssert.PropertyNull( + _deserializer, + "resourceGroupName: promitor-group", + r => r.AzureFirewallName); + } + + protected override IDeserializer CreateDeserializer() + { + return new AzureFirewallDeserializer(Logger); + } + } +} diff --git a/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/ResourceTypes/AzureFirewallDeclarationValidationStepsTests.cs b/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/ResourceTypes/AzureFirewallDeclarationValidationStepsTests.cs new file mode 100644 index 000000000..112228c21 --- /dev/null +++ b/src/Promitor.Tests.Unit/Validation/Scraper/Metrics/ResourceTypes/AzureFirewallDeclarationValidationStepsTests.cs @@ -0,0 +1,152 @@ +using System.ComponentModel; +using Microsoft.Extensions.Logging.Abstractions; +using Promitor.Agents.Scraper.Validation.Steps; +using Promitor.Tests.Unit.Builders.Metrics.v1; +using Promitor.Tests.Unit.Stubs; +using Xunit; + +namespace Promitor.Tests.Unit.Validation.Scraper.Metrics.ResourceTypes +{ + [Category("Unit")] + public class AzureFirewallMetricsDeclarationValidationStepsTests : MetricsDeclarationValidationStepsTests + { + [Fact] + public void AzureFirewall_DeclarationWithoutAzureMetricName_Fails() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithAzureFirewallMetric(azureMetricName: string.Empty) + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider, NullLogger.Instance); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + PromitorAssert.ValidationFailed(validationResult); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(10001)] + public void AzureFirewallDeclaration_DeclarationWithInvalidMetricLimit_Fails(int metricLimit) + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithAzureFirewallMetric(azureMetricLimit: metricLimit) + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider, NullLogger.Instance); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + PromitorAssert.ValidationFailed(validationResult); + } + + [Fact] + public void AzureFirewall_DeclarationWithoutMetricDescription_Succeeded() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithAzureFirewallMetric(metricDescription: string.Empty) + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider, NullLogger.Instance); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + PromitorAssert.ValidationIsSuccessful(validationResult); + } + + [Fact] + public void AzureFirewall_DeclarationWithoutMetricName_Fails() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithAzureFirewallMetric(string.Empty) + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider, NullLogger.Instance); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + PromitorAssert.ValidationFailed(validationResult); + } + + [Fact] + public void AzureFirewallMetricsDeclaration_DeclarationWithoutAzureFirewall_Fails() + { + // Arrange + var rawDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithAzureFirewallMetric(azureFirewallName: string.Empty) + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider, NullLogger.Instance); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + PromitorAssert.ValidationFailed(validationResult); + } + + [Fact] + public void AzureFirewallMetricsDeclaration_DeclarationWithoutResourceAndResourceDiscoveryGroupInfo_Fails() + { + // Arrange + var rawMetricsDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithAzureFirewallMetric(omitResource: true) + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawMetricsDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider, NullLogger.Instance); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + PromitorAssert.ValidationFailed(validationResult); + } + + [Fact] + public void AzureFirewallMetricsDeclaration_DeclarationWithoutResourceButWithResourceDiscoveryGroupInfo_Succeeds() + { + // Arrange + var rawMetricsDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithAzureFirewallMetric(omitResource: true, resourceDiscoveryGroupName: "sample-collection") + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawMetricsDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider, NullLogger.Instance); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + PromitorAssert.ValidationIsSuccessful(validationResult); + } + + [Fact] + public void AzureFirewallMetricsDeclaration_ValidDeclaration_Succeeds() + { + // Arrange + var rawMetricsDeclaration = MetricsDeclarationBuilder.WithMetadata() + .WithAzureFirewallMetric() + .Build(Mapper); + var metricsDeclarationProvider = new MetricsDeclarationProviderStub(rawMetricsDeclaration, Mapper); + + // Act + var scrapingScheduleValidationStep = new MetricsDeclarationValidationStep(metricsDeclarationProvider, NullLogger.Instance); + var validationResult = scrapingScheduleValidationStep.Run(); + + // Assert + PromitorAssert.ValidationIsSuccessful(validationResult); + } + } +}