diff --git a/.spelling b/.spelling index 8fbd0529464..7c80b928e2e 100644 --- a/.spelling +++ b/.spelling @@ -13,6 +13,7 @@ AppImage AppVeyor artifact artifacts +ASP.NET AssemblyLoadContext behavior behaviors @@ -886,6 +887,7 @@ v0.5.0 v0.6.0 v6.0.0 ValidateNotNullOrEmpty +WebListener WebRequest win7-x86 WindowsVersion diff --git a/build.psm1 b/build.psm1 index 0ad818f2cea..162158c4c8a 100644 --- a/build.psm1 +++ b/build.psm1 @@ -778,6 +778,7 @@ function Publish-PSTestTools { $tools = @( @{Path="${PSScriptRoot}/test/tools/TestExe";Output="testexe"} + @{Path="${PSScriptRoot}/test/tools/WebListener";Output="WebListener"} ) if ($null -eq $Options) { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index 1ae16e8a685..7be94bf8b00 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -296,95 +296,6 @@ function ExecuteWebRequest return $result } -function GetSelfSignedCert { - <# - .NOTES - This certificate is not issued for any specific Key Usage - It cannot be used for any service that requires a specific key usage - It can be used for SSL/TLS Client Authentication - #> - $PfxBase64 = @' -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 -hqPP3QQIlSXpfTECuB4CAggA -'@ - $Bytes = [System.Convert]::FromBase64String($PfxBase64) - [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($Bytes) -} - <# Defines the list of redirect codes to test as well as the expected Method when the redirection is handled. @@ -412,6 +323,7 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { BeforeAll { $response = Start-HttpListener -Port 8080 + $WebListener = Start-WebListener } AfterAll { @@ -919,31 +831,6 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { #endregion SkipHeaderVerification Tests - #region Certificate Authentication Tests - - # Test pending creation of native test solution - # https://github.com/PowerShell/PowerShell/issues/4609 - It "Verifies Invoke-WebRequest Certificate Authentication Fails without -Certificate" -Pending { - $command = 'Invoke-WebRequest https://prod.idrix.eu/secure/' - $result = ExecuteWebCommand -command $command - ValidateResponse -response $result - - $result.Output | Should Match ([regex]::Escape('Error: No SSL client certificate presented')) - } - - # Test pending creation of native test solution - # https://github.com/PowerShell/PowerShell/issues/4609 - It "Verifies Invoke-WebRequest Certificate Authentication Successful with -Certificate" -Pending { - $Certificate = GetSelfSignedCert - $command = 'Invoke-WebRequest https://prod.idrix.eu/secure/ -Certificate $Certificate' - $result = ExecuteWebCommand -command $command - ValidateResponse -response $result - - $result.Output.Content | Should Match ([regex]::Escape('SSL Authentication OK!')) - } - - #endregion Certificate Authentication Tests - #region charset encoding tests Context "BasicHtmlWebResponseObject Encoding tests" { @@ -1222,6 +1109,32 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { #endregion Content Header Inclusion + #region Client Certificate Authentication + + It "Verifies Invoke-WebRequest Certificate Authentication Fails without -Certificate" { + $uri = Get-WebListenerUrl -Https -Test 'Cert' + $result = Invoke-WebRequest -Uri $uri -SkipCertificateCheck | + Select-Object -ExpandProperty Content | + ConvertFrom-Json + + $result.Status | Should Be 'FAILED' + } + + # Test skipped on macOS pending support for Client Certificate Authentication + # https://github.com/PowerShell/PowerShell/issues/4650 + It "Verifies Invoke-WebRequest Certificate Authentication Successful with -Certificate" -skip:$IsOSX { + $uri = Get-WebListenerUrl -Https -Test 'Cert' + $certificate = Get-WebListenerClientCertificate + $result = Invoke-WebRequest -Uri $uri -Certificate $certificate -SkipCertificateCheck | + Select-Object -ExpandProperty Content | + ConvertFrom-Json + + $result.Status | Should Be 'OK' + $result.Thumbprint | Should Be $certificate.Thumbprint + } + + #endregion Client Certificate Authentication + BeforeEach { if ($env:http_proxy) { $savedHttpProxy = $env:http_proxy @@ -1253,6 +1166,7 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" { BeforeAll { $response = Start-HttpListener -Port 8081 + $WebListener = Start-WebListener } AfterAll { @@ -1740,28 +1654,27 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" { #endregion SkipHeaderVerification tests - #region Certificate Authentication Tests + #region Client Certificate Authentication - # Test pending creation of native test solution - # https://github.com/PowerShell/PowerShell/issues/4609 - It "Verifies Invoke-RestMethod Certificate Authentication Fails without -Certificate" -Pending { - $command = 'Invoke-RestMethod https://prod.idrix.eu/secure/' - $result = ExecuteWebCommand -command $command - - $result.Output | Should Match ([regex]::Escape('Error: No SSL client certificate presented')) + It "Verifies Invoke-RestMethod Certificate Authentication Fails without -Certificate" { + $uri = Get-WebListenerUrl -Https -Test 'Cert' + $result = Invoke-RestMethod -Uri $uri -SkipCertificateCheck + + $result.Status | Should Be 'FAILED' } - # Test pending creation of native test solution - # https://github.com/PowerShell/PowerShell/issues/4609 - It "Verifies Invoke-RestMethod Certificate Authentication Successful with -Certificate" -Pending { - $Certificate = GetSelfSignedCert - $command = 'Invoke-RestMethod https://prod.idrix.eu/secure/ -Certificate $Certificate' - $result = ExecuteWebCommand -command $command + # Test skipped on macOS pending support for Client Certificate Authentication + # https://github.com/PowerShell/PowerShell/issues/4650 + It "Verifies Invoke-RestMethod Certificate Authentication Successful with -Certificate" -skip:$IsOSX { + $uri = Get-WebListenerUrl -Https -Test 'Cert' + $certificate = Get-WebListenerClientCertificate + $result = Invoke-RestMethod -uri $uri -Certificate $certificate -SkipCertificateCheck - $result.Output | Should Match ([regex]::Escape('SSL Authentication OK!')) + $result.Status | Should Be 'OK' + $result.Thumbprint | Should Be $certificate.Thumbprint } - #endregion Certificate Authentication Tests + #endregion Client Certificate Authentication BeforeEach { if ($env:http_proxy) { diff --git a/test/tools/Modules/WebListener/ClientCert.pfx b/test/tools/Modules/WebListener/ClientCert.pfx new file mode 100644 index 00000000000..3303943fb72 Binary files /dev/null and b/test/tools/Modules/WebListener/ClientCert.pfx differ diff --git a/test/tools/Modules/WebListener/README.md b/test/tools/Modules/WebListener/README.md new file mode 100644 index 00000000000..f66d94b2c16 --- /dev/null +++ b/test/tools/Modules/WebListener/README.md @@ -0,0 +1,17 @@ +# WebListener Module + +A PowerShell module for managing the WebListener App. The included SelF-Signed Certificate `ServerCert.pfx` has the password set to `password` and is issued for the Client and Server Authentication key usages. This certificate is used by the WebListener App for SSL/TLS. The included SelF-Signed Certificate `ClientCert.pfx` has the password set to `password` and has not been issued for any specific key usage. This Certificate is used for Client Certificate Authentication with the WebListener App. + +# Running WebListener + +```powershell +Import-Module .\build.psm1 +Publish-PSTestTools +$Listener = Start-WebListener -HttpPort 8083 -HttpsPort 8084 +``` + +# Stopping WebListener + +```powershell +Stop-WebListener +``` diff --git a/test/tools/Modules/WebListener/ServerCert.pfx b/test/tools/Modules/WebListener/ServerCert.pfx new file mode 100644 index 00000000000..dd1cf6a9a81 Binary files /dev/null and b/test/tools/Modules/WebListener/ServerCert.pfx differ diff --git a/test/tools/Modules/WebListener/WebListener.psd1 b/test/tools/Modules/WebListener/WebListener.psd1 new file mode 100644 index 00000000000..91021b45e5b --- /dev/null +++ b/test/tools/Modules/WebListener/WebListener.psd1 @@ -0,0 +1,14 @@ +@{ + ModuleVersion = '1.0.0' + GUID = '90572e25-3f15-49b0-8f25-fb717d3ef46a' + Author = 'Mark Kraus' + Description = 'An HTTP and HTTPS Listener for testing purposes' + RootModule = 'WebListener.psm1' + FunctionsToExport = @( + 'Get-WebListener' + 'Get-WebListenerClientCertificate' + 'Get-WebListenerUrl' + 'Start-WebListener' + 'Stop-WebListener' + ) +} diff --git a/test/tools/Modules/WebListener/WebListener.psm1 b/test/tools/Modules/WebListener/WebListener.psm1 new file mode 100644 index 00000000000..a9071547a41 --- /dev/null +++ b/test/tools/Modules/WebListener/WebListener.psm1 @@ -0,0 +1,135 @@ +Class WebListener +{ + [int]$HttpPort + [int]$HttpsPort + [System.Management.Automation.Job]$Job + + WebListener () { } + + [String] GetStatus() + { + return $This.Job.JobStateInfo.State + } +} + +[WebListener]$WebListener + +function Get-WebListener +{ + [CmdletBinding(ConfirmImpact = 'Low')] + [OutputType([WebListener])] + param() + + process + { + return [WebListener]$Script:WebListener + } +} + +function Start-WebListener +{ + [CmdletBinding(ConfirmImpact = 'Low')] + [OutputType([WebListener])] + param + ( + [ValidateRange(1,65535)] + [int]$HttpPort = 8083, + + [ValidateRange(1,65535)] + [int]$HttpsPort = 8084 + ) + + process + { + $runningListener = Get-WebListener + if ($null -ne $runningListener -and $runningListener.GetStatus() -eq 'Running') + { + return $runningListener + } + + $initTimeoutSeconds = 15 + $appDll = 'WebListener.dll' + $serverPfx = 'ServerCert.pfx' + $serverPfxPassword = 'password' + $initCompleteMessage = 'Now listening on' + + $serverPfxPath = Join-Path $MyInvocation.MyCommand.Module.ModuleBase $serverPfx + $timeOut = (get-date).AddSeconds($initTimeoutSeconds) + $Job = Start-Job { + $path = Split-Path -parent (get-command WebListener).Path + Push-Location $path + dotnet $using:appDll $using:serverPfxPath $using:serverPfxPassword $using:HttpPort $using:HttpsPort + } + $Script:WebListener = [WebListener]@{ + HttpPort = $HttpPort + HttpsPort = $HttpsPort + Job = $Job + } + # Wait until the app is running or until the initTimeoutSeconds have been reached + do + { + Start-Sleep -Milliseconds 100 + $initStatus = $Job.ChildJobs[0].Output | Out-String + $isRunning = $initStatus -match $initCompleteMessage + } + while (-not $isRunning -and (get-date) -lt $timeOut) + + if (-not $isRunning) + { + $Job | Stop-Job -PassThru | Receive-Job + $Job | Remove-Job + throw 'WebListener did not start before the timeout was reached.' + } + return $Script:WebListener + } +} + +function Stop-WebListener +{ + [CmdletBinding(ConfirmImpact = 'Low')] + [OutputType([Void])] + param() + + process + { + $Script:WebListener.job | Stop-Job -PassThru | Remove-Job + $Script:WebListener = $null + } +} + +function Get-WebListenerClientCertificate { + [CmdletBinding(ConfirmImpact = 'Low')] + [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])] + param() + process { + $pfxPath = Join-Path $MyInvocation.MyCommand.Module.ModuleBase 'ClientCert.pfx' + [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($pfxPath,'password') + } +} + +function Get-WebListenerUrl { + [CmdletBinding()] + [OutputType([Uri])] + param ( + [switch]$Https, + [String]$Test + ) + process { + $runningListener = Get-WebListener + if ($null -eq $runningListener -or $runningListener.GetStatus() -ne 'Running') + { + return $null + } + $Uri = [System.UriBuilder]::new() + $Uri.Host = 'localhost' + $Uri.Port = $runningListener.HttpPort + $Uri.Scheme = 'Http' + if ($Https.IsPresent) + { + $Uri.Port = $runningListener.HttpsPort + $Uri.Scheme = 'Https' + } + $Uri.Path = $Test + return [Uri]$Uri.ToString() + } +} diff --git a/test/tools/WebListener/Controllers/CertController.cs b/test/tools/WebListener/Controllers/CertController.cs new file mode 100644 index 00000000000..1acbf566020 --- /dev/null +++ b/test/tools/WebListener/Controllers/CertController.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using mvc.Models; + +namespace mvc.Controllers +{ + public class CertController : Controller + { + public JsonResult Index() + { + // X509Certificate2 objects do not serialize as JSON. Create a HashTable instead + Hashtable output = new Hashtable + { + {"Status", "FAILED"} + }; + if (null != HttpContext.Connection.ClientCertificate) + { + output = new Hashtable + { + {"Status" , "OK"}, + {"Thumbprint" , HttpContext.Connection.ClientCertificate.Thumbprint}, + {"Subject" , HttpContext.Connection.ClientCertificate.Subject}, + {"SubjectName" , HttpContext.Connection.ClientCertificate.SubjectName.Name}, + {"Issuer" , HttpContext.Connection.ClientCertificate.Issuer}, + {"IssuerName" , HttpContext.Connection.ClientCertificate.IssuerName.Name}, + {"NotAfter" , HttpContext.Connection.ClientCertificate.NotAfter}, + {"NotBefore" , HttpContext.Connection.ClientCertificate.NotBefore} + }; + } + return Json(output); + } + + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + } +} diff --git a/test/tools/WebListener/Controllers/HomeController.cs b/test/tools/WebListener/Controllers/HomeController.cs new file mode 100644 index 00000000000..2a8a7a3e29a --- /dev/null +++ b/test/tools/WebListener/Controllers/HomeController.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using mvc.Models; + +namespace mvc.Controllers +{ + public class HomeController : Controller + { + public IActionResult Index() + { + return View(); + } + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + } +} diff --git a/test/tools/WebListener/Models/ErrorViewModel.cs b/test/tools/WebListener/Models/ErrorViewModel.cs new file mode 100644 index 00000000000..3c4066e0ecb --- /dev/null +++ b/test/tools/WebListener/Models/ErrorViewModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace mvc.Models +{ + public class ErrorViewModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + } +} diff --git a/test/tools/WebListener/Program.cs b/test/tools/WebListener/Program.cs new file mode 100644 index 00000000000..124f28f7e8d --- /dev/null +++ b/test/tools/WebListener/Program.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + + + +namespace mvc +{ + public class Program + { + public static void Main(string[] args) + { + if (args.Count() != 4) + { + System.Console.WriteLine("Required: "); + Environment.Exit(1); + } + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder() + .UseStartup().UseKestrel(options => + { + options.Listen(IPAddress.Loopback, int.Parse(args[2])); + options.Listen(IPAddress.Loopback, int.Parse(args[3]), listenOptions => + { + var certificate = new X509Certificate2(args[0], args[1]); + HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions(); + httpsOption.SslProtocols = SslProtocols.Tls12; + httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate; + httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => {return true;}; + httpsOption.CheckCertificateRevocation = false; + httpsOption.ServerCertificate = certificate; + listenOptions.UseHttps(httpsOption); + }); + }) + .Build(); + } +} diff --git a/test/tools/WebListener/README.md b/test/tools/WebListener/README.md new file mode 100644 index 00000000000..e931b2c3a5b --- /dev/null +++ b/test/tools/WebListener/README.md @@ -0,0 +1,56 @@ +# WebListener App + +ASP.NET Core 2.0 app for testing HTTP and HTTPS Requests. The default page will return a list of available tests. + +# Run with `dotnet` + +``` +dotnet restore +dotnet publish --output bin --configuration Release +cd bin +dotnet WebListener.dll ServerCert.pfx password 8083 8084 +``` + +The test site can then be accessed via `http://localhost:8083/` or `https://localhost:8084/`. + +The `WebListener.dll` takes 4 arguments: + +* The path to the Server Certificate +* The Server Certificate Password +* The TCP Port to bind on for HTTP +* The TCP Port to bind on for HTTPS + +# Run With WebListener Module + +```powershell +Import-Module .\build.psm1 +Publish-PSTestTools +$Listener = Start-WebListener -HttpPort 8083 -HttpsPort 8084 +``` + +# Tests + +## /Cert/ + +Returns a JSON object containing the details of the Client Certificate if one is provided in the request. + +Response when certificate is provided in request: +```json +{ + "Status": "OK", + "IssuerName": "E=randd@adatum.com, CN=adatum.com, OU=R&D, O=A. Datum Corporation, L=Redmond, S=Washington, C=US", + "SubjectName": "E=randd@adatum.com, CN=adatum.com, OU=R&D, O=A. Datum Corporation, L=Redmond, S=Washington, C=US", + "NotAfter": "2044-12-26T12:16:46-06:00", + "Issuer": "E=randd@adatum.com, CN=adatum.com, OU=R&D, O=A. Datum Corporation, L=Redmond, S=Washington, C=US", + "Subject": "E=randd@adatum.com, CN=adatum.com, OU=R&D, O=A. Datum Corporation, L=Redmond, S=Washington, C=US", + "NotBefore": "2017-08-10T13:16:46-05:00", + "Thumbprint": "2DECF1348FF21B780F45D316A039B5EB4C6312F7" +} +``` + +Response when certificate is not provided in request: +```json +{ + "Status": "FAILED" +} +``` diff --git a/test/tools/WebListener/Startup.cs b/test/tools/WebListener/Startup.cs new file mode 100644 index 00000000000..7e6af5f7737 --- /dev/null +++ b/test/tools/WebListener/Startup.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace mvc +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + } + + app.UseStaticFiles(); + + app.UseMvc(routes => + { + routes.MapRoute( + name: "default", + template: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/test/tools/WebListener/Views/Home/Index.cshtml b/test/tools/WebListener/Views/Home/Index.cshtml new file mode 100644 index 00000000000..0007857f793 --- /dev/null +++ b/test/tools/WebListener/Views/Home/Index.cshtml @@ -0,0 +1,5 @@ +

Available Tests

+
    +
  • / - This page
  • +
  • /Cert/ - Client Certificate Details
  • +
diff --git a/test/tools/WebListener/Views/Shared/Error.cshtml b/test/tools/WebListener/Views/Shared/Error.cshtml new file mode 100644 index 00000000000..ec2ea6bd038 --- /dev/null +++ b/test/tools/WebListener/Views/Shared/Error.cshtml @@ -0,0 +1,22 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. +

diff --git a/test/tools/WebListener/Views/Shared/_Layout.cshtml b/test/tools/WebListener/Views/Shared/_Layout.cshtml new file mode 100644 index 00000000000..603b12341f4 --- /dev/null +++ b/test/tools/WebListener/Views/Shared/_Layout.cshtml @@ -0,0 +1 @@ +@RenderBody() diff --git a/test/tools/WebListener/Views/_ViewImports.cshtml b/test/tools/WebListener/Views/_ViewImports.cshtml new file mode 100644 index 00000000000..861be0a31ac --- /dev/null +++ b/test/tools/WebListener/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using mvc +@using mvc.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/test/tools/WebListener/Views/_ViewStart.cshtml b/test/tools/WebListener/Views/_ViewStart.cshtml new file mode 100644 index 00000000000..a5f10045db9 --- /dev/null +++ b/test/tools/WebListener/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/test/tools/WebListener/WebListener.csproj b/test/tools/WebListener/WebListener.csproj new file mode 100644 index 00000000000..d95182c68ce --- /dev/null +++ b/test/tools/WebListener/WebListener.csproj @@ -0,0 +1,16 @@ + + + + A simple ASP.NET Core 2.0 MVC app to provide an HTTP and HTTPS server for testing. + netcoreapp2.0 + + + + + + + + + + + diff --git a/test/tools/WebListener/appsettings.Development.json b/test/tools/WebListener/appsettings.Development.json new file mode 100644 index 00000000000..fa8ce71a97a --- /dev/null +++ b/test/tools/WebListener/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/test/tools/WebListener/appsettings.json b/test/tools/WebListener/appsettings.json new file mode 100644 index 00000000000..5fff67bacc4 --- /dev/null +++ b/test/tools/WebListener/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + } +}