Azure.Admin.Console.psm1
|
#Requires -Version 7.2 Set-StrictMode -Version Latest $moduleRoot = $PSScriptRoot # $PSScriptRoot inside a dot-sourced *.ps1 resolves to that file's own folder, not # the module root, so any function that needs the module root (e.g. to find the # bundled Tests folder or the vendored Spectre.Console.dll) reads this instead. $script:AACModuleRoot = $moduleRoot # The active sign-in, set by Connect-AAC and read/refreshed by Get-AACAccessToken. # Module-scoped, so its access and refresh tokens can't be read (or # replaced) by other code in the session - only the module's own functions # see it. Declared here rather than left implicitly $null because # Set-StrictMode -Version Latest treats reading a variable that was never # assigned as an error. $script:AACSession = $null # Set by Import-AACPdfLibrary once the PDF assemblies in .\lib\pdf are loaded # (only when a PDF report is first exported). $script:AACPdfLibraryLoaded = $false # The live Spectre.Console progress display (Invoke-AACProgress) and its # tasks by Id (Update-AACProgress); $null when no display is running. $script:AACProgressContext = $null $script:AACProgressTasks = @{} # True while Invoke-AACProgress runs without a live display (output is not # an interactive terminal): finished tasks are then written as plain lines. $script:AACProgressPlain = $false # Set once Invoke-AACProgress has told the user how to get the Unicode display. $script:AACUnicodeHintShown = $false # The resource map's page template, ELK and icons (Get-AACResourceMapAsset), # read and checked once per session. $script:AACResourceMapAsset = $null # The HttpClient Invoke-AACArmParallel sends its requests through - one per # session, so connections are pooled and reused. $script:AACHttpClient = $null # This module renders its UI with Spectre.Console (https://spectreconsole.net) # loaded directly from the vendored DLL in .\lib - no PowerShell wrapper module # in between. That DLL must be loaded before anything else, because a function # whose parameters are typed as a Spectre.Console type (e.g. [Spectre.Console.Color]) # needs that type resolvable at *parse* time, not just when the function runs. $spectreConsolePath = Join-Path -Path $moduleRoot -ChildPath 'lib/Spectre.Console.dll' if (-not (Test-Path -LiteralPath $spectreConsolePath)) { throw "Azure Admin Console could not find its vendored Spectre.Console.dll at '$spectreConsolePath'. Reinstall the module." } # Pinned SHA-256 of the vendored Spectre.Console 0.49.1 - a swapped or # tampered DLL is refused rather than loaded. Update it together with the DLL. $spectreConsoleHash = '26BB8DC1CA36D7ADA9979FFC9DDF658FF48068CEE0154AC540A8870FA7434359' # .NET's SHA-256 rather than Get-FileHash: the same check, about 70 ms sooner at every import. if ([System.Convert]::ToHexString([System.Security.Cryptography.SHA256]::HashData([System.IO.File]::ReadAllBytes($spectreConsolePath))) -ne $spectreConsoleHash) { throw "Azure Admin Console refused to load '$spectreConsolePath': its SHA-256 hash doesn't match the Spectre.Console.dll this module was released with. Reinstall the module." } try { Add-Type -Path $spectreConsolePath -ErrorAction Stop } catch { throw "Azure Admin Console failed to load Spectre.Console.dll: $($_.Exception.Message)" } # The module is a parent with child areas under src\ - Core (sign-in, the REST # helpers, views and reports), then Azure, M365 and DevOps - each with its own # Public\ (one exported command per file) and Private\ (helpers). An area may # use Core and itself, not another area (Tests\Core\Module.Tests.ps1 checks # it); src\<Area>\area.psd1 says what the area is and its load order. # # The packaged module (build.ps1) carries every function in one file, which # loads 1.5-2 seconds faster than ~400 files dot-sourced one by one; it also # sets $publicFunctionNames. The repository loads the files themselves - area # by area (Core first), each area's Private helpers before its commands. $bundlePath = Join-Path -Path $moduleRoot -ChildPath 'Azure.Admin.Console.Functions.ps1' if (Test-Path -LiteralPath $bundlePath) { try { . $bundlePath } catch { throw "Azure Admin Console failed to load its functions ('$bundlePath'): $($_.Exception.Message) Reinstall the module." } } else { $areas = @(Get-ChildItem -LiteralPath (Join-Path -Path $moduleRoot -ChildPath 'src') -Directory | Where-Object { Test-Path -LiteralPath (Join-Path -Path $_.FullName -ChildPath 'area.psd1') } | Sort-Object -Property @{ Expression = { [int](Import-PowerShellDataFile -LiteralPath (Join-Path -Path $_.FullName -ChildPath 'area.psd1')).Order } }, Name) $publicFunctionNames = [System.Collections.Generic.List[string]]::new() foreach ($area in $areas) { foreach ($folder in 'Private', 'Public') { $folderPath = Join-Path -Path $area.FullName -ChildPath $folder if (-not (Test-Path -LiteralPath $folderPath)) { continue } $files = Get-ChildItem -LiteralPath $folderPath -Filter '*.ps1' -File | Sort-Object -Property Name foreach ($file in $files) { try { . $file.FullName } catch { throw "Azure Admin Console failed to load '$($file.FullName)': $($_.Exception.Message)" } if ($folder -eq 'Public') { $publicFunctionNames.Add($file.BaseName) } } } } } # What each AAC.* type shows at the console by default (Select-Object * or # Format-List * shows everything) - registered in one Update-TypeData call below: # a call per type rebuilt the session's type table 113 times, about 1.4 seconds # of every import. $displayProperties = [ordered]@{ # Get-AACFirewallRule's objects carry ~35 properties (all of them go to # CSV); at the console show a readable table by default. Select-Object * or # Format-List * shows everything. 'AAC.FirewallRule' = 'FirewallPolicy', 'RuleCollection', 'RuleName', 'Action' # Same for Get-AACAdvisorRecommendation's ~25 (or more, with Ext_ columns). 'AAC.AdvisorRecommendation' = 'Category', 'Impact', 'ResourceName', 'Problem' # Invoke-AACApplicationInsightQuery's ~30 properties per exception. 'AAC.ApplicationInsightsException' = 'TimeGenerated', 'Severity', 'ExceptionType', 'Message', 'AppRoleName' # Invoke-AACPSRule's ~18 properties. 'AAC.PSRuleResult' = 'Outcome', 'RuleName', 'ResourceName', 'Reason' # Show-AACCost -PassThru: one property per month on top of these. # Get-AACTerraformPlan's ~20 properties per resource change. 'AAC.TerraformChange' = 'Action', 'Address', 'ResourceName', 'ChangedAttributes' # Invoke-AACLogAnalyticsWorkspaceAssessment: one object per workspace, its sections as properties. 'AAC.LogAnalyticsWorkspaceAssessment' = 'Name', 'PricingTier', 'RetentionDays', 'BillableGB', 'AverageDailyGB', 'Recommendations' 'AAC.LogAnalyticsTable' = 'Table', 'Billing', 'BillableGB', 'NonBillableGB', 'SharePercent', 'Plan', 'RetentionDays' 'AAC.LogAnalyticsRecommendation' = 'Severity', 'Category', 'Recommendation', 'Action' # Get-AACDiagnosticSetting: coverage per resource, one row per setting, findings. 'AAC.DiagnosticCoverage' = 'Status', 'Resource', 'ResourceType', 'Workspaces', 'MissingCategories' 'AAC.DiagnosticSettingDetail' = 'Resource', 'Setting', 'Destinations', 'Workspace', 'LogsEnabled' 'AAC.DiagnosticFinding' = 'Severity', 'Finding', 'Resource', 'Detail' # Deploy-AACStorageAccount: the deployment, its changes, gates and apply results. 'AAC.StorageDeployment' = 'Name', 'Status', 'Writes', 'ResourceGroupName', 'Location' 'AAC.StorageChange' = 'Order', 'Action', 'Resource', 'Reason' 'AAC.StorageGate' = 'Outcome', 'Gate', 'Resource', 'Item', 'Detail' 'AAC.StorageApplyResult' = 'Status', 'Resource', 'Action', 'Seconds', 'Detail' # Invoke-AACAksAssessment: clusters, node pools, settings, checks, findings, policy views. 'AAC.AksCluster' = 'Name', 'Version', 'Support', 'Tier', 'Nodes', 'WafScore', 'High', 'Medium', 'Low', 'PolicyViolations' 'AAC.AksNodePool' = 'Cluster', 'Pool', 'Mode', 'VmSize', 'Nodes', 'Autoscale', 'Zones', 'Version', 'NodeImageAge' 'AAC.AksSetting' = 'Cluster', 'Area', 'Setting', 'Value' 'AAC.AksCheck' = 'Cluster', 'Pillar', 'Check', 'Status', 'Severity' 'AAC.AksFinding' = 'Severity', 'Pillar', 'Source', 'Cluster', 'Check' 'AAC.AksUpgrade' = 'Cluster', 'Component', 'Current', 'Support', 'Available' 'AAC.AksPolicyViolation' = 'Cluster', 'Namespace', 'Workload', 'Component', 'Policy', 'Effect' 'AAC.AksPolicyNamespace' = 'Namespace', 'Violations', 'Workloads', 'Policies', 'Deny' 'AAC.AksPolicyWorkload' = 'Cluster', 'Namespace', 'WorkloadKind', 'Workload', 'Violations', 'Policies' 'AAC.AksPolicySummary' = 'Policy', 'Effect', 'Violations', 'Clusters', 'Namespaces', 'Workloads' # Invoke-AACPolicyAssessment: assignments, compliance, exemptions, definitions, findings. 'AAC.PolicyAssignmentReport' = 'Assignment', 'Scope', 'Kind', 'Enforcement', 'CompliancePercent', 'Rating', 'NonCompliant', 'Exemptions' 'AAC.PolicyAssignmentCompliance' = 'Assignment', 'Subscription', 'CompliancePercent', 'Rating', 'NonCompliant', 'Resources' 'AAC.PolicyAssessmentPolicy' = 'Assignment', 'Policy', 'Effect', 'Category', 'CompliancePercent', 'NonCompliant' 'AAC.PolicyInitiativeMember' = 'Assignment', 'Policy', 'Effect', 'Parameters', 'CompliancePercent', 'NonCompliant' 'AAC.PolicyCategory' = 'Category', 'Policies', 'Assignments', 'CompliancePercent', 'Rating', 'NonCompliant' 'AAC.PolicySubscription' = 'Subscription', 'ManagementGroups', 'CompliancePercent', 'Rating', 'NonCompliant', 'Assignments', 'Exemptions' 'AAC.PolicyManagementGroup' = 'ManagementGroup', 'Parent', 'CompliancePercent', 'Rating', 'Assignments', 'Subscriptions' 'AAC.PolicyInitiative' = 'Initiative', 'PolicyType', 'Category', 'Policies', 'Assigned', 'Deprecated' 'AAC.PolicyDefinitionReport' = 'Definition', 'PolicyType', 'Category', 'Effect', 'Assigned', 'Deprecated' 'AAC.PolicyExemptionReport' = 'Exemption', 'Assignment', 'Scope', 'Category', 'ExpiresOn', 'Status' 'AAC.PolicyRoleAssignment' = 'Assignment', 'Identity', 'Role', 'Scope', 'Required' 'AAC.PolicyFinding' = 'Severity', 'Area', 'Finding', 'Item', 'Scope' # Invoke-AACDefenderAssessment: the assessment, and the rows of each of its tables. 'AAC.DefenderAssessment' = 'SecureScore', 'Subscriptions', 'Findings', 'Recommendations', 'AttackPaths', 'Alerts', 'Inventory' 'AAC.DefenderFinding' = 'Severity', 'Area', 'Finding', 'Item', 'SubscriptionName' 'AAC.DefenderSubscription' = 'Subscription', 'SecureScore', 'PlansOn', 'UnhealthyResources', 'AttackPaths', 'ActiveAlerts', 'Findings' 'AAC.DefenderRecommendation' = 'Recommendation', 'Severity', 'RiskLevel', 'Status', 'UnhealthyResources', 'HealthyResources', 'Control' 'AAC.DefenderUnhealthyResource' = 'Resource', 'Recommendation', 'Severity', 'RiskLevel', 'SubscriptionName' 'AAC.DefenderAttackPath' = 'AttackPath', 'RiskLevel', 'EntryPoint', 'Target', 'Steps' 'AAC.DefenderAlert' = 'Alert', 'Severity', 'Status', 'Resource', 'Tactics', 'TimeGenerated' 'AAC.DefenderResource' = 'Resource', 'Type', 'Plan', 'PlanState', 'Unhealthy', 'High', 'Vulnerabilities', 'Alerts', 'AttackPaths' 'AAC.DefenderVulnerability' = 'Vulnerability', 'Severity', 'CVEs', 'Resource', 'Patchable' 'AAC.DefenderPlanReport' = 'SubscriptionName', 'Plan', 'State', 'SubPlan', 'Resources' 'AAC.DefenderSetting' = 'SubscriptionName', 'Setting', 'Value', 'Status' 'AAC.DefenderComplianceControl' = 'Standard', 'Control', 'State', 'FailedAssessments', 'SubscriptionName' # Invoke-AACM365Assessment: the assessment, and the rows of its tables. 'AAC.M365Assessment' = 'Tenant', 'SecureScore', 'Findings', 'ConditionalAccess', 'RoleAssignments', 'ManagedDevices', 'Permissions' 'AAC.M365Finding' = 'Severity', 'Area', 'Finding', 'Item' 'AAC.M365Setting' = 'Area', 'Setting', 'Value', 'Status' 'AAC.M365ConditionalAccessPolicy' = 'Policy', 'State', 'Users', 'Applications', 'Grant' 'AAC.M365RoleAssignment' = 'Role', 'Principal', 'Assignment', 'Privileged', 'MfaRegistered' 'AAC.M365UserRegistration' = 'UserPrincipalName', 'Admin', 'MfaRegistered', 'DefaultMethod' 'AAC.M365SecureScoreControl' = 'Control', 'Status', 'Score', 'MaxScore', 'Category' 'AAC.M365ManagedDevice' = 'Device', 'User', 'OS', 'Compliance', 'Encrypted', 'LastSync', 'Stale' 'AAC.M365EntraDevice' = 'Device', 'OS', 'Join', 'Managed', 'LastSignIn', 'Stale' 'AAC.M365Permission' = 'Data', 'Status', 'Permission' 'AAC.M365MfaCoverage' = 'Scope', 'Users', 'MfaPercent', 'PhishingResistantPercent', 'NoMethod' 'AAC.M365EmergencyAccount' = 'UserPrincipalName', 'DetectedBy', 'GlobalAdministrator', 'CloudOnly', 'PhishingResistant' 'AAC.M365PrivilegedAccount' = 'Principal', 'Roles', 'Dangling', 'Issues', 'Overlap' 'AAC.M365AppCredential' = 'App', 'Type', 'Status', 'End', 'DaysLeft', 'ValidityDays' 'AAC.M365AppPermission' = 'App', 'Permission', 'Kind', 'Risk', 'Owner' 'AAC.M365RedirectUri' = 'App', 'Uri', 'Resolves', 'Issue', 'Severity' 'AAC.M365LegacySignIn' = 'Protocol', 'Successful', 'Failed', 'Users', 'Latest' 'AAC.M365PimRoleSetting' = 'Role', 'MfaOnActivation', 'Approval', 'MaxActivationHours', 'PermanentActive' 'AAC.M365GroupExposure' = 'Group', 'Why', 'Members', 'Guests', 'Dynamic' 'AAC.M365AccessReview' = 'Review', 'Covers', 'Status', 'Recurrence' 'AAC.M365RiskyUser' = 'UserPrincipalName', 'RiskLevel', 'RiskState', 'Updated' 'AAC.M365RiskDetection' = 'Detection', 'Detections', 'High', 'Users', 'Latest' 'AAC.M365Incident' = 'Incident', 'Severity', 'Status', 'AssignedTo', 'AgeDays' 'AAC.M365InactiveUser' = 'UserPrincipalName', 'Products', 'LastActivity' 'AAC.M365AppProtectionPolicy' = 'Policy', 'Platform', 'Assigned', 'PinRequired', 'SendDataTo' 'AAC.M365Coverage' = 'Area', 'Lens', 'Status', 'Reads', 'Reason' # Invoke-AACAssessment: the run's summary (its sheets are plain rows). 'AAC.Assessment' = 'Subscriptions', 'Resources', 'ResourceTypes', 'AdvisorHigh', 'Retirements', 'SecurityHigh', 'ReportFolder' # Invoke-AACVirtualNetworkAssessment: networks, subnets, peerings, free ranges, findings. 'AAC.VirtualNetwork' = 'Name', 'Role', 'AddressSpace', 'UsedIps', 'AvailableIps', 'UsedPercent', 'SubnetCount', 'PeeringCount', 'High', 'Medium', 'Low' 'AAC.VirtualNetworkSubnet' = 'VirtualNetwork', 'Subnet', 'Prefix', 'Usable', 'Used', 'Available', 'UsedPercent', 'Nsg', 'Outbound' 'AAC.VirtualNetworkPeering' = 'VirtualNetwork', 'Peering', 'RemoteVirtualNetwork', 'State', 'Sync', 'UseRemoteGateways', 'AllowGatewayTransit' 'AAC.VirtualNetworkFreeRange' = 'VirtualNetwork', 'AddressSpace', 'Prefix', 'Size' 'AAC.VirtualNetworkFinding' = 'Severity', 'Category', 'Finding', 'VirtualNetwork', 'Item', 'Action' 'AAC.VirtualNetworkPrivateEndpoint' = 'VirtualNetwork', 'Subnet', 'PrivateEndpoint', 'Target', 'Groups', 'Status', 'ZoneLinked' 'AAC.ApplicationSecurityGroupUse' = 'Asg', 'Nics', 'Rules', 'VirtualNetworks', 'ResourceGroup' 'AAC.SubscriptionCost' = 'SubscriptionName', 'Currency', 'MonthToDate', 'Total', 'TopServices', 'Status' # Show-AACDashboard: the status and headline first. 'AAC.Dashboard' = 'Status', 'Headline', 'Resources', 'Regions', 'AdvisorHigh', 'Health' 'AAC.DashboardChange' = 'Time', 'Change', 'Resource', 'ResourceGroup', 'ChangedBy' 'AAC.DashboardServiceEvent' = 'Type', 'Title', 'TrackingId', 'Started', 'Subscription' 'AAC.DashboardUnhealthyResource' = 'Resource', 'State', 'Summary', 'ResourceGroup' # The expert commands (issue 10): what matters first at the prompt. 'AAC.CostAnomaly' = 'Severity', 'Kind', 'Name', 'Subscription', 'Start', 'CostImpact', 'Currency' 'AAC.AttackPath' = 'Severity', 'Category', 'Resource', 'BlastRadius', 'Path' 'AAC.AccessAssignment' = 'Severity', 'Principal', 'PrincipalType', 'Role', 'Scope', 'Assignment', 'Recommendation' 'AAC.ChangeRecord' = 'Time', 'Severity', 'Category', 'Resource', 'Caller', 'Status', 'Detail' 'AAC.HealthCheck' = 'Status', 'Tier', 'Category', 'Check', 'Detail' 'AAC.ComplianceGap' = 'Severity', 'Phase', 'Framework', 'ControlId', 'FailingResources', 'Effort' 'AAC.FailoverReadiness' = 'Severity', 'Resource', 'Protection', 'Confidence', 'Rpo', 'Finding' 'AAC.DependencyNode' = 'Resource', 'Type', 'BlastRadius', 'Dependents', 'Redundant', 'SinglePointOfFailure' 'AAC.ResourceUtilization' = 'Category', 'Resource', 'Kind', 'CpuP95', 'MemoryP95', 'Trend', 'SuggestedSize', 'EstimatedSaving' 'AAC.ConfigurationDrift' = 'Severity', 'Resource', 'Category', 'Property', 'Detail', 'Origin', 'Strategy' # Security alerts, operations and troubleshooting (issue 8), and the resource export. 'AAC.SecurityAlert' = 'Severity', 'Status', 'Alert', 'Resource', 'Intent', 'AgeDays' 'AAC.ResourceHealth' = 'State', 'Resource', 'ResourceType', 'Summary', 'Hours', 'LastEvent' 'AAC.ServiceHealthEvent' = 'Status', 'EventType', 'Title', 'Regions', 'ImpactedResources', 'TrackingId' 'AAC.UnusedResource' = 'Severity', 'Category', 'Resource', 'Billed', 'AgeDays', 'MonthlyCost', 'ResourceGroup' 'AAC.ResourceError' = 'Time', 'Category', 'ErrorCode', 'Resource', 'Caller', 'Message' 'AAC.PrivateEndpoint' = 'Status', 'Endpoint', 'Target', 'GroupId', 'ConnectionState', 'ZoneLinked', 'Issues' 'AAC.PrivateEndpointFinding' = 'Severity', 'Finding', 'Resource', 'Detail' 'AAC.LoadBalancerBackend' = 'Health', 'Balancer', 'Pool', 'Backend', 'Address', 'Availability' 'AAC.LoadBalancer' = 'Health', 'Balancer', 'Kind', 'Sku', 'Backends', 'Unhealthy', 'Issues' 'AAC.LoadBalancerFinding' = 'Severity', 'Finding', 'Resource', 'Detail' 'AAC.LegacyAuthentication' = 'Severity', 'Area', 'Finding', 'Resource', 'ResourceGroup' 'AAC.OrphanedIdentity' = 'Severity', 'Kind', 'Identity', 'RoleAssignments', 'LastSignIn', 'Reason' 'AAC.DiagnosticBundle' = 'Path', 'Resources', 'Files', 'SizeKB', 'Redacted' 'AAC.DiagnosticBundleResource' = 'Status', 'Resource', 'ResourceType', 'Health', 'FailedOperations', 'Alerts', 'Issues' 'AAC.Resource' = 'Name', 'ResourceType', 'ResourceGroup', 'Subscription', 'Location', 'Details' } $typeData = foreach ($typeName in $displayProperties.Keys) { $type = [System.Management.Automation.Runspaces.TypeData]::new($typeName) $type.DefaultDisplayPropertySet = [System.Management.Automation.Runspaces.PropertySetData]::new([string[]]$displayProperties[$typeName]) $type } Update-TypeData -TypeData $typeData -Force Export-ModuleMember -Function $publicFunctionNames |