diff --git a/.editorconfig b/.editorconfig index f7a39e1..a06e4ea 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,12 +21,15 @@ indent_size = 2 [*.cs] # Code style defaults -csharp_using_directive_placement = outside_namespace:suggestion +csharp_using_directive_placement = outside_namespace:error +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion dotnet_sort_system_directives_first = true dotnet_style_readonly_field = true:suggestion +csharp_style_namespace_declarations = file_scoped:error # License header file_header_template = Copyright (c) Microsoft Corporation.\nLicensed under the MIT License. +dotnet_diagnostic.IDE0073.severity = error # Suggest more modern language features when available dotnet_style_object_initializer = true:suggestion @@ -42,9 +45,17 @@ dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion dotnet_style_prefer_conditional_expression_over_return = true:suggestion csharp_prefer_simple_default_expression = true:suggestion +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_switch_expression = false:none +csharp_style_prefer_pattern_matching = false:none + # Prefer "var" everywhere csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion csharp_style_var_elsewhere = true:suggestion # Define the 'private_fields' symbol group: diff --git a/.github/workflows/analyze.yaml b/.github/workflows/analyze.yaml index d861a1e..3f2bc6f 100644 --- a/.github/workflows/analyze.yaml +++ b/.github/workflows/analyze.yaml @@ -12,29 +12,52 @@ name: Analyze on: push: - branches: [ main, 'release/*' ] + branches: [main, 'release/*'] pull_request: - branches: [ main, 'release/*' ] + branches: [main, 'release/*'] schedule: - - cron: '51 20 * * 0' # At 08:51 PM, on Sunday each week + - cron: '24 22 * * 0' # At 10:24 PM, on Sunday each week workflow_dispatch: +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +permissions: {} + jobs: oss: name: Analyze with PSRule runs-on: ubuntu-latest permissions: contents: read + security-events: write steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Checkout - uses: actions/checkout@v3 + - name: Run PSRule analysis + uses: microsoft/ps-rule@v2.9.0 + with: + modules: PSRule.Rules.MSFT.OSS + prerelease: true + outputFormat: Sarif + outputPath: reports/ps-rule-results.sarif - - name: Run PSRule analysis - uses: microsoft/ps-rule@v2.9.0 - with: - modules: PSRule.Rules.MSFT.OSS - prerelease: true + - name: Upload results to security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: reports/ps-rule-results.sarif + + - name: Upload results + uses: actions/upload-artifact@v4 + if: always() + with: + name: PSRule-Sarif + path: reports/ps-rule-results.sarif + retention-days: 1 + if-no-files-found: error devskim: name: Analyze with DevSkim @@ -44,20 +67,29 @@ jobs: contents: read security-events: write steps: - - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run DevSkim scanner uses: microsoft/DevSkim-Action@v1 with: - directory-to-scan: src/ + directory-to-scan: . - name: Upload results to security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 + if: always() with: sarif_file: devskim-results.sarif + - name: Upload results + uses: actions/upload-artifact@v4 + if: always() + with: + name: DevSkim-Sarif + path: devskim-results.sarif + retention-days: 1 + if-no-files-found: error + codeql: name: Analyze with CodeQL runs-on: ubuntu-latest @@ -66,17 +98,26 @@ jobs: contents: read security-events: write steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Checkout - uses: actions/checkout@v3 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: 'csharp' - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: 'csharp' + - name: Autobuild + uses: github/codeql-action/autobuild@v3 - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + id: codeql-analyze - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: Upload results + uses: actions/upload-artifact@v4 + if: always() + with: + name: CodeQL-Sarif + path: ${{ steps.codeql-analyze.outputs.sarif-output }} + retention-days: 1 + if-no-files-found: error diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..a8e2100 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,143 @@ +# +# CI Pipeline +# + +# NOTES: +# This workflow builds and tests module updates. + +name: Build +on: + push: + branches: [main, 'release/*'] + pull_request: + branches: [main, 'release/*'] + workflow_dispatch: + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +permissions: {} + +jobs: + build: + name: Build + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.x + + - name: Install dependencies + shell: pwsh + timeout-minutes: 3 + run: ./scripts/pipeline-deps.ps1 + + - name: Build module + shell: pwsh + timeout-minutes: 5 + run: Invoke-Build -Configuration Release -AssertStyle GitHubActions + + - name: Upload module + uses: actions/upload-artifact@v4 + with: + name: Module + path: ./out/modules/PSRule/* + retention-days: 3 + if-no-files-found: error + + # - name: Upload Test Results + # uses: actions/upload-artifact@v3 + # if: always() + # with: + # name: Module.DotNet.TestResults + # path: ./reports/*.trx + # retention-days: 3 + # if-no-files-found: error + + - name: Upload PSRule Results + uses: actions/upload-artifact@v4 + if: always() + with: + name: Results-PSRule + path: ./reports/ps-rule*.xml + retention-days: 3 + if-no-files-found: error + + test: + name: Test (${{ matrix.rid }}-${{ matrix.shell }}) + runs-on: ${{ matrix.os }} + needs: build + permissions: + contents: read + + strategy: + # Get full test results from all platforms. + fail-fast: false + + matrix: + os: ['ubuntu-latest'] + rid: ['linux-x64'] + shell: ['pwsh'] + include: + - os: windows-latest + rid: win-x64 + shell: pwsh + - os: windows-latest + rid: win-x64 + shell: powershell + - os: ubuntu-latest + rid: linux-x64 + shell: pwsh + - os: ubuntu-latest + rid: linux-musl-x64 + shell: pwsh + - os: macos-latest + rid: osx-x64 + shell: pwsh + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.x + + - if: ${{ matrix.shell == 'pwsh' }} + name: Install dependencies (PowerShell) + shell: pwsh + timeout-minutes: 3 + run: ./scripts/pipeline-deps.ps1 + + - if: ${{ matrix.shell == 'powershell' }} + name: Install dependencies (Windows PowerShell) + shell: powershell + timeout-minutes: 3 + run: ./scripts/pipeline-deps.ps1 + + - name: Download module + uses: actions/download-artifact@v4 + with: + name: Module + path: ./out/modules/PSRule + + - if: ${{ matrix.shell == 'pwsh' }} + name: Test module (PowerShell) + shell: pwsh + timeout-minutes: 15 + run: Invoke-Build TestModule -Configuration Release -AssertStyle GitHubActions + + - if: ${{ matrix.shell == 'powershell' }} + name: Test module (Windows PowerShell) + shell: powershell + timeout-minutes: 30 + run: Invoke-Build TestModule -Configuration Release -AssertStyle GitHubActions diff --git a/.github/workflows/dependencies.yaml b/.github/workflows/dependencies.yaml index 4c78074..be1ab49 100644 --- a/.github/workflows/dependencies.yaml +++ b/.github/workflows/dependencies.yaml @@ -8,12 +8,14 @@ name: Dependencies on: schedule: - - cron: '45 1 * * 1' # At 01:45 AM, on Monday each week + - cron: '30 1 * * 1' # At 01:30 AM, on Monday each week workflow_dispatch: env: WORKING_BRANCH: dependencies/powershell-bump +permissions: {} + jobs: dependencies: name: Bump dependencies @@ -23,9 +25,8 @@ jobs: contents: write pull-requests: write steps: - - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/first-interaction.yaml b/.github/workflows/first-interaction.yaml new file mode 100644 index 0000000..d7d2f70 --- /dev/null +++ b/.github/workflows/first-interaction.yaml @@ -0,0 +1,27 @@ +# +# Stale item management +# + +# NOTES: +# This workflow greets a person for their a first issue or PR. + +name: First interaction + +on: [pull_request_target, issues] + +permissions: {} + +jobs: + greeting: + name: Greeting + runs-on: ubuntu-latest + if: github.repository == 'microsoft/PSRule.Monitor' + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: 'Thanks for raising your first issue, the team appreciates the time you have taken 😉' + pr-message: 'Thank you for your contribution, one of the team will evaluate shortly.' diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 0000000..96d0f80 --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,40 @@ +# +# Stale item management +# + +# NOTES: +# This workflow manages stale work items on the repository. + +name: Stale maintenance +on: + schedule: + - cron: '30 1 * * *' + workflow_dispatch: + +permissions: {} + +jobs: + issue: + name: Close stale issues + runs-on: ubuntu-latest + if: github.repository == 'microsoft/PSRule.Monitor' + permissions: + issues: write + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs within 7 days. + Thank you for your contributions. + + close-issue-message: 'This issue was closed because it has not had any recent activity.' + + days-before-stale: 14 + days-before-pr-stale: -1 + + days-before-close: 7 + days-before-pr-close: -1 + + any-of-labels: 'question,duplicate,incomplete,waiting-feedback' + stale-issue-label: stale diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e022f4..c2cb50f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +What's changed since v0.6.0: + +- Engineering: + - Updated to .NET 8.0 for build process and testing. + [#150](https://github.com/microsoft/PSRule.Monitor/issues/150) + ## v0.6.0 What's changed since v0.5.0: diff --git a/pipeline.build.ps1 b/pipeline.build.ps1 index bd3ebc1..f381c5c 100644 --- a/pipeline.build.ps1 +++ b/pipeline.build.ps1 @@ -162,13 +162,13 @@ task TestDotNet { if ($CodeCoverage) { exec { # Test library - dotnet test --collect:"Code Coverage" --logger trx -r (Join-Path $PWD -ChildPath reports/) tests/PSRule.Monitor.Tests + dotnet test -f net8.0 --collect:"Code Coverage" --logger trx -r (Join-Path $PWD -ChildPath reports/) tests/PSRule.Monitor.Tests } } else { exec { # Test library - dotnet test --logger trx -r (Join-Path $PWD -ChildPath reports/) tests/PSRule.Monitor.Tests + dotnet test -f net8.0 --logger trx -r (Join-Path $PWD -ChildPath reports/) tests/PSRule.Monitor.Tests } } } diff --git a/.azure-pipelines/pipeline-deps.ps1 b/scripts/pipeline-deps.ps1 similarity index 100% rename from .azure-pipelines/pipeline-deps.ps1 rename to scripts/pipeline-deps.ps1 diff --git a/src/PSRule.Common.props b/src/PSRule.Common.props index 3035f26..dbf2d2b 100644 --- a/src/PSRule.Common.props +++ b/src/PSRule.Common.props @@ -3,7 +3,8 @@ netstandard2.0 - 9.0 + 12.0 + en-US true portable @@ -30,7 +31,8 @@ © Microsoft Corporation. All rights reserved. Log PSRule analysis results to Azure Monitor. -This project uses GitHub Issues to track bugs and feature requests. See GitHub project for more information. + This project uses GitHub Issues to track bugs and feature requests. See GitHub project for + more information. For a list of changes see https://aka.ms/ps-rule-monitor/changelog. package_icon.png @@ -64,4 +66,4 @@ This project uses GitHub Issues to track bugs and feature requests. See GitHub p \ - + \ No newline at end of file diff --git a/src/PSRule.Monitor/Common/JsonConverters.cs b/src/PSRule.Monitor/Common/JsonConverters.cs index 017954b..99cd0f6 100644 --- a/src/PSRule.Monitor/Common/JsonConverters.cs +++ b/src/PSRule.Monitor/Common/JsonConverters.cs @@ -1,34 +1,33 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Newtonsoft.Json; using System; using System.Collections; +using Newtonsoft.Json; -namespace PSRule.Monitor +namespace PSRule.Monitor; + +/// +/// A JSON converter to convert an object into a flat string. +/// +internal sealed class StringifyMapConverter : JsonConverter { - /// - /// A JSON converter to convert an object into a flat string. - /// - internal sealed class StringifyMapConverter : JsonConverter + public override bool CanConvert(Type objectType) { - public override bool CanConvert(Type objectType) - { - return typeof(Hashtable).IsAssignableFrom(objectType); - } + return typeof(Hashtable).IsAssignableFrom(objectType); + } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - throw new NotImplementedException(); - } + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (!(value is Hashtable map)) - return; + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is not Hashtable map) + return; - var v = JsonConvert.SerializeObject(map); - writer.WriteValue(v); - } + var v = JsonConvert.SerializeObject(map); + writer.WriteValue(v); } } diff --git a/src/PSRule.Monitor/Common/SecureStringAttribute.cs b/src/PSRule.Monitor/Common/SecureStringAttribute.cs index 1112d38..3e506be 100644 --- a/src/PSRule.Monitor/Common/SecureStringAttribute.cs +++ b/src/PSRule.Monitor/Common/SecureStringAttribute.cs @@ -5,32 +5,31 @@ using System.Net; using System.Security; -namespace PSRule.Monitor +namespace PSRule.Monitor; + +/// +/// A parameter transformation attribute for converting a string to a secure string. +/// +public sealed class SecureStringAttribute : ArgumentTransformationAttribute { - /// - /// A parameter transformation attribute for converting a string to a secure string. - /// - public sealed class SecureStringAttribute : ArgumentTransformationAttribute - { - public SecureStringAttribute() - : base() { } + public SecureStringAttribute() + : base() { } - public override bool TransformNullOptionalParameters => false; + public override bool TransformNullOptionalParameters => false; - public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) - { - return TrySecureString(inputData, out SecureString s) || (inputData is PSObject pso && TrySecureString(pso, out s)) ? s : null; - } + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + return TrySecureString(inputData, out var s) || (inputData is PSObject pso && TrySecureString(pso, out s)) ? s : null; + } - private static bool TrySecureString(object o, out SecureString value) + private static bool TrySecureString(object o, out SecureString value) + { + value = null; + if (o is string s) { - value = null; - if (o is string s) - { - value = new NetworkCredential("na", s).SecurePassword; - return true; - } - return false; + value = new NetworkCredential("na", s).SecurePassword; + return true; } + return false; } } diff --git a/src/PSRule.Monitor/Configuration/PSRuleOption.cs b/src/PSRule.Monitor/Configuration/PSRuleOption.cs index bb43605..5bc5630 100644 --- a/src/PSRule.Monitor/Configuration/PSRuleOption.cs +++ b/src/PSRule.Monitor/Configuration/PSRuleOption.cs @@ -4,50 +4,49 @@ using System.IO; using System.Management.Automation; -namespace PSRule.Monitor.Configuration +namespace PSRule.Monitor.Configuration; + +/// +/// A delegate to allow callback to PowerShell to get current working path. +/// +internal delegate string PathDelegate(); + +public sealed class PSRuleOption { /// - /// A delgate to allow callback to PowerShell to get current working path. + /// A callback that is overridden by PowerShell so that the current working path can be retrieved. /// - internal delegate string PathDelegate(); + private static PathDelegate _GetWorkingPath = () => Directory.GetCurrentDirectory(); - public sealed class PSRuleOption + /// + /// Set working path from PowerShell host environment. + /// + /// An $ExecutionContext object. + /// + /// Called from PowerShell. + /// + public static void UseExecutionContext(EngineIntrinsics executionContext) { - /// - /// A callback that is overridden by PowerShell so that the current working path can be retrieved. - /// - private static PathDelegate _GetWorkingPath = () => Directory.GetCurrentDirectory(); - - /// - /// Set working path from PowerShell host environment. - /// - /// An $ExecutionContext object. - /// - /// Called from PowerShell. - /// - public static void UseExecutionContext(EngineIntrinsics executionContext) + if (executionContext == null) { - if (executionContext == null) - { - _GetWorkingPath = () => Directory.GetCurrentDirectory(); - return; - } - _GetWorkingPath = () => executionContext.SessionState.Path.CurrentFileSystemLocation.Path; + _GetWorkingPath = () => Directory.GetCurrentDirectory(); + return; } + _GetWorkingPath = () => executionContext.SessionState.Path.CurrentFileSystemLocation.Path; + } - public static string GetWorkingPath() - { - return _GetWorkingPath(); - } + public static string GetWorkingPath() + { + return _GetWorkingPath(); + } - /// - /// Get a full path instead of a relative path that may be passed from PowerShell. - /// - /// - /// - internal static string GetRootedPath(string path) - { - return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(GetWorkingPath(), path)); - } + /// + /// Get a full path instead of a relative path that may be passed from PowerShell. + /// + /// + /// + internal static string GetRootedPath(string path) + { + return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(GetWorkingPath(), path)); } } diff --git a/src/PSRule.Monitor/Data/LogRecord.cs b/src/PSRule.Monitor/Data/LogRecord.cs index 293be8e..454e299 100644 --- a/src/PSRule.Monitor/Data/LogRecord.cs +++ b/src/PSRule.Monitor/Data/LogRecord.cs @@ -1,47 +1,46 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Newtonsoft.Json; using System; using System.Collections; +using Newtonsoft.Json; + +namespace PSRule.Monitor.Data; -namespace PSRule.Monitor.Data +/// +/// An Azure Monitor log record. +/// +internal sealed class LogRecord { - /// - /// An Azure Monitor log record. - /// - internal sealed class LogRecord - { - public string RuleId { get; set; } + public string RuleId { get; set; } - public string RuleName { get; set; } + public string RuleName { get; set; } - public string DisplayName { get; set; } + public string DisplayName { get; set; } - public string ModuleName { get; set; } + public string ModuleName { get; set; } - public string TargetName { get; set; } + public string TargetName { get; set; } - public string TargetType { get; set; } + public string TargetType { get; set; } - public string Outcome { get; set; } + public string Outcome { get; set; } - [JsonIgnore] - public string ResourceId { get; set; } + [JsonIgnore] + public string ResourceId { get; set; } - [JsonConverter(typeof(StringifyMapConverter))] - public Hashtable Field { get; set; } + [JsonConverter(typeof(StringifyMapConverter))] + public Hashtable Field { get; set; } - [JsonConverter(typeof(StringifyMapConverter))] - public Hashtable Data { get; set; } + [JsonConverter(typeof(StringifyMapConverter))] + public Hashtable Data { get; set; } - [JsonConverter(typeof(StringifyMapConverter))] - public Hashtable Annotations { get; set; } + [JsonConverter(typeof(StringifyMapConverter))] + public Hashtable Annotations { get; set; } - public string RunId { get; set; } + public string RunId { get; set; } - public Guid CorrelationId { get; set; } + public Guid CorrelationId { get; set; } - public long Duration { get; set; } - } + public long Duration { get; set; } } diff --git a/src/PSRule.Monitor/Pipeline/BatchQueue.cs b/src/PSRule.Monitor/Pipeline/BatchQueue.cs index 9296fd8..522b5ff 100644 --- a/src/PSRule.Monitor/Pipeline/BatchQueue.cs +++ b/src/PSRule.Monitor/Pipeline/BatchQueue.cs @@ -1,54 +1,53 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Monitor.Data; using System.Collections.Concurrent; using System.Collections.Generic; +using PSRule.Monitor.Data; + +namespace PSRule.Monitor.Pipeline; -namespace PSRule.Monitor.Pipeline +internal sealed class BatchQueue { - internal sealed class BatchQueue + private readonly ConcurrentQueue _Queue; + + public BatchQueue() + { + _Queue = new ConcurrentQueue(); + } + + public int Count + { + get { return _Queue.Count; } + } + + public bool IsEmpty + { + get { return _Queue.IsEmpty; } + } + + public void Enqueue(LogRecord record) + { + if (record == null) + return; + + _Queue.Enqueue(record); + } + + public bool TryDequeue(int minSize, int maxSize, out LogRecord[] records) { - private readonly ConcurrentQueue _Queue; - - public BatchQueue() - { - _Queue = new ConcurrentQueue(); - } - - public int Count - { - get { return _Queue.Count; } - } - - public bool IsEmpty - { - get { return _Queue.IsEmpty; } - } - - public void Enqueue(LogRecord record) - { - if (record == null) - return; - - _Queue.Enqueue(record); - } - - public bool TryDequeue(int minSize, int maxSize, out LogRecord[] records) - { - records = null; - if (_Queue.Count < minSize || _Queue.IsEmpty) - return false; - - string resourceId = _Queue.TryPeek(out LogRecord record) ? record.ResourceId : null; - var batchSize = _Queue.Count > maxSize ? maxSize : _Queue.Count; - var batch = new List(batchSize); - for (var i = 0; i < maxSize && !_Queue.IsEmpty && _Queue.TryPeek(out record) && record.ResourceId == resourceId; i++) - if (_Queue.TryDequeue(out record)) - batch.Add(record); - - records = batch.ToArray(); - return true; - } + records = null; + if (_Queue.Count < minSize || _Queue.IsEmpty) + return false; + + var resourceId = _Queue.TryPeek(out var record) ? record.ResourceId : null; + var batchSize = _Queue.Count > maxSize ? maxSize : _Queue.Count; + var batch = new List(batchSize); + for (var i = 0; i < maxSize && !_Queue.IsEmpty && _Queue.TryPeek(out record) && record.ResourceId == resourceId; i++) + if (_Queue.TryDequeue(out record)) + batch.Add(record); + + records = batch.ToArray(); + return true; } } diff --git a/src/PSRule.Monitor/Pipeline/CollectionHash.cs b/src/PSRule.Monitor/Pipeline/CollectionHash.cs index 6b4a6c3..c7c81ec 100644 --- a/src/PSRule.Monitor/Pipeline/CollectionHash.cs +++ b/src/PSRule.Monitor/Pipeline/CollectionHash.cs @@ -8,56 +8,55 @@ using System.Security.Cryptography; using System.Text; -namespace PSRule.Monitor.Pipeline +namespace PSRule.Monitor.Pipeline; + +internal sealed class CollectionHash : IDisposable { - internal sealed class CollectionHash : IDisposable - { - private readonly string _WorkspaceId; - private readonly HMACSHA256 _Algorithm; + private readonly string _WorkspaceId; + private readonly HMACSHA256 _Algorithm; - private static readonly CultureInfo FormatCulture = new CultureInfo("en-US"); + private static readonly CultureInfo FormatCulture = new("en-US"); - internal CollectionHash(string workspaceId, SecureString sharedKey) - { - _WorkspaceId = workspaceId; - _Algorithm = new HMACSHA256(Convert.FromBase64String(new NetworkCredential(string.Empty, sharedKey).Password)); - } + internal CollectionHash(string workspaceId, SecureString sharedKey) + { + _WorkspaceId = workspaceId; + _Algorithm = new HMACSHA256(Convert.FromBase64String(new NetworkCredential(string.Empty, sharedKey).Password)); + } - internal string ComputeSignature(int length, DateTime date, string contentType) - { - var challenge = string.Concat("POST\n", length, "\n", contentType, "; charset=utf-8\n", "x-ms-date:", date.ToString("r", FormatCulture), "\n/api/logs"); - return string.Concat("SharedKey ", _WorkspaceId, ":", ComputeHash(challenge)); - } + internal string ComputeSignature(int length, DateTime date, string contentType) + { + var challenge = string.Concat("POST\n", length, "\n", contentType, "; charset=utf-8\n", "x-ms-date:", date.ToString("r", FormatCulture), "\n/api/logs"); + return string.Concat("SharedKey ", _WorkspaceId, ":", ComputeHash(challenge)); + } - private string ComputeHash(string challenge) - { - byte[] challengeBytes = Encoding.UTF8.GetBytes(challenge); - byte[] hash = _Algorithm.ComputeHash(challengeBytes); - return Convert.ToBase64String(hash); - } + private string ComputeHash(string challenge) + { + var challengeBytes = Encoding.UTF8.GetBytes(challenge); + var hash = _Algorithm.ComputeHash(challengeBytes); + return Convert.ToBase64String(hash); + } - #region IDisposable + #region IDisposable - private bool _Disposed; // To detect redundant calls + private bool _Disposed; // To detect redundant calls - void Dispose(bool disposing) + void Dispose(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) + if (disposing) { - if (disposing) - { - _Algorithm.Dispose(); - } - _Disposed = true; + _Algorithm.Dispose(); } + _Disposed = true; } + } - // This code added to correctly implement the disposable pattern. - public void Dispose() - { - Dispose(true); - } - - #endregion IDisposable + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + Dispose(true); } + + #endregion IDisposable } diff --git a/src/PSRule.Monitor/Pipeline/Exceptions.cs b/src/PSRule.Monitor/Pipeline/Exceptions.cs index 601e59f..c699b05 100644 --- a/src/PSRule.Monitor/Pipeline/Exceptions.cs +++ b/src/PSRule.Monitor/Pipeline/Exceptions.cs @@ -6,92 +6,91 @@ using System.Runtime.Serialization; using System.Security.Permissions; -namespace PSRule.Monitor.Pipeline +namespace PSRule.Monitor.Pipeline; + +/// +/// A base class for all pipeline exceptions. +/// +public abstract class PipelineException : Exception { - /// - /// A base class for all pipeline exceptions. - /// - public abstract class PipelineException : Exception - { - protected PipelineException() - : base() { } + protected PipelineException() + : base() { } - protected PipelineException(string message) - : base(message) { } + protected PipelineException(string message) + : base(message) { } - protected PipelineException(string message, Exception innerException) - : base(message, innerException) { } + protected PipelineException(string message, Exception innerException) + : base(message, innerException) { } - protected PipelineException(SerializationInfo info, StreamingContext context) - : base(info, context) { } - } + protected PipelineException(SerializationInfo info, StreamingContext context) + : base(info, context) { } +} - /// - /// A base class for runtime exceptions. - /// - public abstract class RuntimeException : PipelineException - { - protected RuntimeException() - : base() { } +/// +/// A base class for runtime exceptions. +/// +public abstract class RuntimeException : PipelineException +{ + protected RuntimeException() + : base() { } + + protected RuntimeException(string message) + : base(message) { } + + protected RuntimeException(string message, Exception innerException) + : base(message, innerException) { } - protected RuntimeException(string message) - : base(message) { } + protected RuntimeException(Exception innerException, InvocationInfo invocationInfo, string ruleId) + : base(innerException?.Message, innerException) + { + CommandInvocation = invocationInfo; + RuleId = ruleId; + } - protected RuntimeException(string message, Exception innerException) - : base(message, innerException) { } + protected RuntimeException(SerializationInfo info, StreamingContext context) + : base(info, context) { } - protected RuntimeException(Exception innerException, InvocationInfo invocationInfo, string ruleId) - : base(innerException?.Message, innerException) - { - CommandInvocation = invocationInfo; - RuleId = ruleId; - } + public InvocationInfo CommandInvocation { get; } - protected RuntimeException(SerializationInfo info, StreamingContext context) - : base(info, context) { } + public string RuleId { get; } +} - public InvocationInfo CommandInvocation { get; } +/// +/// An exception when building the pipeline. +/// +[Serializable] +public sealed class PipelineBuilderException : PipelineException +{ + /// + /// Creates a pipeline builder exception. + /// + public PipelineBuilderException() + : base() { } - public string RuleId { get; } - } + /// + /// Creates a pipeline builder exception. + /// + /// The detail of the exception. + public PipelineBuilderException(string message) + : base(message) { } /// - /// An exception when building the pipeline. + /// Creates a pipeline builder exception. /// - [Serializable] - public sealed class PipelineBuilderException : PipelineException + /// The detail of the exception. + /// A nested exception that caused the issue. + public PipelineBuilderException(string message, Exception innerException) + : base(message, innerException) { } + + private PipelineBuilderException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) { - /// - /// Creates a pipeline builder exception. - /// - public PipelineBuilderException() - : base() { } - - /// - /// Creates a pipeline builder exception. - /// - /// The detail of the exception. - public PipelineBuilderException(string message) - : base(message) { } - - /// - /// Creates a pipeline builder exception. - /// - /// The detail of the exception. - /// A nested exception that caused the issue. - public PipelineBuilderException(string message, Exception innerException) - : base(message, innerException) { } - - private PipelineBuilderException(SerializationInfo info, StreamingContext context) - : base(info, context) { } - - [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - throw new ArgumentNullException(nameof(info)); - - base.GetObjectData(info, context); - } + if (info == null) + throw new ArgumentNullException(nameof(info)); + + base.GetObjectData(info, context); } } diff --git a/src/PSRule.Monitor/Pipeline/InjestPipeline.cs b/src/PSRule.Monitor/Pipeline/InjestPipeline.cs index 9f264fa..55cdc62 100644 --- a/src/PSRule.Monitor/Pipeline/InjestPipeline.cs +++ b/src/PSRule.Monitor/Pipeline/InjestPipeline.cs @@ -1,99 +1,98 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Monitor.Resources; using System.Management.Automation; using System.Security; using System.Threading; +using PSRule.Monitor.Resources; -namespace PSRule.Monitor.Pipeline -{ - public interface IInjestPipelineBuilder : IPipelineBuilder - { - void WorkspaceId(string workspaceId); +namespace PSRule.Monitor.Pipeline; - void SharedKey(SecureString sharedKey); +public interface IInjestPipelineBuilder : IPipelineBuilder +{ + void WorkspaceId(string workspaceId); - void LogName(string logName); - } + void SharedKey(SecureString sharedKey); - internal sealed class InjestPipelineBuilder : PipelineBuilderBase, IInjestPipelineBuilder - { - private const string DEFAULT_LOGNAME = "PSRule"; + void LogName(string logName); +} - private string _WorkspaceId; - private SecureString _SharedKey; - private string _LogName = DEFAULT_LOGNAME; +internal sealed class InjestPipelineBuilder : PipelineBuilderBase, IInjestPipelineBuilder +{ + private const string DEFAULT_LOGNAME = "PSRule"; - public void WorkspaceId(string workspaceId) - { - if (string.IsNullOrEmpty(workspaceId)) - throw new PipelineBuilderException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.InvalidWorkspaceId)); + private string _WorkspaceId; + private SecureString _SharedKey; + private string _LogName = DEFAULT_LOGNAME; - _WorkspaceId = workspaceId; - } + public void WorkspaceId(string workspaceId) + { + if (string.IsNullOrEmpty(workspaceId)) + throw new PipelineBuilderException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.InvalidWorkspaceId)); - public void SharedKey(SecureString sharedKey) - { - if (sharedKey == null) - throw new PipelineBuilderException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.InvalidSharedKey)); + _WorkspaceId = workspaceId; + } - _SharedKey = sharedKey; - } + public void SharedKey(SecureString sharedKey) + { + if (sharedKey == null) + throw new PipelineBuilderException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.InvalidSharedKey)); - public void LogName(string logName) - { - _LogName = logName; - } + _SharedKey = sharedKey; + } - public override IPipeline Build() - { - var logClient = new LogClient(_WorkspaceId, _LogName); - return new InjestPipeline(PrepareContext(), PrepareReader(), _WorkspaceId, _SharedKey, logClient); - } + public void LogName(string logName) + { + _LogName = logName; } - internal sealed class InjestPipeline : PipelineBase + public override IPipeline Build() { - private readonly WorkspaceClient _WorkspaceClient; + var logClient = new LogClient(_WorkspaceId, _LogName); + return new InjestPipeline(PrepareContext(), PrepareReader(), _WorkspaceId, _SharedKey, logClient); + } +} - // Track whether Dispose has been called. - private bool _Disposed; +internal sealed class InjestPipeline : PipelineBase +{ + private readonly WorkspaceClient _WorkspaceClient; - internal InjestPipeline(PipelineContext context, PipelineReader reader, string workspaceId, SecureString sharedKey, ILogClient logClient) - : base(context, reader) - { - _WorkspaceClient = new WorkspaceClient(workspaceId, sharedKey, logClient); - } + // Track whether Dispose has been called. + private bool _Disposed; - public override void Process(PSObject sourceObject) - { - Reader.Enqueue(sourceObject); - while (Reader.TryDequeue(out PSObject next)) - _WorkspaceClient.Enqueue(next); + internal InjestPipeline(PipelineContext context, PipelineReader reader, string workspaceId, SecureString sharedKey, ILogClient logClient) + : base(context, reader) + { + _WorkspaceClient = new WorkspaceClient(workspaceId, sharedKey, logClient); + } - _WorkspaceClient.Send(30, 100); - } + public override void Process(PSObject sourceObject) + { + Reader.Enqueue(sourceObject); + while (Reader.TryDequeue(out var next)) + _WorkspaceClient.Enqueue(next); - public override void End() - { - _WorkspaceClient.Send(); - } + _WorkspaceClient.Send(30, 100); + } + + public override void End() + { + _WorkspaceClient.Send(); + } - #region IDisposable + #region IDisposable - protected override void Dispose(bool disposing) + protected override void Dispose(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) - { - if (disposing) - _WorkspaceClient.Dispose(); - - _Disposed = true; - } - base.Dispose(disposing); - } + if (disposing) + _WorkspaceClient.Dispose(); - #endregion IDisposable + _Disposed = true; + } + base.Dispose(disposing); } + + #endregion IDisposable } diff --git a/src/PSRule.Monitor/Pipeline/LogClient.cs b/src/PSRule.Monitor/Pipeline/LogClient.cs index f96d952..3265b4f 100644 --- a/src/PSRule.Monitor/Pipeline/LogClient.cs +++ b/src/PSRule.Monitor/Pipeline/LogClient.cs @@ -6,92 +6,89 @@ using System.Net.Http; using System.Text; -namespace PSRule.Monitor.Pipeline +namespace PSRule.Monitor.Pipeline; + +internal interface ILogClient : IDisposable { - internal interface ILogClient : IDisposable - { - void Post(string signature, DateTime date, string resourceId, string json); - } + void Post(string signature, DateTime date, string resourceId, string json); +} - internal sealed class LogClient : ILogClient - { - private const string CONTENTTYPE = "application/json"; - private const string TIMESTAMPFIELD = ""; - private const string APIVERSION = "2016-04-01"; +internal sealed class LogClient : ILogClient +{ + private const string CONTENTTYPE = "application/json"; + private const string TIMESTAMPFIELD = ""; + private const string APIVERSION = "2016-04-01"; - private const string HEADER_ACCEPT = "Accept"; - private const string HEADER_AUTHORIZATION = "Authorization"; - private const string HEADER_LOGTYPE = "Log-Type"; - private const string HEADER_DATE = "x-ms-date"; - private const string HEADER_RESOURCEID = "x-ms-AzureResourceId"; - private const string HEADER_TIMEGENERATED = "time-generated-field"; + private const string HEADER_ACCEPT = "Accept"; + private const string HEADER_AUTHORIZATION = "Authorization"; + private const string HEADER_LOGTYPE = "Log-Type"; + private const string HEADER_DATE = "x-ms-date"; + private const string HEADER_RESOURCEID = "x-ms-AzureResourceId"; + private const string HEADER_TIMEGENERATED = "time-generated-field"; - private static readonly CultureInfo FormatCulture = new CultureInfo("en-US"); + private static readonly CultureInfo FormatCulture = new("en-US"); - private readonly HttpClient _HttpClient; - private readonly Uri _EndpointUri; + private readonly HttpClient _HttpClient; + private readonly Uri _EndpointUri; - // Track whether Dispose has been called. - private bool _Disposed; + // Track whether Dispose has been called. + private bool _Disposed; - public LogClient(string workspaceId, string logName) - { - _EndpointUri = new Uri(string.Concat("https://", workspaceId, ".ods.opinsights.azure.com/api/logs?api-version=", APIVERSION)); - _HttpClient = GetClient(logName); - } + public LogClient(string workspaceId, string logName) + { + _EndpointUri = new Uri(string.Concat("https://", workspaceId, ".ods.opinsights.azure.com/api/logs?api-version=", APIVERSION)); + _HttpClient = GetClient(logName); + } - /// - /// Post log data to Azure Monitor endpoint. - /// - public void Post(string signature, DateTime date, string resourceId, string json) - { - using (var request = PrepareRequest(signature, date, resourceId, json)) - { - var response = _HttpClient.SendAsync(request); - response.Wait(); - var result = response.Result.Content.ReadAsStringAsync().Result; - } - } + /// + /// Post log data to Azure Monitor endpoint. + /// + public void Post(string signature, DateTime date, string resourceId, string json) + { + using var request = PrepareRequest(signature, date, resourceId, json); + var response = _HttpClient.SendAsync(request); + response.Wait(); + var result = response.Result.Content.ReadAsStringAsync().Result; + } - private static HttpClient GetClient(string logName) - { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add(HEADER_ACCEPT, CONTENTTYPE); - client.DefaultRequestHeaders.Add(HEADER_LOGTYPE, logName); - return client; - } + private static HttpClient GetClient(string logName) + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add(HEADER_ACCEPT, CONTENTTYPE); + client.DefaultRequestHeaders.Add(HEADER_LOGTYPE, logName); + return client; + } - private HttpRequestMessage PrepareRequest(string signature, DateTime date, string resourceId, string json) - { - var request = new HttpRequestMessage(HttpMethod.Post, _EndpointUri); - request.Headers.Add(HEADER_AUTHORIZATION, signature); - request.Headers.Add(HEADER_DATE, date.ToString("r", FormatCulture)); - request.Headers.Add(HEADER_TIMEGENERATED, TIMESTAMPFIELD); - request.Headers.Add(HEADER_RESOURCEID, resourceId); - request.Content = new StringContent(json, Encoding.UTF8, CONTENTTYPE); - return request; - } + private HttpRequestMessage PrepareRequest(string signature, DateTime date, string resourceId, string json) + { + var request = new HttpRequestMessage(HttpMethod.Post, _EndpointUri); + request.Headers.Add(HEADER_AUTHORIZATION, signature); + request.Headers.Add(HEADER_DATE, date.ToString("r", FormatCulture)); + request.Headers.Add(HEADER_TIMEGENERATED, TIMESTAMPFIELD); + request.Headers.Add(HEADER_RESOURCEID, resourceId); + request.Content = new StringContent(json, Encoding.UTF8, CONTENTTYPE); + return request; + } - #region IDisposable + #region IDisposable - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - private void Dispose(bool disposing) + private void Dispose(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) + if (disposing) { - if (disposing) - { - _HttpClient.Dispose(); - } - _Disposed = true; + _HttpClient.Dispose(); } + _Disposed = true; } - - #endregion IDisposable } + + #endregion IDisposable } diff --git a/src/PSRule.Monitor/Pipeline/MonitorClient.cs b/src/PSRule.Monitor/Pipeline/MonitorClient.cs index 849a661..841eb5f 100644 --- a/src/PSRule.Monitor/Pipeline/MonitorClient.cs +++ b/src/PSRule.Monitor/Pipeline/MonitorClient.cs @@ -6,134 +6,133 @@ using System.Management.Automation; using System.Reflection; -namespace PSRule.Monitor.Pipeline +namespace PSRule.Monitor.Pipeline; + +internal abstract class MonitorClient : IDisposable { - internal abstract class MonitorClient : IDisposable + // Track whether Dispose has been called. + private bool _Disposed; + + protected static string GetPropertyValue(PSObject obj, string propertyName) { - // Track whether Dispose has been called. - private bool _Disposed; + return obj.Properties[propertyName] == null || obj.Properties[propertyName].Value == null ? null : obj.Properties[propertyName].Value.ToString(); + } - protected static string GetPropertyValue(PSObject obj, string propertyName) - { - return obj.Properties[propertyName] == null || obj.Properties[propertyName].Value == null ? null : obj.Properties[propertyName].Value.ToString(); - } + protected static Guid? GetPropertyGuid(PSObject obj, string propertyName) + { + var result = GetPropertyValue(obj, propertyName); + if (result == null) + return null; - protected static Guid? GetPropertyGuid(PSObject obj, string propertyName) - { - var result = GetPropertyValue(obj, propertyName); - if (result == null) - return null; + return Guid.Parse(result); + } - return Guid.Parse(result); - } + protected static T GetProperty(PSObject obj, string propertyName) + { + return obj.Properties[propertyName] == null ? default : (T)obj.Properties[propertyName].Value; + } - protected static T GetProperty(PSObject obj, string propertyName) - { - return obj.Properties[propertyName] == null ? default(T) : (T)obj.Properties[propertyName].Value; - } + protected static object GetProperty(object obj, string propertyName) + { + return TryProperty(obj, propertyName, out var value) ? value : null; + } - protected static object GetProperty(object obj, string propertyName) + private static bool TryProperty(object obj, string propertyName, out object value) + { + value = null; + var typeInfo = obj.GetType(); + if (obj is PSObject o && o.Properties[propertyName] != null) { - return TryProperty(obj, propertyName, out object value) ? value : null; + value = o.Properties[propertyName].Value; + return true; } - - private static bool TryProperty(object obj, string propertyName, out object value) + else { - value = null; - var typeInfo = obj.GetType(); - if (obj is PSObject o && o.Properties[propertyName] != null) + var propertyInfo = typeInfo.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty); + if (propertyInfo != null) { - value = o.Properties[propertyName].Value; + value = propertyInfo.GetValue(obj); return true; } - else - { - var propertyInfo = typeInfo.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty); - if (propertyInfo != null) - { - value = propertyInfo.GetValue(obj); - return true; - } - } - return false; } + return false; + } - protected static Hashtable GetPropertyMap(object o) - { - if (o == null) - return null; + protected static Hashtable GetPropertyMap(object o) + { + if (o == null) + return null; - var result = new Hashtable(); - if (o is IDictionary dictionary) + var result = new Hashtable(); + if (o is IDictionary dictionary) + { + foreach (DictionaryEntry kv in dictionary) { - foreach (DictionaryEntry kv in dictionary) - { - if (HasValue(kv.Value)) - result[kv.Key] = kv.Value; - } + if (HasValue(kv.Value)) + result[kv.Key] = kv.Value; } - else if (o is PSObject pso) + } + else if (o is PSObject pso) + { + foreach (var p in pso.Properties) { - foreach (var p in pso.Properties) - { - if (p.MemberType == PSMemberTypes.NoteProperty && HasValue(p.Value)) - result[p.Name] = p.Value; - } + if (p.MemberType == PSMemberTypes.NoteProperty && HasValue(p.Value)) + result[p.Name] = p.Value; } - return result.Count == 0 ? null : result; } + return result.Count == 0 ? null : result; + } - private static bool HasValue(object value) - { - return !(value == null || (value is string s && string.IsNullOrEmpty(s))); - } + private static bool HasValue(object value) + { + return !(value == null || (value is string s && string.IsNullOrEmpty(s))); + } - protected static string GetField(object o, string propertyName) - { - if (o is IDictionary dictionary && TryDictionary(dictionary, propertyName, out object value) && value != null) - return value.ToString(); + protected static string GetField(object o, string propertyName) + { + if (o is IDictionary dictionary && TryDictionary(dictionary, propertyName, out var value) && value != null) + return value.ToString(); - if (o is PSObject pso) - return GetPropertyValue(pso, propertyName); + if (o is PSObject pso) + return GetPropertyValue(pso, propertyName); - return null; - } + return null; + } - protected static bool TryDictionary(IDictionary dictionary, string key, out object value) + protected static bool TryDictionary(IDictionary dictionary, string key, out object value) + { + value = null; + var comparer = StringComparer.OrdinalIgnoreCase; + foreach (var k in dictionary.Keys) { - value = null; - var comparer = StringComparer.OrdinalIgnoreCase; - foreach (var k in dictionary.Keys) + if (comparer.Equals(key, k)) { - if (comparer.Equals(key, k)) - { - value = dictionary[k]; - return true; - } + value = dictionary[k]; + return true; } - return false; } + return false; + } - #region IDisposable + #region IDisposable - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) + if (disposing) { - if (disposing) - { - // Do nothing yet - } - _Disposed = true; + // Do nothing yet } + _Disposed = true; } - - #endregion IDisposable } + + #endregion IDisposable } diff --git a/src/PSRule.Monitor/Pipeline/PipelineBuilder.cs b/src/PSRule.Monitor/Pipeline/PipelineBuilder.cs index 8633f9a..2cba1d5 100644 --- a/src/PSRule.Monitor/Pipeline/PipelineBuilder.cs +++ b/src/PSRule.Monitor/Pipeline/PipelineBuilder.cs @@ -1,135 +1,134 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Monitor.Configuration; using System; using System.Diagnostics.CodeAnalysis; using System.Management.Automation; +using PSRule.Monitor.Configuration; + +namespace PSRule.Monitor.Pipeline; -namespace PSRule.Monitor.Pipeline +public static class PipelineBuilder { - public static class PipelineBuilder + public static IInjestPipelineBuilder Injest(PSRuleOption option) { - public static IInjestPipelineBuilder Injest(PSRuleOption option) - { - var builder = new InjestPipelineBuilder(); - builder.Configure(option); - return builder; - } + var builder = new InjestPipelineBuilder(); + builder.Configure(option); + return builder; } +} - public interface IPipelineBuilder - { - void UseCommandRuntime(ICommandRuntime2 commandRuntime); +public interface IPipelineBuilder +{ + void UseCommandRuntime(ICommandRuntime2 commandRuntime); - void UseExecutionContext(EngineIntrinsics executionContext); + void UseExecutionContext(EngineIntrinsics executionContext); - [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords")] - IPipelineBuilder Configure(PSRuleOption option); + [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords")] + IPipelineBuilder Configure(PSRuleOption option); - IPipeline Build(); - } + IPipeline Build(); +} - public interface IPipeline - { - void Begin(); +public interface IPipeline +{ + void Begin(); - void Process(PSObject sourceObject); + void Process(PSObject sourceObject); - [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords")] - void End(); - } + [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords")] + void End(); +} - internal abstract class PipelineBuilderBase : IPipelineBuilder - { - protected readonly PSRuleOption Option; +internal abstract class PipelineBuilderBase : IPipelineBuilder +{ + protected readonly PSRuleOption Option; - protected PipelineBuilderBase() - { - Option = new PSRuleOption(); - } + protected PipelineBuilderBase() + { + Option = new PSRuleOption(); + } - public virtual void UseCommandRuntime(ICommandRuntime2 commandRuntime) - { - // Do nothing - } + public virtual void UseCommandRuntime(ICommandRuntime2 commandRuntime) + { + // Do nothing + } - public void UseExecutionContext(EngineIntrinsics executionContext) - { - // Do nothing - } + public void UseExecutionContext(EngineIntrinsics executionContext) + { + // Do nothing + } - public virtual IPipelineBuilder Configure(PSRuleOption option) - { - return this; - } + public virtual IPipelineBuilder Configure(PSRuleOption option) + { + return this; + } - public abstract IPipeline Build(); + public abstract IPipeline Build(); - protected PipelineContext PrepareContext() - { - return new PipelineContext(Option); - } - - protected virtual PipelineReader PrepareReader() - { - return new PipelineReader(); - } + protected PipelineContext PrepareContext() + { + return new PipelineContext(Option); } - internal abstract class PipelineBase : IDisposable, IPipeline + protected virtual PipelineReader PrepareReader() { - protected readonly PipelineContext Context; - protected readonly PipelineReader Reader; + return new PipelineReader(); + } +} - // Track whether Dispose has been called. - private bool _Disposed; +internal abstract class PipelineBase : IDisposable, IPipeline +{ + protected readonly PipelineContext Context; + protected readonly PipelineReader Reader; - protected PipelineBase(PipelineContext context, PipelineReader reader) - { - Context = context; - Reader = reader; - } + // Track whether Dispose has been called. + private bool _Disposed; - #region IPipeline + protected PipelineBase(PipelineContext context, PipelineReader reader) + { + Context = context; + Reader = reader; + } - public virtual void Begin() - { - // Do nothing - } + #region IPipeline - public virtual void Process(PSObject sourceObject) - { - // Do nothing - } + public virtual void Begin() + { + // Do nothing + } - public virtual void End() - { - // Do nothing - } + public virtual void Process(PSObject sourceObject) + { + // Do nothing + } + + public virtual void End() + { + // Do nothing + } - #endregion IPipeline + #endregion IPipeline - #region IDisposable + #region IDisposable - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) + if (disposing) { - if (disposing) - { - Context.Dispose(); - } - _Disposed = true; + Context.Dispose(); } + _Disposed = true; } - - #endregion IDisposable } + + #endregion IDisposable } diff --git a/src/PSRule.Monitor/Pipeline/PipelineContext.cs b/src/PSRule.Monitor/Pipeline/PipelineContext.cs index cf0dd52..285ea57 100644 --- a/src/PSRule.Monitor/Pipeline/PipelineContext.cs +++ b/src/PSRule.Monitor/Pipeline/PipelineContext.cs @@ -1,43 +1,42 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Monitor.Configuration; using System; +using PSRule.Monitor.Configuration; -namespace PSRule.Monitor.Pipeline +namespace PSRule.Monitor.Pipeline; + +internal sealed class PipelineContext : IDisposable { - internal sealed class PipelineContext : IDisposable - { - internal readonly PSRuleOption Option; + internal readonly PSRuleOption Option; - // Track whether Dispose has been called. - private bool _Disposed; + // Track whether Dispose has been called. + private bool _Disposed; - public PipelineContext(PSRuleOption option) - { - Option = option; - } + public PipelineContext(PSRuleOption option) + { + Option = option; + } - #region IDisposable + #region IDisposable - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - private void Dispose(bool disposing) + private void Dispose(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) + if (disposing) { - if (disposing) - { - // Add cleanup - } - _Disposed = true; + // Add cleanup } + _Disposed = true; } - - #endregion IDisposable } + + #endregion IDisposable } diff --git a/src/PSRule.Monitor/Pipeline/PipelineReader.cs b/src/PSRule.Monitor/Pipeline/PipelineReader.cs index 2c06cd0..4125365 100644 --- a/src/PSRule.Monitor/Pipeline/PipelineReader.cs +++ b/src/PSRule.Monitor/Pipeline/PipelineReader.cs @@ -4,38 +4,37 @@ using System.Collections.Concurrent; using System.Management.Automation; -namespace PSRule.Monitor.Pipeline +namespace PSRule.Monitor.Pipeline; + +internal sealed class PipelineReader { - internal sealed class PipelineReader + private readonly ConcurrentQueue _Queue; + + public PipelineReader() + { + _Queue = new ConcurrentQueue(); + } + + public int Count + { + get { return _Queue.Count; } + } + + public bool IsEmpty + { + get { return _Queue.IsEmpty; } + } + + public void Enqueue(PSObject sourceObject) + { + if (sourceObject == null) + return; + + _Queue.Enqueue(sourceObject); + } + + public bool TryDequeue(out PSObject sourceObject) { - private readonly ConcurrentQueue _Queue; - - public PipelineReader() - { - _Queue = new ConcurrentQueue(); - } - - public int Count - { - get { return _Queue.Count; } - } - - public bool IsEmpty - { - get { return _Queue.IsEmpty; } - } - - public void Enqueue(PSObject sourceObject) - { - if (sourceObject == null) - return; - - _Queue.Enqueue(sourceObject); - } - - public bool TryDequeue(out PSObject sourceObject) - { - return _Queue.TryDequeue(out sourceObject); - } + return _Queue.TryDequeue(out sourceObject); } } diff --git a/src/PSRule.Monitor/Pipeline/WorkspaceClient.cs b/src/PSRule.Monitor/Pipeline/WorkspaceClient.cs index 04d4e86..11bc183 100644 --- a/src/PSRule.Monitor/Pipeline/WorkspaceClient.cs +++ b/src/PSRule.Monitor/Pipeline/WorkspaceClient.cs @@ -1,144 +1,143 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Newtonsoft.Json; -using PSRule.Monitor.Data; using System; using System.Management.Automation; using System.Security; using System.Text; +using Newtonsoft.Json; +using PSRule.Monitor.Data; + +namespace PSRule.Monitor.Pipeline; -namespace PSRule.Monitor.Pipeline +internal static class WorkspaceClientExtensions { - internal static class WorkspaceClientExtensions + public static void Enqueue(this WorkspaceClient client, PSObject[] results) { - public static void Enqueue(this WorkspaceClient client, PSObject[] results) - { - if (results == null || results.Length == 0) - return; + if (results == null || results.Length == 0) + return; - for (var i = 0; i < results.Length; i++) - client.Enqueue(results[i]); - } + for (var i = 0; i < results.Length; i++) + client.Enqueue(results[i]); } +} - internal sealed class WorkspaceClient : MonitorClient - { - private const string CONTENTTYPE = "application/json"; +internal sealed class WorkspaceClient : MonitorClient +{ + private const string CONTENTTYPE = "application/json"; - private readonly CollectionHash _Hash; - private readonly BatchQueue _SubmissionQueue; - private readonly ILogClient _LogClient; - private readonly Guid _CorrelationId; + private readonly CollectionHash _Hash; + private readonly BatchQueue _SubmissionQueue; + private readonly ILogClient _LogClient; + private readonly Guid _CorrelationId; - // Track whether Dispose has been called. - private bool _Disposed; + // Track whether Dispose has been called. + private bool _Disposed; - public WorkspaceClient(string workspaceId, SecureString sharedKey, ILogClient logClient) - { - _Hash = new CollectionHash(workspaceId, sharedKey); - _SubmissionQueue = new BatchQueue(); - _LogClient = logClient; - _CorrelationId = Guid.NewGuid(); - } + public WorkspaceClient(string workspaceId, SecureString sharedKey, ILogClient logClient) + { + _Hash = new CollectionHash(workspaceId, sharedKey); + _SubmissionQueue = new BatchQueue(); + _LogClient = logClient; + _CorrelationId = Guid.NewGuid(); + } - public void Enqueue(PSObject result) - { - _SubmissionQueue.Enqueue(ProcessResult(result)); - } + public void Enqueue(PSObject result) + { + _SubmissionQueue.Enqueue(ProcessResult(result)); + } - public void Send(int minSize, int maxSize) - { - while (_SubmissionQueue.TryDequeue(minSize, maxSize, out LogRecord[] records)) - SubmitBatch(records); - } + public void Send(int minSize, int maxSize) + { + while (_SubmissionQueue.TryDequeue(minSize, maxSize, out var records)) + SubmitBatch(records); + } - public void Send() - { - Send(0, 100); - } + public void Send() + { + Send(0, 100); + } - /// - /// Submits a batch of records to Azure Monitor data collector. - /// - private void SubmitBatch(LogRecord[] records) - { - var json = JsonConvert.SerializeObject(records); - var resourceId = records[0].ResourceId; - - // Create a hash for the API signature - var date = DateTime.UtcNow; - var data = Encoding.UTF8.GetBytes(json); - var signature = _Hash.ComputeSignature(data.Length, date, CONTENTTYPE); - PostData(signature, date, resourceId, json); - } + /// + /// Submits a batch of records to Azure Monitor data collector. + /// + private void SubmitBatch(LogRecord[] records) + { + var json = JsonConvert.SerializeObject(records); + var resourceId = records[0].ResourceId; + + // Create a hash for the API signature + var date = DateTime.UtcNow; + var data = Encoding.UTF8.GetBytes(json); + var signature = _Hash.ComputeSignature(data.Length, date, CONTENTTYPE); + PostData(signature, date, resourceId, json); + } - /// - /// Maps a RuleRecord to a LogRecord. - /// - private LogRecord ProcessResult(PSObject sourceObject) + /// + /// Maps a RuleRecord to a LogRecord. + /// + private LogRecord ProcessResult(PSObject sourceObject) + { + if (sourceObject == null) + return null; + + var ruleId = GetPropertyValue(sourceObject, "ruleId"); + var ruleName = GetPropertyValue(sourceObject, "ruleName"); + var targetName = GetPropertyValue(sourceObject, "targetName"); + var targetType = GetPropertyValue(sourceObject, "targetType"); + var outcome = GetPropertyValue(sourceObject, "outcome"); + var data = GetProperty(sourceObject, "data"); + var field = GetProperty(sourceObject, "field"); + var info = GetProperty(sourceObject, "info"); + var resourceId = GetField(data, "resourceId") ?? GetField(field, "resourceId"); + var displayName = GetField(info, "displayName") ?? ruleName; + var moduleName = GetField(info, "moduleName"); + var annotations = GetProperty(info, "annotations"); + var runId = GetPropertyValue(sourceObject, "runId"); + var duration = GetProperty(sourceObject, "time"); + var record = new LogRecord { - if (sourceObject == null) - return null; - - var ruleId = GetPropertyValue(sourceObject, "ruleId"); - var ruleName = GetPropertyValue(sourceObject, "ruleName"); - var targetName = GetPropertyValue(sourceObject, "targetName"); - var targetType = GetPropertyValue(sourceObject, "targetType"); - var outcome = GetPropertyValue(sourceObject, "outcome"); - var data = GetProperty(sourceObject, "data"); - var field = GetProperty(sourceObject, "field"); - var info = GetProperty(sourceObject, "info"); - var resourceId = GetField(data, "resourceId") ?? GetField(field, "resourceId"); - var displayName = GetField(info, "displayName") ?? ruleName; - var moduleName = GetField(info, "moduleName"); - var annotations = GetProperty(info, "annotations"); - var runId = GetPropertyValue(sourceObject, "runId"); - var duration = GetProperty(sourceObject, "time"); - var record = new LogRecord - { - RuleId = ruleId, - RuleName = ruleName, - DisplayName = displayName, - ModuleName = moduleName, - TargetName = targetName, - TargetType = targetType, - Outcome = outcome, - ResourceId = resourceId, - Data = GetPropertyMap(data), - Field = GetPropertyMap(field), - Annotations = GetPropertyMap(annotations), - RunId = runId, - CorrelationId = _CorrelationId, - Duration = duration, - }; - return record; - } + RuleId = ruleId, + RuleName = ruleName, + DisplayName = displayName, + ModuleName = moduleName, + TargetName = targetName, + TargetType = targetType, + Outcome = outcome, + ResourceId = resourceId, + Data = GetPropertyMap(data), + Field = GetPropertyMap(field), + Annotations = GetPropertyMap(annotations), + RunId = runId, + CorrelationId = _CorrelationId, + Duration = duration, + }; + return record; + } - /// - /// Post log data to Azure Monitor endpoint. - /// - private void PostData(string signature, DateTime date, string resourceId, string json) - { - _LogClient.Post(signature, date, resourceId, json); - } + /// + /// Post log data to Azure Monitor endpoint. + /// + private void PostData(string signature, DateTime date, string resourceId, string json) + { + _LogClient.Post(signature, date, resourceId, json); + } - #region IDisposable + #region IDisposable - protected override void Dispose(bool disposing) + protected override void Dispose(bool disposing) + { + if (!_Disposed) { - if (!_Disposed) + if (disposing) { - if (disposing) - { - _Hash.Dispose(); - _LogClient.Dispose(); - } - _Disposed = true; + _Hash.Dispose(); + _LogClient.Dispose(); } - base.Dispose(disposing); + _Disposed = true; } - - #endregion IDisposable + base.Dispose(disposing); } + + #endregion IDisposable } diff --git a/src/PSRule.Monitor/Runtime/Workspace.cs b/src/PSRule.Monitor/Runtime/Workspace.cs index b69a189..38f8dab 100644 --- a/src/PSRule.Monitor/Runtime/Workspace.cs +++ b/src/PSRule.Monitor/Runtime/Workspace.cs @@ -1,31 +1,28 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Monitor.Pipeline; using System.Management.Automation; using System.Net; using System.Security; +using PSRule.Monitor.Pipeline; + +namespace PSRule.Monitor.Runtime; -namespace PSRule.Monitor.Runtime +/// +/// Helper methods exposed to PowerShell for interacting with a Log Analytics workspace. +/// +public static class Workspace { - /// - /// Helper methods exposed to PowerShell for interacting with a Log Analytics workspace. - /// - public static class Workspace + public static void Send(PSObject[] results, string workspaceId, string sharedKey) { - public static void Send(PSObject[] results, string workspaceId, string sharedKey) - { - var logClient = new LogClient(workspaceId, "PSRule"); - using (var client = new WorkspaceClient(workspaceId, GetSecureString(sharedKey), logClient)) - { - client.Enqueue(results); - client.Send(); - } - } + var logClient = new LogClient(workspaceId, "PSRule"); + using var client = new WorkspaceClient(workspaceId, GetSecureString(sharedKey), logClient); + client.Enqueue(results); + client.Send(); + } - private static SecureString GetSecureString(string sharedKey) - { - return new NetworkCredential("na", sharedKey).SecurePassword; - } + private static SecureString GetSecureString(string sharedKey) + { + return new NetworkCredential("na", sharedKey).SecurePassword; } } diff --git a/tests/PSRule.Monitor.Tests/InjestPipelineTests.cs b/tests/PSRule.Monitor.Tests/InjestPipelineTests.cs index 5a1b07a..81dd09a 100644 --- a/tests/PSRule.Monitor.Tests/InjestPipelineTests.cs +++ b/tests/PSRule.Monitor.Tests/InjestPipelineTests.cs @@ -1,86 +1,85 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using PSRule.Monitor.Configuration; -using PSRule.Monitor.Pipeline; using System; using System.Management.Automation; using System.Security; using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PSRule.Monitor.Configuration; +using PSRule.Monitor.Pipeline; using Xunit; -namespace PSRule.Monitor +namespace PSRule.Monitor; + +public sealed class InjestPipelineTests { - public sealed class InjestPipelineTests + [Fact] + public void InvokePipeline() + { + var pipeline = GetPipeline(out var logClient); + pipeline.Begin(); + var o = GetObject(); + pipeline.Process(o); + pipeline.End(); + + Assert.Single(logClient.Output); + var actual = logClient.Output[0]; + var actualJson = JsonConvert.DeserializeObject(actual.Json); + + Assert.Equal("test-resource-id", actual.ResourceId); + Assert.NotEmpty(actual.Signature); + + Assert.Equal(o.Properties["ruleName"].Value, actualJson[0]["RuleName"].Value()); + Assert.Equal(o.Properties["targetName"].Value, actualJson[0]["TargetName"].Value()); + Assert.Equal(o.Properties["targetType"].Value, actualJson[0]["TargetType"].Value()); + Assert.Equal(o.Properties["outcome"].Value, actualJson[0]["Outcome"].Value()); + Assert.Equal(o.Properties["ruleName"].Value, actualJson[0]["DisplayName"].Value()); + Assert.Equal("test-module", actualJson[0]["ModuleName"].Value()); + } + + #region Helper methods + + private const string _WorkspaceId = "00000000-0000-0000-0000-000000000000"; + + private static PSObject GetObject() + { + var info = new PSObject(); + info.Properties.Add(new PSNoteProperty("moduleName", "test-module")); + + var data = new PSObject(); + data.Properties.Add(new PSNoteProperty("resourceId", "test-resource-id")); + + var o = new PSObject(); + o.Properties.Add(new PSNoteProperty("ruleName", "test-rule")); + o.Properties.Add(new PSNoteProperty("targetName", "test-name")); + o.Properties.Add(new PSNoteProperty("targetType", "test-type")); + o.Properties.Add(new PSNoteProperty("outcome", "Fail")); + o.Properties.Add(new PSNoteProperty("info", info)); + o.Properties.Add(new PSNoteProperty("data", data)); + return o; + } + + private static IPipeline GetPipeline(out TestLogClient logClient) { - [Fact] - public void InvokePipeline() - { - var pipeline = GetPipeline(out TestLogClient logClient); - pipeline.Begin(); - var o = GetObject(); - pipeline.Process(o); - pipeline.End(); - - Assert.Single(logClient.Output); - var actual = logClient.Output[0]; - var actualJson = JsonConvert.DeserializeObject(actual.Json); - - Assert.Equal("test-resource-id", actual.ResourceId); - Assert.NotEmpty(actual.Signature); - - Assert.Equal(o.Properties["ruleName"].Value, actualJson[0]["RuleName"].Value()); - Assert.Equal(o.Properties["targetName"].Value, actualJson[0]["TargetName"].Value()); - Assert.Equal(o.Properties["targetType"].Value, actualJson[0]["TargetType"].Value()); - Assert.Equal(o.Properties["outcome"].Value, actualJson[0]["Outcome"].Value()); - Assert.Equal(o.Properties["ruleName"].Value, actualJson[0]["DisplayName"].Value()); - Assert.Equal("test-module", actualJson[0]["ModuleName"].Value()); - } - - #region Helper methods - - private const string _WorkspaceId = "00000000-0000-0000-0000-000000000000"; - - private static PSObject GetObject() - { - var info = new PSObject(); - info.Properties.Add(new PSNoteProperty("moduleName", "test-module")); - - var data = new PSObject(); - data.Properties.Add(new PSNoteProperty("resourceId", "test-resource-id")); - - var o = new PSObject(); - o.Properties.Add(new PSNoteProperty("ruleName", "test-rule")); - o.Properties.Add(new PSNoteProperty("targetName", "test-name")); - o.Properties.Add(new PSNoteProperty("targetType", "test-type")); - o.Properties.Add(new PSNoteProperty("outcome", "Fail")); - o.Properties.Add(new PSNoteProperty("info", info)); - o.Properties.Add(new PSNoteProperty("data", data)); - return o; - } - - private static IPipeline GetPipeline(out TestLogClient logClient) - { - var key = new SecureString(); - foreach (var c in Convert.ToBase64String(Encoding.UTF8.GetBytes(_WorkspaceId))) - key.AppendChar(c); - - logClient = new TestLogClient(); - return new InjestPipeline(GetContent(), GetReader(), _WorkspaceId, key, logClient); - } - - private static PipelineReader GetReader() - { - return new PipelineReader(); - } - - private static PipelineContext GetContent() - { - return new PipelineContext(new PSRuleOption()); - } - - #endregion Helper methods + var key = new SecureString(); + foreach (var c in Convert.ToBase64String(Encoding.UTF8.GetBytes(_WorkspaceId))) + key.AppendChar(c); + + logClient = new TestLogClient(); + return new InjestPipeline(GetContent(), GetReader(), _WorkspaceId, key, logClient); } + + private static PipelineReader GetReader() + { + return new PipelineReader(); + } + + private static PipelineContext GetContent() + { + return new PipelineContext(new PSRuleOption()); + } + + #endregion Helper methods } diff --git a/tests/PSRule.Monitor.Tests/PSRule.Monitor.Tests.csproj b/tests/PSRule.Monitor.Tests/PSRule.Monitor.Tests.csproj index 9e0c2c5..b187a7c 100644 --- a/tests/PSRule.Monitor.Tests/PSRule.Monitor.Tests.csproj +++ b/tests/PSRule.Monitor.Tests/PSRule.Monitor.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 {22bf9268-67e2-4c43-b254-1c43262cddcc} true false @@ -11,7 +11,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -27,4 +27,4 @@ - + \ No newline at end of file diff --git a/tests/PSRule.Monitor.Tests/TestLogClient.cs b/tests/PSRule.Monitor.Tests/TestLogClient.cs index 0292e1b..434bae1 100644 --- a/tests/PSRule.Monitor.Tests/TestLogClient.cs +++ b/tests/PSRule.Monitor.Tests/TestLogClient.cs @@ -1,53 +1,52 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Monitor.Pipeline; using System; using System.Collections.Generic; +using PSRule.Monitor.Pipeline; -namespace PSRule.Monitor +namespace PSRule.Monitor; + +internal sealed class TestLogClient : ILogClient { - internal sealed class TestLogClient : ILogClient + public TestLogClient() + { + Output = new List(); + } + + public List Output { get; } + + public void Dispose() { - public TestLogClient() - { - Output = new List(); - } - - public List Output { get; } - - public void Dispose() - { - // Test class only - } - - public void Post(string signature, DateTime date, string resourceId, string json) - { - Output.Add(new LogEntry( - signature, - date, - resourceId, - json - )); - } + // Test class only } - internal sealed class LogEntry + public void Post(string signature, DateTime date, string resourceId, string json) { - public LogEntry(string signature, DateTime date, string resourceId, string json) - { - Signature = signature; - Date = date; - ResourceId = resourceId; - Json = json; - } + Output.Add(new LogEntry( + signature, + date, + resourceId, + json + )); + } +} - public string Signature { get; } +internal sealed class LogEntry +{ + public LogEntry(string signature, DateTime date, string resourceId, string json) + { + Signature = signature; + Date = date; + ResourceId = resourceId; + Json = json; + } - public DateTime Date { get; } + public string Signature { get; } - public string ResourceId { get; } + public DateTime Date { get; } - public string Json { get; } - } + public string ResourceId { get; } + + public string Json { get; } }