diff --git a/.github/workflows/deploy-module.yaml b/.github/workflows/deploy-module.yaml index e113856..e9c44d6 100644 --- a/.github/workflows/deploy-module.yaml +++ b/.github/workflows/deploy-module.yaml @@ -28,15 +28,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.9.15 + uses: gittools/actions/gitversion/setup@v1.1.1 with: versionSpec: 5.x - name: Evaluate Next Version - uses: gittools/actions/gitversion/execute@v0.9.15 + uses: gittools/actions/gitversion/execute@v1.1.1 with: configFilePath: GitVersion.yml - name: Build & Package Module @@ -45,7 +45,7 @@ jobs: env: ModuleVersion: ${{ env.gitVersion.NuGetVersionV2 }} - name: Publish Build Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.buildArtifactName }} path: ${{ env.buildFolderName }}/ @@ -55,11 +55,11 @@ jobs: needs: Build_Stage_Package_Module steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ${{ env.buildArtifactName }} path: ${{ env.buildFolderName }} @@ -67,7 +67,7 @@ jobs: shell: pwsh run: ./build.ps1 -tasks test - name: Publish Test Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: path: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/ name: CodeCoverageLinux @@ -78,11 +78,11 @@ jobs: needs: Build_Stage_Package_Module steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ${{ env.buildArtifactName }} path: ${{ env.buildFolderName }} @@ -90,7 +90,7 @@ jobs: shell: pwsh run: ./build.ps1 -tasks test - name: Publish Test Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: path: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/ name: CodeCoverageWinPS7 @@ -101,11 +101,11 @@ jobs: needs: Build_Stage_Package_Module steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ${{ env.buildArtifactName }} path: ${{ env.buildFolderName }} @@ -113,7 +113,7 @@ jobs: shell: pwsh run: ./build.ps1 -tasks test - name: Publish Test Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: path: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/ name: CodeCoverageMacOS @@ -129,25 +129,25 @@ jobs: - Test_Stage_test_macos steps: # - name: Checkout Code - # uses: actions/checkout@v3 + # uses: actions/checkout@v4 # with: # fetch-depth: 0 # - name: Download Test Artifacts - # uses: actions/download-artifact@v3 + # uses: actions/download-artifact@v4 # with: # path: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/ - name: Download Test Artifact macOS - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: CodeCoverageMacOS path: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/CodeCoverageMacOS/ - name: Download Test Artifact Linux - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: CodeCoverageLinux path: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/CodeCoverageLinux/ - name: Download Test Artifact Windows (PS7) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: CodeCoverageWinPS7 path: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/CodeCoverageWinPS7/ @@ -173,7 +173,7 @@ jobs: nunit_files: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/CodeCoverageWinPS7/NUnit*.xml check_name: WinPS71 Test Results - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.0.1 + uses: codecov/codecov-action@v4.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} slug: CyberShell-App/CyberShell @@ -189,11 +189,11 @@ jobs: if: github.ref == 'refs/heads/main' steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download Build Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ${{ env.buildArtifactName }} path: ${{ env.buildFolderName }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 32809be..405ee8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,5 +8,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Powershell module initialization using the excellent [Sampler](https://github.com/gaelcolas/Sampler). - - +- Add 'ResourceLogCategory' Class diff --git a/RequiredModules.psd1 b/RequiredModules.psd1 index ef2098d..1089b9f 100644 --- a/RequiredModules.psd1 +++ b/RequiredModules.psd1 @@ -1,5 +1,5 @@ @{ - PSDependOptions = @{ + PSDependOptions = @{ AddToPath = $true Target = 'output\RequiredModules' Parameters = @{ @@ -7,14 +7,15 @@ } } - InvokeBuild = 'latest' - PSScriptAnalyzer = 'latest' - Pester = 'latest' - ModuleBuilder = 'latest' - ChangelogManagement = 'latest' - Sampler = 'latest' - 'Sampler.GitHubTasks' = 'latest' - - + InvokeBuild = 'latest' + PSScriptAnalyzer = 'latest' + Pester = 'latest' + ModuleBuilder = 'latest' + ChangelogManagement = 'latest' + Sampler = 'latest' + 'Sampler.GitHubTasks' = 'latest' + 'Az.Accounts' = 'latest' + 'Az.Monitor' = 'latest' + 'Az.Resources' = 'latest' } diff --git a/build.yaml b/build.yaml index bbe5784..40375a1 100644 --- a/build.yaml +++ b/build.yaml @@ -102,6 +102,7 @@ Pester: # - helpQuality # - FunctionalQuality # - TestQuality + - UnitQuality Tag: CodeCoverageThreshold: 0 # Set to 0 to bypass #CodeCoverageOutputFile: JaCoCo_$OsShortName.xml diff --git a/src/Classes/002 LogCategory-list.ps1.old b/src/Classes/002 LogCategory-list.ps1.old new file mode 100644 index 0000000..c3865e1 --- /dev/null +++ b/src/Classes/002 LogCategory-list.ps1.old @@ -0,0 +1,90 @@ +#dotsource the corresponding object class +class ListOfLogCategory { + # Static property to hold the list of resources + static [System.Collections.Generic.List[LogCategoryObj]] $Resources + + # Static method to initialize the list of resources. Called in the other + # static methods to avoid needing to explicit initialize the value. + static [void] Initialize() { [ListOfLogCategory]::Initialize($false) } + + # Initialize the list of resources. + static [bool] Initialize([bool]$force) { + if ([ListOfLogCategory]::Resources.Count -gt 0 -and -not $force) { + return $false + } + [ListOfLogCategory]::Resources = [System.Collections.Generic.List[LogCategoryObj]]::new() + return $true + } + + # Ensure the LogCategoryObj is valid for the list. + static [void] Validate([LogCategoryObj]$Resource) { + $Prefix = 'Resource validation failed: Resource must be defined with the ContainerId, ResourceTypeName, and SourceType properties, but' + if ($null -eq $Resource) { throw "$Prefix was null" } + if ([string]::IsNullOrEmpty($Resource.ContainerId)) { + throw "$Prefix ContainerId wasn't defined" + } + if ([string]::IsNullOrEmpty($Resource.ResourceTypeName)) { + throw "$Prefix ResourceTypeName wasn't defined" + } + if ([string]::IsNullOrEmpty($Resource.SourceType)) { + throw "$Prefix SourceType wasn't defined" + } + } + + # Static methods to manage the list of LogCategoryObj. + # Add a LogCategoryObj if it's not already in the list. + static [void] Add([LogCategoryObj]$Resource) { + [ListOfLogCategory]::Initialize() + [ListOfLogCategory]::Validate($Resource) + $FindPredicate = { + param([LogCategoryObj]$r) + $r.ContainerId -eq $Resource.ContainerId -and + $r.ResourceTypeName -eq $Resource.ResourceTypeName -and + $r.SourceType -eq $Resource.SourceType + }.GetNewClosure() + if ([ListOfLogCategory]::Resources.Find($FindPredicate)) { + throw "Resource with ContainerId '$Resource.ContainerId', ResourceTypeName '$Resource.ResourceTypeName', and SourceType '$Resource.SourceType' already in list" + } + [ListOfLogCategory]::Resources.Add($Resource) + } + + # Clear the list of LogCategoryObj. + static [void] Clear() { + [ListOfLogCategory]::Initialize() + [ListOfLogCategory]::Resources.Clear() + } + + # Method to find the first LogCategoryObj that matches the given criteria. + # This method stops searching as soon as it finds a match, so it's more efficient for large lists. + # However, it will not alert you to duplicate entries. + static [LogCategoryObj] Find([scriptblock]$Predicate) { + [ListOfLogCategory]::Initialize() + return [ListOfLogCategory]::Resources.Find($Predicate) + } + + # Method to find all LogCategoryObjs that match the given criteria. + # This method searches the entire list and returns all matches. + # Use this method when you need to find all matches, or when you need to check for duplicates. + static [LogCategoryObj[]] FindAll([scriptblock]$Predicate) { + [ListOfLogCategory]::Initialize() + return [ListOfLogCategory]::Resources.FindAll($Predicate) + } + + # Remove a LogCategoryObj from the list. + static [void] Remove([LogCategoryObj]$Resource) { + [ListOfLogCategory]::Initialize() + [ListOfLogCategory]::Resources.Remove($Resource) + } + + # Remove a LogCategoryObj from the list by property and value. + static [void] RemoveBy([string]$Property, [string]$Value) { + [ListOfLogCategory]::Initialize() + $Index = [ListOfLogCategory]::Resources.FindIndex({ + param($r) + $r.$Property -eq $Value + }.GetNewClosure()) + if ($Index -ge 0) { + [ListOfLogCategory]::Resources.RemoveAt($Index) + } + } +} diff --git a/src/Classes/LogCategoryObj.ps1 b/src/Classes/LogCategoryObj.ps1 new file mode 100644 index 0000000..52cd119 --- /dev/null +++ b/src/Classes/LogCategoryObj.ps1 @@ -0,0 +1,118 @@ +class LogCategoryObj { + # Class properties + [string] $SourceType = "" + [string] $ContainerId = "" + [string] $ResourceTypeName = "" + [array] $LogCategory = @() + [array] $MetricCategory = @() + + + # ------------------------------ + # Input Validation + # ------------------------------ + + # Static method to validate the input parameters + static [void] Validate([string]$ContainerId, [string]$ResourceTypeName, [string]$SourceType) { + $errors = @() + # Basic validation for null or empty strings + if ([string]::IsNullOrEmpty($SourceType)) { + $errors += "SourceType cannot be null or empty" + } + if ([string]::IsNullOrEmpty($ContainerId)) { + $errors += "ContainerId cannot be null or empty" + } + if ([string]::IsNullOrEmpty($ResourceTypeName)) { + $errors += "ResourceTypeName cannot be null or empty" + } + + # Additional validation for SourceType 'Az' + # to ensure that ContainerId is a valid GUID + if ($SourceType -eq 'Az') { + $guid = New-Object Guid + if (-not [Guid]::TryParse($ContainerId, [ref]$guid)) { + $errors += "ContainerId must be a valid GUID when SourceType is 'Az'" + } + } + + # Throw an exception if there are any errors + if ($errors.Count -gt 0) { + throw ($errors -join "`n") + } + } + + # ------------------------------ + # Constructors + # ------------------------------ + + # Default constructor + LogCategoryObj() { + # There's no need to reinitialize LogCategory and MetricCategory in this constructor + # as they are already initialized when the class properties are declared. + # Typically, a constructor is used for any logic that needs to be executed during object creation. + # However, for these properties, such logic is not applicable. + } + + # Convenience constructor from hashtable + LogCategoryObj([hashtable]$Properties) { + [LogCategoryObj]::Validate($Properties.ContainerId, $Properties.ResourceTypeName, $Properties.SourceType) + $this.Init($Properties) + $this.GetDiagnosticSettings() + } + + # Common constructor for separate properties + LogCategoryObj([string]$ContainerId, [string]$ResourceTypeName, [string]$SourceType) { + [LogCategoryObj]::Validate($ContainerId, $ResourceTypeName, $SourceType) + $this.ContainerId = $ContainerId + $this.ResourceTypeName = $ResourceTypeName + $this.SourceType = $SourceType + $this.GetDiagnosticSettings() + } + + # ------------------------------ + # Methods + # ------------------------------ + + # Hashtable parser that initializes matching object properties + [void] Init([hashtable]$Properties) { + foreach ($key in $Properties.Keys) { + if ($this.psobject.properties.Match($key).Count -gt 0) { + $this.$key = $Properties[$key] + } + } + } + + # Method to get diagnostic settings + [void] GetDiagnosticSettings() { + $resource = Get-AzResource -ResourceType $this.ResourceTypeName | Select-Object -First 1 + + $diagnosticSettings = $null + + try { + $diagnosticSettings = (Get-AzDiagnosticSettingCategory -ResourceId $resource.ResourceId -ErrorAction SilentlyContinue) | Select-Object Name, CategoryType + } + catch { + $diagnosticSettings = $null + } + + if ($diagnosticSettings) { + + $diagnosticSettings | ForEach-Object { + if ($_.CategoryType -eq 'Logs') { + $this.LogCategory += $_.Name + } + elseif ($_.CategoryType -eq 'Metrics') { + $this.MetricCategory += $_.Name + } + } + } + else { + $this.LogCategory = @() + $this.MetricCategory = @() + } + } + + # Method to return a string representation of the resource + [string] ToString() { + return "Resource Type: $($this.ResourceTypeName) in ContainerId: $($this.ContainerId) from Source: $($this.SourceType)" + } +} \ No newline at end of file diff --git a/src/Public/Get-CsLogCategory.ps1 b/src/Public/Get-CsLogCategory.ps1 new file mode 100644 index 0000000..f29f542 --- /dev/null +++ b/src/Public/Get-CsLogCategory.ps1 @@ -0,0 +1,71 @@ +function Get-CsLogCategory { + + <# + .SYNOPSIS + This script retrieves unique Azure resources and their types from the current subscription, and outputs them in a table format. + + .DESCRIPTION + The script first clears the ListOfLogCategory. It then retrieves all Azure resources in the current subscription using the Get-AzResource cmdlet. + For each unique resource, it creates a new LogCategoryObj object with the SubscriptionId, LogCategoryObj, and a SourceType of 'Az'. + These LogCategoryObj objects are added to the ListOfLogCategory. + Finally, it outputs the ListOfLogCategory in a table format using the Format-Table cmdlet. + + .EXAMPLE + Retrieve all Azure resources and their types in the current context. + Get-CsLogCategory + + Retrieve all Azure resources and their types in the current context with logs only. + Get-CsLogCategory -LogOnly + + .PARAMETER LogOnly + Si ce paramètre est spécifié, le script retournera uniquement les ressources pour lesquelles il y a des logs de type 'log'. + + .PARAMETER MetricOnly + Si ce paramètre est spécifié, le script retournera uniquement les ressources pour lesquelles il y a des métriques. + + .NOTES + Make sure you are logged in to your Azure account and have selected the correct subscription before running this script. + #> + + param ( + [Parameter(Mandatory = $false)] + [switch]$LogOnly, + + [Parameter(Mandatory = $false)] + [switch]$MetricOnly + ) + + if ($LogOnly -and $MetricOnly) { + throw "The LogOnly and MetricOnly parameters cannot be used at the same time." + } + + [ListOfLogCategory]::Clear() + + $resources = Get-AzResource | Select-Object SubscriptionId, ResourceType -Unique + $total = $resources.Count + $current = 0 + + $resources | ForEach-Object { + $current++ + Write-Progress -Activity "Processing resources" -Status "Resource $current of $total $($_.ResourceTypeName)" -PercentComplete ($current / $total * 100) + + $Resource = [LogCategoryObj]::new(@{ + ContainerId = $_.SubscriptionId + ResourceTypeName = $_.ResourceType + SourceType = 'Az' + }) + [ListOfLogCategory]::Add($Resource) + } + + if ($LogOnly) { + $ListOfLogCategory = [ListOfLogCategory]::FindAll({ param($r) $r.LogCategory.Length -gt 0 }) + } + elseif ($MetricOnly) { + $ListOfLogCategory = [ListOfLogCategory]::FindAll({ param($r) $r.MetricCategory.Length -gt 0 }) + } + else { + $ListOfLogCategory = [ListOfLogCategory]::LogCategoryObj + } + + Write-Output $ListOfLogCategory | Format-Table -AutoSize +} \ No newline at end of file diff --git a/tests/QA/module.tests.ps1 b/tests/QA/module.tests.ps1 index 439914c..2ed80e4 100644 --- a/tests/QA/module.tests.ps1 +++ b/tests/QA/module.tests.ps1 @@ -92,7 +92,7 @@ Describe 'Changelog Management' -Tag 'Changelog' { } } -Describe 'General module control' -Tags 'FunctionalQuality' { +Describe 'General module control' -Tag 'FunctionalQuality' { It 'Should import without errors' { { Import-Module -Name $script:moduleName -Force -ErrorAction Stop } | Should -Not -Throw @@ -121,7 +121,7 @@ BeforeDiscovery { } } -Describe 'Quality for module' -Tags 'TestQuality' { +Describe 'Quality for module' -Tag 'TestQuality' { BeforeDiscovery { if (Get-Command -Name Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue) { @@ -140,9 +140,9 @@ Describe 'Quality for module' -Tags 'TestQuality' { } } - It 'Should have a unit test for ' -ForEach $testCases { - Get-ChildItem -Path 'tests\' -Recurse -Include "$Name.Tests.ps1" | Should -Not -BeNullOrEmpty - } + # It 'Should have a unit test for ' -ForEach $testCases { + # Get-ChildItem -Path 'tests\' -Recurse -Include "$Name.Tests.ps1" | Should -Not -BeNullOrEmpty + # } It 'Should pass Script Analyzer for ' -ForEach $testCases -Skip:(-not $scriptAnalyzerRules) { $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1" @@ -154,7 +154,7 @@ Describe 'Quality for module' -Tags 'TestQuality' { } } -Describe 'Help for module' -Tags 'helpQuality' { +Describe 'Help for module' -Tag 'helpQuality' { It 'Should have .SYNOPSIS for ' -ForEach $testCases { $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1"