diff --git a/src/AzSK.AAD/0.9.0/AzSK.AAD.psd1 b/src/AzSK.AAD/0.9.0/AzSK.AAD.psd1 new file mode 100644 index 000000000..3fe299b80 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/AzSK.AAD.psd1 @@ -0,0 +1,125 @@ +# +# Module manifest for module 'AzSK' +# +# Generated by: Microsoft AzSK Team +# +# Generated on: 2017-May-16 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = '.\AzSK.AAD.psm1' + + # Version number of this module. + ModuleVersion = '0.9.0' + + # ID used to uniquely identify this module + GUID = 'b9bba5c3-9036-4163-b0b0-6d4e83519b0a' + + # Author of this module + Author = 'AzSK Team' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = '(c) 2019 Microsoft Corporation. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Security Checks for Azure Active Directory (Preview)' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.0' + + # Name of the Windows PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the Windows PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module + DotNetFrameworkVersion = '4.0' + + # Minimum version of the common language runtime (CLR) required by this module + CLRVersion = '4.0' + + # Processor architecture (None, X86, Amd64) required by this module + ProcessorArchitecture = 'None' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @( + @{ModuleName = 'Az.Accounts'; RequiredVersion = '1.2.1'} + @{ModuleName = 'AzureAD'; RequiredVersion = '2.0.2.4'} + ) + + # Assemblies that must be loaded prior to importing this module + RequiredAssemblies = @('.\Lib\Newtonsoft.Json.dll','.\Lib\Microsoft.ApplicationInsights.dll','.\Lib\Microsoft.IdentityModel.Clients.ActiveDirectory.dll') + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + NestedModules = @() + + # Functions to export from this module + FunctionsToExport = @( + 'Get-AzSKAADSecurityStatusTenant', 'Get-AzSKAADSecurityStatusUser', 'Set-AzSKMonitoringSettings', + 'Set-AzSKLocalAIOrgTelemetrySettings', 'Set-AzSKUsageTelemetryLevel' + ) + + # Cmdlets to export from this module + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = '*' + + # Aliases to export from this module + # AliasesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess + PrivateData = @{ + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = 'AAD', 'AzSK', 'AzureActiveDirectory', 'AADSecurity' + + # A URL to the license for this module. + LicenseUri = 'https://github.com/azsk/azsk-docs/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/azsk/azsk-docs' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = ' + * Azure Active Directory (AAD) security controls for + * Users + * Apps & SPNs + * Various tenant wide settings + * Etc. ' + + } + } # End of PSData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + DefaultCommandPrefix = '' + +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/AzSK.AAD.psm1 b/src/AzSK.AAD/0.9.0/AzSK.AAD.psm1 new file mode 100644 index 000000000..e7241f3ad --- /dev/null +++ b/src/AzSK.AAD/0.9.0/AzSK.AAD.psm1 @@ -0,0 +1,346 @@ + +Set-StrictMode -Version Latest + +. $PSScriptRoot\Framework\Framework.ps1 + +@("$PSScriptRoot\SVT") | + ForEach-Object { + (Get-ChildItem -Path $_ -Recurse -File -Include "*.ps1") | + ForEach-Object { + . $_.FullName + } +} + +function Set-AzSKAADPolicySettings { + <# + .SYNOPSIS + This command would help to set online policy store URL. + .DESCRIPTION + This command would help to set online policy store URL. + + .PARAMETER ScannerToolPath + Provide the credential scanner tool path + .PARAMETER ScannerToolName + Provide the credential scanner tool name. + + .LINK + https://aka.ms/azskossdocs + + #> + Param( + [Parameter(Mandatory = $false, HelpMessage = "Provide scanner tool path")] + [string] + [Alias("stp")] + $ScannerToolPath, + + [Parameter(Mandatory = $false, HelpMessage = "Provide scanner tool name")] + [string] + [Alias("stn")] + $ScannerToolName + + ) + Begin { + [CommandHelper]::BeginCommand($PSCmdlet.MyInvocation); + [ListenerHelper]::RegisterListeners(); + } + Process { + try { + + $azskSettings = [ConfigurationManager]::GetLocalAzSKSettings(); + if($ScannerToolPath -and $ScannerToolName) + { + $azskSettings.ScanToolPath = $ScannerToolPath + $azskSettings.ScanToolName = $ScannerToolName + } + + [ConfigurationManager]::UpdateAzSKSettings($azskSettings); + [EventBase]::PublishGenericCustomMessage("Successfully configured policy settings. `nStart a fresh PS console/session to ensure any policy updates are (re-)loaded.", [MessageType]::Warning); + } + catch { + [EventBase]::PublishGenericException($_); + } + } + End { + [ListenerHelper]::UnregisterListeners(); + } +} + +function Set-AzSKLocalAIOrgTelemetrySettings { + <# + .SYNOPSIS + This command would help to set local control telemetry settings. + .DESCRIPTION + This command would help to set local control telemetry settings. + + .PARAMETER LocalAIOrgTelemetryKey + Provide local telemetry key. + .PARAMETER EnableLocalAIOrgTelemetry + Enables local control telemetry. + .LINK + https://aka.ms/azskossdocs + + #> + Param( + [Parameter(Mandatory = $true, HelpMessage = "Provide the local control telemetry key")] + [string] + [Alias("lotk")] + $LocalAIOrgTelemetryKey, + + [Parameter(Mandatory = $true, HelpMessage = "Provide the flag to enable local control telemetry")] + [bool] + [Alias("elot")] + $EnableLocalAIOrgTelemetry + ) + Begin { + [CommandHelper]::BeginCommand($PSCmdlet.MyInvocation); + [ListenerHelper]::RegisterListeners(); + } + Process { + try { + #TODO: This should support both params as optional (we can always throw an error if neither is provided) + #TODO: That is, if a key is provided, assume bEnable=$true...else look for bEnabled and toggle telemetry. + $azskSettings = [ConfigurationManager]::GetLocalAzSKSettings(); + $azskSettings.LocalControlTelemetryKey = $LocalAIOrgTelemetryKey + $azskSettings.LocalEnableControlTelemetry = $EnableLocalAIOrgTelemetry + [ConfigurationManager]::UpdateAzSKSettings($azskSettings); + [EventBase]::PublishGenericCustomMessage("Successfully set control telemetry settings"); + } + catch { + [EventBase]::PublishGenericException($_); + } + } + End { + [ListenerHelper]::UnregisterListeners(); + } +} + +function Set-AzSKUsageTelemetryLevel { + <# + .SYNOPSIS + This command would help to set telemetry level. + .DESCRIPTION + This command would help to set telemetry level. + + .PARAMETER Level + Provide the telemetry level + .LINK + https://aka.ms/azskossdocs + + #> + Param( + [Parameter(Mandatory = $true, HelpMessage = "Provide the telemetry level")] + [ValidateSet("None", "Anonymous")] + [string] + [Alias("lvl")] + $Level + ) + Begin { + [CommandHelper]::BeginCommand($PSCmdlet.MyInvocation); + [ListenerHelper]::RegisterListeners(); + } + Process { + try { + $azskSettings = [ConfigurationManager]::GetLocalAzSKSettings(); + $azskSettings.UsageTelemetryLevel = $Level + [ConfigurationManager]::UpdateAzSKSettings($azskSettings); + [EventBase]::PublishGenericCustomMessage("Successfully set usage telemetry level"); + } + catch { + [EventBase]::PublishGenericException($_); + } + } + End { + [ListenerHelper]::UnregisterListeners(); + } +} + +function Set-AzSKMonitoringSettings +{ + <# + .SYNOPSIS + This command would help in updating the Log Analytics configuration settings under the current powershell session. + .DESCRIPTION + This command will update the Log Analytics settings under the current powershell session. This also remembers the current settings and use them in the subsequent sessions. + + .PARAMETER OMSWorkspaceID + Workspace ID of your Log Analytics instance. Control scan results get pushed to this instance. + .PARAMETER OMSSharedKey + Shared key of your Log Analytics instance. + .PARAMETER AltOMSWorkspaceID + Workspace ID of your alternate Log Analytics instance. Control scan results get pushed to this instance. + .PARAMETER AltOMSSharedKey + Workspace shared key of your alternate Log Analytics instance. + .PARAMETER Source + Provide the source of Log Analytics Events. (e. g. CA,CICD,SDL) + .PARAMETER Disable + Use -Disable option to clean the Log Analytics setting under the current instance. + + .LINK + https://aka.ms/azskossdocs + + #> + [Alias("Set-AzSKOMSSettings")] + param( + + [Parameter(Mandatory = $false, HelpMessage="Workspace ID of your Log Analytics instance. Control scan results get pushed to this instance.", ParameterSetName = "Setup")] + [AllowEmptyString()] + [string] + [Alias("owid","wid","WorkspaceID")] + $OMSWorkspaceID, + + [Parameter(Mandatory = $false, HelpMessage="Shared key of your Log Analytics instance.", ParameterSetName = "Setup")] + [AllowEmptyString()] + [string] + [Alias("okey","wkey","SharedKey")] + $OMSSharedKey, + + [Parameter(Mandatory = $false, HelpMessage="Workspace ID of your alternate Log Analytics instance. Control scan results get pushed to this instance.", ParameterSetName = "Setup")] + [AllowEmptyString()] + [string] + [Alias("aowid","awid","AltWorkspaceID")] + $AltOMSWorkspaceID, + + [Parameter(Mandatory = $false, HelpMessage="Shared key of your alternate Log Analytics instance.", ParameterSetName = "Setup")] + [AllowEmptyString()] + [string] + [Alias("aokey","awkey","AltSharedKey")] + $AltOMSSharedKey, + + [Parameter(Mandatory = $false, HelpMessage="Provide the source of Log Analytics Events.(e.g. CC,CICD,SDL)", ParameterSetName = "Setup")] + [AllowEmptyString()] + [string] + [Alias("so")] + $Source, + + [Parameter(Mandatory = $true, HelpMessage="Use -Disable option to clean the Log Analytics setting under the current instance.", ParameterSetName = "Disable")] + [switch] + [Alias("dsbl")] + $Disable + + ) + Begin + { + [CommandHelper]::BeginCommand($PSCmdlet.MyInvocation); + [ListenerHelper]::RegisterListeners(); + } + Process + { + try + { + $appSettings = [ConfigurationManager]::GetLocalAzSKSettings(); + if(-not $Disable) + { + if(-not [string]::IsNullOrWhiteSpace($OMSWorkspaceID) -and -not [string]::IsNullOrWhiteSpace($OMSSharedKey)) + { + $appSettings.OMSWorkspaceId = $OMSWorkspaceID + $appSettings.OMSSharedKey = $OMSSharedKey + } + elseif(([string]::IsNullOrWhiteSpace($OMSWorkspaceID) -and -not [string]::IsNullOrWhiteSpace($OMSSharedKey)) ` + -and (-not [string]::IsNullOrWhiteSpace($OMSWorkspaceID) -and [string]::IsNullOrWhiteSpace($OMSSharedKey))) + { + [EventBase]::PublishGenericCustomMessage("You need to send both the OMSWorkspaceId and OMSSharedKey", [MessageType]::Error); + return; + } + if(-not [string]::IsNullOrWhiteSpace($AltOMSWorkspaceID) -and -not [string]::IsNullOrWhiteSpace($AltOMSSharedKey)) + { + $appSettings.AltOMSWorkspaceId = $AltOMSWorkspaceID + $appSettings.AltOMSSharedKey = $AltOMSSharedKey + } + elseif(([string]::IsNullOrWhiteSpace($AltOMSWorkspaceID) -and -not [string]::IsNullOrWhiteSpace($AltOMSSharedKey)) ` + -and (-not [string]::IsNullOrWhiteSpace($AltOMSWorkspaceID) -and [string]::IsNullOrWhiteSpace($AltOMSSharedKey))) + { + [EventBase]::PublishGenericCustomMessage("You need to send both the AltOMSWorkspaceId and AltOMSSharedKey", [MessageType]::Error); + return; + } + } + else { + $appSettings.OMSWorkspaceId = "" + $appSettings.OMSSharedKey = "" + $appSettings.AltOMSWorkspaceId = "" + $appSettings.AltOMSSharedKey = "" + } + if(-not [string]::IsNullOrWhiteSpace($Source)) + { + $appSettings.OMSSource = $Source + } + else + { + $appSettings.OMSSource = "SDL" + } + $appSettings.OMSType = [OMSHelper]::DefaultOMSType + [ConfigurationManager]::UpdateAzSKSettings($appSettings); + [EventBase]::PublishGenericCustomMessage([Constants]::SingleDashLine + "`r`nWe have added new queries for the Monitoring solution. These will help reflect the aggregate control pass/fail status more accurately. Please go here to get them: https://aka.ms/devopskit/omsqueries `r`n",[MessageType]::Warning); + [EventBase]::PublishGenericCustomMessage("Successfully changed policy settings"); + } + catch + { + [EventBase]::PublishGenericException($_); + } + } + End + { + [ListenerHelper]::UnregisterListeners(); + } +} + +function Set-AzSKPrivacyNoticeResponse { + <# + .SYNOPSIS + This command would help to set user preferences for EULA and Privacy. + .DESCRIPTION + This command would help to set user preferences for EULA and Privacy. + + .PARAMETER AcceptPrivacyNotice + Provide the flag to suppress the Privacy notice prompt and submit the acceptance. (Yes/No) + + .LINK + https://aka.ms/azskossdocs + + #> + Param + ( + [Parameter(Mandatory = $true, HelpMessage = "Provide the flag to suppress the Privacy notice prompt and submit the acceptance. (Yes/No)")] + [string] + [ValidateSet("Yes", "No")] + [Alias("apn")] + $AcceptPrivacyNotice + ) + Begin { + [CommandHelper]::BeginCommand($PSCmdlet.MyInvocation); + [ListenerHelper]::RegisterListeners(); + } + Process { + try { + $azskSettings = [ConfigurationManager]::GetLocalAzSKSettings(); + + if ($AcceptPrivacyNotice -eq "yes") { + $azskSettings.PrivacyNoticeAccepted = $true + $azskSettings.UsageTelemetryLevel = "Anonymous" + } + + if ($AcceptPrivacyNotice -eq "no") { + $azskSettings.PrivacyNoticeAccepted = $false + $azskSettings.UsageTelemetryLevel = "None" + } + [ConfigurationManager]::UpdateAzSKSettings($azskSettings) + [EventBase]::PublishGenericCustomMessage("Successfully updated privacy settings."); + } + catch { + [EventBase]::PublishGenericException($_); + } + + } + End { + [ListenerHelper]::UnregisterListeners(); + } +} + +function Clear-AzSKSessionState { + + Write-Host "Clearing AzSK session state..." -ForegroundColor Yellow + [ConfigOverride]::ClearConfigInstance() + Write-Host "Session state cleared." -ForegroundColor Yellow + +} + +. $PSScriptRoot\Framework\Helpers\AliasHelper.ps1 diff --git a/src/AzSK.AAD/0.9.0/Framework/Abstracts/AzSKRoot.ps1 b/src/AzSK.AAD/0.9.0/Framework/Abstracts/AzSKRoot.ps1 new file mode 100644 index 000000000..1d7b9086e --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Abstracts/AzSKRoot.ps1 @@ -0,0 +1,145 @@ +Set-StrictMode -Version Latest +class AzSKRoot: EventBase +{ + [TenantContext] $TenantContext; + [bool] $RunningLatestPSModule = $true; + + AzSKRoot([string] $tenantId) + { + [Helpers]::AbstractClass($this, [AzSKRoot]); + + $aadCtx = [AccountHelper]::GetCurrentAADContext($tenantId) + + #If the user did not specify a tenantId, we determine it from AAD ctx (from the login session) + if ([string]::IsNullOrEmpty($tenantId)) + { + $tenantId = $aadCtx.TenantId + } + $this.TenantContext = [TenantContext] @{ + TenantId = $tenantId; + Scope = "/Organization/$tenantId"; + TenantName = $aadCtx.TenantDomain; + }; + } + + [PSObject] LoadServerConfigFile([string] $fileName) + { + return [ConfigurationManager]::LoadServerConfigFile($fileName); + } + + hidden [AzSKRootEventArgument] CreateRootEventArgumentObject() + { + return [AzSKRootEventArgument]@{ + TenantContext = $this.TenantContext; + }; + } + + hidden [void] PublishAzSKRootEvent([string] $eventType, [MessageData[]] $messages) + { + [AzSKRootEventArgument] $arguments = $this.CreateRootEventArgumentObject(); + + if($messages) + { + $arguments.Messages += $messages; + } + + $this.PublishEvent($eventType, $arguments); + } + + hidden [void] PublishAzSKRootEvent([string] $eventType, [string] $message, [MessageType] $messageType) + { + if (-not [string]::IsNullOrEmpty($message)) + { + [MessageData] $data = [MessageData]@{ + Message = $message; + MessageType = $messageType; + }; + $this.PublishAzSKRootEvent($eventType, $data); + } + else + { + [MessageData[]] $blankMessages = @(); + $this.PublishAzSKRootEvent($eventType, $blankMessages); + } + } + + hidden [void] PublishAzSKRootEvent([string] $eventType, [PSObject] $dataObject) + { + if ($dataObject) + { + [MessageData] $data = [MessageData]@{ + DataObject = $dataObject; + }; + $this.PublishAzSKRootEvent($eventType, $data); + } + else + { + [MessageData[]] $blankMessages = @(); + $this.PublishAzSKRootEvent($eventType, $blankMessages); + } + } + + [MessageData[]] PublishCustomMessage([MessageData[]] $messages) + { + if($messages) + { + $this.PublishAzSKRootEvent([AzSKRootEvent]::CustomMessage, $messages); + return $messages; + } + return @(); + } + [CustomData] PublishCustomData([CustomData] $CustomData) + { + if($CustomData) + { + $this.PublishAzSKRootEvent([AzSKRootEvent]::PublishCustomData, $CustomData); + return $CustomData; + } + return $null; + } + + [void] CommandProcessing([MessageData[]] $messages) + { + if($messages) + { + $this.PublishAzSKRootEvent([AzSKRootEvent]::CommandProcessing, $messages); + } + } + + [void] PublishRunIdentifier([System.Management.Automation.InvocationInfo] $invocationContext) + { + if($invocationContext) + { + $this.InvocationContext = $invocationContext; + } + $this.RunIdentifier = $this.GenerateRunIdentifier(); + $this.PublishAzSKRootEvent([AzSKRootEvent]::GenerateRunIdentifier, [MessageData]::new($this.RunIdentifier, $invocationContext)); + } + + [bool] IsLatestVersionConfiguredOnSub([String] $ConfigVersion,[string] $TagName,[string] $FeatureName) + { + $IsLatestVersionPresent = $this.IsLatestVersionConfiguredOnSub($ConfigVersion,$TagName) + if($IsLatestVersionPresent){ + $this.PublishCustomMessage("$FeatureName configuration in your subscription is already up to date. If you would like to reconfigure, please rerun the command with '-Force' parameter."); + } + return $IsLatestVersionPresent + } + + [bool] IsLatestVersionConfiguredOnSub([String] $ConfigVersion,[string] $TagName) + { + $IsLatestVersionPresent = $false + $tagsOnSub = [Helpers]::GetResourceGroupTags([ConfigurationManager]::GetAzSKConfigData().AzSKRGName) + if($tagsOnSub) + { + $SubConfigVersion= $tagsOnSub.GetEnumerator() | Where-Object {$_.Name -eq $TagName -and $_.Value -eq $ConfigVersion} + + if(($SubConfigVersion | Measure-Object).Count -gt 0) + { + $IsLatestVersionPresent = $true + } + } + return $IsLatestVersionPresent + } + + +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Abstracts/CommandBase.ps1 b/src/AzSK.AAD/0.9.0/Framework/Abstracts/CommandBase.ps1 new file mode 100644 index 000000000..ec5c5994c --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Abstracts/CommandBase.ps1 @@ -0,0 +1,375 @@ +using namespace System.Management.Automation +Set-StrictMode -Version Latest +# Base class for all classes being called from PS commands +# Provides functionality to fire important events at command call +class CommandBase: AzSKRoot { + [string[]] $FilterTags = @(); + [bool] $DoNotOpenOutputFolder = $false; + [bool] $Force = $false + [bool] $IsLocalComplianceStoreEnabled = $false + CommandBase([string] $tenantId, [InvocationInfo] $invocationContext): + Base($tenantId) { + [Helpers]::AbstractClass($this, [CommandBase]); + if (-not $invocationContext) { + throw [System.ArgumentException] ("The argument 'invocationContext' is null. Pass the `$PSCmdlet.MyInvocation from PowerShell command."); + } + $this.InvocationContext = $invocationContext; + [PrivacyNotice]::ValidatePrivacyAcceptance() + + if($null -ne $this.InvocationContext.BoundParameters["DoNotOpenOutputFolder"]) + { + $this.DoNotOpenOutputFolder = $this.InvocationContext.BoundParameters["DoNotOpenOutputFolder"]; + } + if($null -ne $this.InvocationContext.BoundParameters["Force"]) + { + $this.Force = $this.InvocationContext.BoundParameters["Force"]; + } + } + + [void] CommandStarted() { + $this.PublishAzSKRootEvent([AzSKRootEvent]::CommandStarted, $this.CheckModuleVersion()); + } + + [void] CommandError([System.Management.Automation.ErrorRecord] $exception) { + [AzSKRootEventArgument] $arguments = $this.CreateRootEventArgumentObject(); + $arguments.ExceptionMessage = $exception; + + $this.PublishEvent([AzSKRootEvent]::CommandError, $arguments); + } + + [void] CommandCompleted([MessageData[]] $messages) { + $this.PublishAzSKRootEvent([AzSKRootEvent]::CommandCompleted, $messages); + } + + [string] InvokeFunction([PSMethod] $methodToCall) { + return $this.InvokeFunction($methodToCall, @()); + } + + [string] InvokeFunction([PSMethod] $methodToCall, [System.Object[]] $arguments) { + if (-not $methodToCall) { + throw [System.ArgumentException] ("The argument 'methodToCall' is null. Pass the reference of method to call. e.g.: [YourClass]::new().YourMethod"); + } + + # Reset cached context + [AccountHelper]::ResetCurrentRmContext() #BUGBUG - why is this needed? (Create/call ResetAzContext accordingly) + + $this.PublishRunIdentifier($this.InvocationContext); + [AIOrgTelemetryHelper]::TrackCommandExecution("Command Started", + @{"RunIdentifier" = $this.RunIdentifier}, @{}, $this.InvocationContext); + $sw = [System.Diagnostics.Stopwatch]::StartNew(); + $isExecutionSuccessful = $true + $this.CommandStarted(); + $this.PostCommandStartedAction(); + $methodResult = @(); + try { + $methodResult = $methodToCall.Invoke($arguments); + } + catch { + $isExecutionSuccessful = $true + # Unwrapping the first layer of exception which is added by Invoke function + [AIOrgTelemetryHelper]::TrackCommandExecution("Command Errored", + @{"RunIdentifier" = $this.RunIdentifier; "ErrorRecord"= $_.Exception.InnerException.ErrorRecord}, + @{"TimeTakenInMs" = $sw.ElapsedMilliseconds; "SuccessCount" = 0}, + $this.InvocationContext); + $this.CommandError($_.Exception.InnerException.ErrorRecord); + } + + $this.CommandCompleted($methodResult); + [AIOrgTelemetryHelper]::TrackCommandExecution("Command Completed", + @{"RunIdentifier" = $this.RunIdentifier}, + @{"TimeTakenInMs" = $sw.ElapsedMilliseconds; "SuccessCount" = 1}, + $this.InvocationContext) + $this.PostCommandCompletedAction($methodResult); + + $folderPath = $this.GetOutputFolderPath(); + + #Generate PDF report + $GeneratePDFReport = $this.InvocationContext.BoundParameters["GeneratePDF"]; + + try { + if (-not [string]::IsNullOrEmpty($folderpath)) { + switch ($GeneratePDFReport) { + None { + # Do nothing + } + Landscape { + [AzSKPDFExtension]::GeneratePDF($folderpath, $this.TenantContext, $this.InvocationContext, $true); + } + Portrait { + [AzSKPDFExtension]::GeneratePDF($folderpath, $this.TenantContext, $this.InvocationContext, $false); + } + } + } + } + catch { + # Unwrapping the first layer of exception which is added by Invoke function + $this.CommandError($_); + } + + $AttestControlParamFound = $this.InvocationContext.BoundParameters["AttestControls"]; + if($null -eq $AttestControlParamFound) + { + if((-not $this.DoNotOpenOutputFolder) -and (-not [string]::IsNullOrEmpty($folderPath))) + { + try + { + Invoke-Item -Path $folderPath; + } + catch + { + #ignore if any exception occurs + } + } + } + return $folderPath; + + # Call clear temp folder function. + } + + [void] PostCommandStartedAction() + { + + } + + [string] GetOutputFolderPath() { + return [WriteFolderPath]::GetInstance().FolderPath; + } + + + [void] CheckModuleVersion() { + + $currentModuleVersion = [System.Version] $this.GetCurrentModuleVersion() + $serverVersion = [System.Version] ([ConfigurationManager]::GetAzSKConfigData().GetLatestAzSKVersion($this.GetModuleName())); + $currentModuleVersion = [System.Version] $this.GetCurrentModuleVersion() + if($currentModuleVersion -ne "0.0.0.0" -and $serverVersion -gt $this.GetCurrentModuleVersion()) { + $this.RunningLatestPSModule = $false; + $this.InvokeAutoUpdate() + $this.PublishCustomMessage(([Constants]::VersionCheckMessage -f $serverVersion), [MessageType]::Warning); + $this.PublishCustomMessage(([ConfigurationManager]::GetAzSKConfigData().InstallationCommand + "`r`n"), [MessageType]::Update); + $this.PublishCustomMessage([Constants]::VersionWarningMessage, [MessageType]::Warning); + + $serverVersions = @() + [ConfigurationManager]::GetAzSKConfigData().GetAzSKVersionList($this.GetModuleName()) | ForEach-Object { + #Take major and minor version and ignore build version for comparision + $serverVersions+= [System.Version] ("$($_.Major)" +"." + "$($_.Minor)") + } + $serverVersions = $serverVersions | Select-Object -Unique + $latestVersionList = $serverVersions | Where-Object {$_ -gt $currentModuleVersion} + if(($latestVersionList | Measure-Object).Count -gt [ConfigurationManager]::GetAzSKConfigData().BackwardCompatibleVersionCount) + { + throw ([SuppressedException]::new(("Your version of AzSK is too old. Please update now!"),[SuppressedExceptionType]::Generic)) + } + } + + $psGalleryVersion = [System.Version] ([ConfigurationManager]::GetAzSKConfigData().GetAzSKLatestPSGalleryVersion($this.GetModuleName())); + if($psGalleryVersion -ne $serverVersion) + { + $serverVersions = @() + [ConfigurationManager]::GetAzSKConfigData().GetAzSKVersionList($this.GetModuleName()) | ForEach-Object { + #Take major and minor version and ignore build version for comparision + $serverVersions+= [System.Version] ("$($_.Major)" +"." + "$($_.Minor)") + } + $serverVersions = $serverVersions | Select-Object -Unique + $latestVersionAvailableFromGallery = $serverVersions | Where-Object {$_ -gt $serverVersion} + if(($latestVersionAvailableFromGallery | Measure-Object).Count -gt [ConfigurationManager]::GetAzSKConfigData().BackwardCompatibleVersionCount) + { + $this.PublishCustomMessage("Your Org AzSK version[$serverVersion] is too old. Consider updating it to latest available version[$psGalleryVersion].",[MessageType]::Error); + } + } + + } + + [void] InvokeAutoUpdate() + { + $AutoUpdateSwitch= [ConfigurationManager]::GetAzSKSettings().AutoUpdateSwitch; + $AutoUpdateCommand = [ConfigurationManager]::GetAzSKSettings().AutoUpdateCommand; + + if($AutoUpdateSwitch -ne [AutoUpdate]::On) + { + if($AutoUpdateSwitch -eq [AutoUpdate]::NotSet) + { + Write-Host "Auto-update for AzSK is currently not enabled for your machine. To set it, run the command below:" -ForegroundColor Yellow + Write-Host "Set-AzSKPolicySettings -AutoUpdate On`n" -ForegroundColor Green + } + return; + } + + #Step 1: Get the list of active running powershell prcesses including the current running PS Session + $PSProcesses = Get-Process | Where-Object { ($_.Name -eq 'powershell' -or $_.Name -eq 'powershell_ise' -or $_.Name -eq 'powershelltoolsprocesshost')} + + $userChoice = "" + if(($PSProcesses | Measure-Object).Count -ge 1) + { + Write-Host "A new version of AzSK is available. Starting the auto-update workflow...`nTo prepare for auto-update, please:`n`t a) Save your work from all active PS sessions including the current one and`n`t b) Close all PS sessions other than the current one. " -ForegroundColor Cyan + } + + #User choice that captures the decision to close the active PS Sessions + $secondUserChoice ="" + $InvalidOption = $true; + while($InvalidOption) + { + if([string]::IsNullOrWhiteSpace($userChoice) -or ($userChoice.Trim() -ne 'y' -and $userChoice.Trim() -ne 'n')) + { + $userChoice = Read-Host "Continue (Y/N)" + if([string]::IsNullOrWhiteSpace($userChoice) -or ($userChoice.Trim() -ne 'y' -and $userChoice.Trim() -ne 'n')) + { + Write-Host "Enter the valid option." -ForegroundColor Yellow + } + continue; + } + elseif($userChoice.Trim() -eq 'n') + { + $InvalidOption = $false; + } + elseif($userChoice.Trim() -eq 'y') + { + #Get the number of PS active sessions + $PSProcesses = Get-Process | Where-Object { ($_.Name -eq 'powershell' -or $_.Name -eq 'powershell_ise' -or $_.Name -eq 'powershelltoolsprocesshost') -and $_.Id -ne $PID} + if(($PSProcesses | Measure-Object).Count -gt 0) + { + Write-Host "`nThe following other PS sessions are still active. Please save your work and close them. You can also use Task Manager to close these sessions." -ForegroundColor Yellow + Write-Host ($PSProcesses | Select-Object Id, ProcessName, Path | Out-String) + $secondUserChoice = Read-Host "Continue (Y/N)" + } + elseif(($PSProcesses | Measure-Object).Count -eq 0) + { + Write-Host "`nThe current PS session will be closed now. Have you saved your work?" -ForegroundColor Yellow + $secondUserChoice = Read-Host "Continue (Y/N)" + } + if(-not [string]::IsNullOrWhiteSpace($secondUserChoice) -and ` + (($PSProcesses | Measure-Object).Count -eq 0 -and $secondUserChoice.Trim() -eq 'y') -or ` + $secondUserChoice.Trim() -eq 'n') + { + $InvalidOption = $false; + } + } + } + #Check if the first user want to continue with auto-update using userChoice field and then check if user still wants to continue with auto-update after finding the active PS sessions. + #In either case it is no it would exit the auto-update process + if($userChoice.Trim() -eq "n" -or $secondUserChoice.Trim() -eq 'n') + { + Write-Host "Exiting auto-update workflow. To disable auto-update permanently, run the command below:" -ForegroundColor Yellow + Write-Host "Set-AzSKPolicySettings -AutoUpdate Off`n" -ForegroundColor Green + return + } + $AzSKTemp = [Constants]::AzSKAppFolderPath + "\Temp\"; + try + { + $fileName = "au_" + $(get-date).ToUniversalTime().ToString("yyyyMMdd_HHmmss") + ".ps1"; + + $autoUpdateContent = [ConfigurationHelper]::LoadOfflineConfigFile("ModuleAutoUpdate.ps1"); + if(-not (Test-Path -Path $AzSKTemp)) + { + mkdir -Path $AzSKTemp -Force + } + Remove-Item -Path "$AzSKTemp\au_*" -Force -Recurse -ErrorAction SilentlyContinue + + $autoUpdateContent = $autoUpdateContent.Replace("##installurl##",$AutoUpdateCommand); + $autoUpdateContent | Out-File "$AzSKTemp\$fileName" -Force + + Start-Process -WindowStyle Normal -FilePath "powershell.exe" -ArgumentList "$AzSKTemp\$fileName" + } + catch + { + $this.CommandError($_.Exception.InnerException.ErrorRecord); + } + } + + [void] CommandProgress([int] $totalItems, [int] $currentItem) { + $this.CommandProgress($totalItems, $currentItem, 1); + } + + [void] CommandProgress([int] $totalItems, [int] $currentItem, [int] $granularity) { + if ($totalItems -gt 0) { + # $granularity indicates the number of items after which percentage progress will be printed + # Set the max granularity to total items + if ($granularity -gt $totalItems) { + $granularity = $totalItems; + } + + # Conditions for posting progress: 0%, 100% and based on granularity + if ($currentItem -eq 0 -or $currentItem -eq $totalItems -or (($currentItem % $granularity) -eq 0)) { + $this.PublishCustomMessage("$([int](($currentItem / $totalItems) * 100))% Completed"); + } + } + } + + # Dummy function declaration to define the function signature + [void] PostCommandCompletedAction([MessageData[]] $messages) + { } + + [bool] ValidateOrgPolicyOnSubscription([bool] $Force) + { + $AzSKConfigData = [ConfigurationManager]::GetAzSKConfigData() + $tagsOnSub = [Helpers]::GetResourceGroupTags($AzSKConfigData.AzSKRGName) + $IsTagSettingRequired = $false + if($tagsOnSub) + { + $SubOrgTag= $tagsOnSub.GetEnumerator() | Where-Object {$_.Name -like "AzSKOrgName*"} + + if(($SubOrgTag | Measure-Object).Count -gt 0) + { + $OrgName =$SubOrgTag.Name.Split("_")[1] + if(-not [string]::IsNullOrWhiteSpace($OrgName) -and $OrgName -ne $AzSKConfigData.PolicyOrgName) + { + if($AzSKConfigData.PolicyOrgName -eq "org-neutral") + { + throw [SuppressedException]::new("The current subscription has been configured with DevOps kit policy for the '$OrgName' Org, However the DevOps kit command is running with a different ('$($AzSKConfigData.PolicyOrgName)') Org policy. `nPlease review FAQ at: https://aka.ms/devopskit/orgpolicy/faq and correct this condition depending upon which context(manual,CICD,CA scan) you are seeing this error. If FAQ does not help to resolve the issue, please contact your Org policy Owner ($($SubOrgTag.Value)).",[SuppressedExceptionType]::Generic) + + } + else + { + if(-not $Force) + { + $this.PublishCustomMessage("Warning: The current subscription has been configured with DevOps kit policy for the '$OrgName' Org, However the DevOps kit command is running with a different ('$($AzSKConfigData.PolicyOrgName)') Org policy. `nPlease review FAQ at: https://aka.ms/devopskit/orgpolicy/faq and correct this condition depending upon which context(manual,CICD,CA scan) you are seeing this error. If FAQ does not help to resolve the issue, please contact your Org policy Owner ($($SubOrgTag.Value)).",[MessageType]::Warning); + $IsTagSettingRequired = $false + } + } + } + } + elseif($AzSKConfigData.PolicyOrgName -ne "org-neutral"){ + $IsTagSettingRequired =$true + } + } + else { + $IsTagSettingRequired = $true + } + return $IsTagSettingRequired + } + + [void] SetOrgPolicyTag([bool] $Force) + { + try + { + $AzSKConfigData = [ConfigurationManager]::GetAzSKConfigData() + $tagsOnSub = [Helpers]::GetResourceGroupTags($AzSKConfigData.AzSKRGName) + if($tagsOnSub) + { + $SubOrgTag= $tagsOnSub.GetEnumerator() | Where-Object {$_.Name -like "AzSKOrgName*"} + if( + (($SubOrgTag | Measure-Object).Count -eq 0 -and $AzSKConfigData.PolicyOrgName -ne "org-neutral") -or + (($SubOrgTag | Measure-Object).Count -gt 0 -and $AzSKConfigData.PolicyOrgName -ne "org-neutral" -and $AzSKConfigData.PolicyOrgName -ne $SubOrgTag.Value -and $Force)) + { + if(($SubOrgTag | Measure-Object).Count -gt 0) + { + $SubOrgTag | ForEach-Object{ + [Helpers]::SetResourceGroupTags($AzSKConfigData.AzSKRGName,@{$_.Name=$_.Value}, $true) + } + } + $TagName = [Constants]::OrgPolicyTagPrefix +$AzSKConfigData.PolicyOrgName + $SupportMail = $AzSKConfigData.SupportDL + if(-not [string]::IsNullOrWhiteSpace($SupportMail) -and [Constants]::SupportDL -eq $SupportMail) + { + $SupportMail = "Not Available" + } + [Helpers]::SetResourceGroupTags($AzSKConfigData.AzSKRGName,@{$TagName=$SupportMail}, $false) + + } + + } + } + catch{ + # Exception occurred during setting tag. This is kept blank intentionaly to avoid flow break + } + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Abstracts/ComplianceBase.ps1 b/src/AzSK.AAD/0.9.0/Framework/Abstracts/ComplianceBase.ps1 new file mode 100644 index 000000000..b8c934373 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Abstracts/ComplianceBase.ps1 @@ -0,0 +1,49 @@ +# using namespace Microsoft.Azure.Management.Storage.Models +# Set-StrictMode -Version Latest +# class ComplianceBase +# { +# [TenantContext] $TenantContext; +# hidden [StorageHelper] $azskStorageInstance = $null; +# hidden [string] $ComplianceTableName = [Constants]::ComplianceReportTableName; + +# ComplianceBase([TenantContext] $TenantContext) +# { +# $this.TenantContext = $TenantContext +# $this.GetStorageHelperInstance(); +# } +# [StorageHelper] GetStorageHelperInstance() +# { +# if($null -eq $this.azskStorageInstance) +# { +# try { +# $azskStorageAccount = [UserSubscriptionDataHelper]::GetUserSubscriptionStorage(); + +# if(($azskStorageAccount | Measure-Object).Count -eq 1 -and $azskStorageAccount.Kind -ne [Kind]::StorageV2) +# { +# [UserSubscriptionDataHelper]::UpgradeBlobToV2Storage(); +# } +# $azskRGName = [UserSubscriptionDataHelper]::GetUserSubscriptionRGName(); +# if($azskStorageAccount) +# { +# $this.azskStorageInstance = [StorageHelper]::new($this.TenantContext.subscriptionId, $azskRGName,$azskStorageAccount.Location, $azskStorageAccount.Name, [Kind]::StorageV2); +# $this.azskStorageInstance.CreateTableIfNotExists([Constants]::ComplianceReportTableName); +# } +# } +# catch { +# #eat this exception as the storage account would be null in the case of exception +# } +# } +# return $this.azskStorageInstance +# } + +# hidden [bool] HaveRequiredPermissions() +# { +# if($null -eq $this.azskStorageInstance -or ($null -ne $this.azskStorageInstance -and $this.azskStorageInstance.HaveWritePermissions -eq 0)) +# { +# return $false; +# } +# else { +# return $true; +# } +# } +# } diff --git a/src/AzSK.AAD/0.9.0/Framework/Abstracts/EventBase.ps1 b/src/AzSK.AAD/0.9.0/Framework/Abstracts/EventBase.ps1 new file mode 100644 index 000000000..29f818c79 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Abstracts/EventBase.ps1 @@ -0,0 +1,144 @@ +using namespace System.Management.Automation +Set-StrictMode -Version Latest + +# Class for providing capability to fire events, +# also includes support to fire AzSKGenericEvent and holds InvocationContext +class EventBase +{ + [string] $RunIdentifier = "default"; + [InvocationInfo] $InvocationContext; + static [int] $logLvl = 0; + + [string] GenerateRunIdentifier() + { + return $(Get-Date -format "yyyyMMdd_HHmmss"); + } + + hidden [void] PublishEvent([string] $eventType, [PSObject] $eventArgument) + { + New-Event -SourceIdentifier $eventType ` + -Sender $this ` + -EventArguments $eventArgument | Out-Null + + if ([EventBase]::logLvl -ge 1){Write-Host -ForegroundColor Magenta "NewEvt type: $eventType from objType: $($this.GetType())"} + } + + [void] PublishException([ErrorRecord] $eventArgument) + { + $this.PublishEvent([AzSKGenericEvent]::Exception, $eventArgument); + } + + [MessageData[]] PublishCustomMessage([MessageData[]] $messageData) + { + if($messageData) + { + $this.PublishEvent([AzSKGenericEvent]::CustomMessage, $messageData); + return $messageData; + } + return @(); + } + + [MessageData[]] PublishCustomMessage([string] $message, [MessageType] $messageType) + { + return $this.PublishCustomMessage([MessageData]::new($message, $messageType)); + } + + [MessageData[]] PublishCustomMessage([string] $message, [PSObject] $dataObject) + { + return $this.PublishCustomMessage([MessageData]::new($message, $dataObject)); + } + + [MessageData[]] PublishCustomMessage([string] $message) + { + return $this.PublishCustomMessage($message, [MessageType]::Info); + } + + [string] GetModuleName() + { + if($this.InvocationContext) + { + return $this.InvocationContext.MyCommand.Module.Name; + } + + throw [System.ArgumentException] "The parameter 'InvocationContext' is not set" + } + + [CommandDetails] GetCommandMetadata() + { + if($this.InvocationContext) + { + $commandNoun = $this.InvocationContext.MyCommand.Noun + if(-not [string]::IsNullOrWhiteSpace($this.InvocationContext.MyCommand.Module.Prefix)) + { + # Remove the module prefix from command name + $commandNoun = $commandNoun.TrimStart($this.InvocationContext.MyCommand.Module.Prefix); + } + + return [CommandHelper]::Mapping | + Where-Object { $_.Noun -eq $commandNoun -and $_.Verb -eq $this.InvocationContext.MyCommand.Verb } | + Select-Object -First 1; + } + + throw [System.ArgumentException] "The parameter 'InvocationContext' is not set" + } + + [bool] IsLatestVersionRequired() + { + if($this.InvocationContext) + { + $commandNoun = $this.InvocationContext.MyCommand.Noun + if(-not [string]::IsNullOrWhiteSpace($this.InvocationContext.MyCommand.Module.Prefix)) + { + # Remove the module prefix from command name + $commandNoun = $commandNoun.TrimStart($this.InvocationContext.MyCommand.Module.Prefix); + } + + $mapping = [CommandHelper]::Mapping | + Where-Object { $_.Noun -eq $commandNoun -and $_.Verb -eq $this.InvocationContext.MyCommand.Verb } | + Select-Object -First 1; + return $mapping.IsLatestRequired; + } + + throw [System.ArgumentException] "The parameter 'InvocationContext' is not set" + } + + [System.Version] GetCurrentModuleVersion() + { + if($this.InvocationContext) + { + return [System.Version] ($this.InvocationContext.MyCommand.Version); + } + + # Return default version which is 0.0. + return [System.Version]::new(); + } + + [string[]] ConvertToStringArray([string] $stringArray) + { + $result = @(); + if(-not [string]::IsNullOrWhiteSpace($stringArray)) + { + $result += $stringArray.Split(',', [StringSplitOptions]::RemoveEmptyEntries) | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + ForEach-Object { $_.Trim() } | + Select-Object -Unique; + } + return $result; + } + + # Static Methods + static [void] PublishGenericException([ErrorRecord] $eventArgument) + { + [EventBase]::new().PublishException($eventArgument); + } + + static [void] PublishGenericCustomMessage([string] $message) + { + [EventBase]::PublishGenericCustomMessage($message, [MessageType]::Info); + } + + static [void] PublishGenericCustomMessage([string] $message, [MessageType] $messageType) + { + [EventBase]::new().PublishCustomMessage($message, $messageType); + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Abstracts/FileOutputBase.ps1 b/src/AzSK.AAD/0.9.0/Framework/Abstracts/FileOutputBase.ps1 new file mode 100644 index 000000000..f5021a637 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Abstracts/FileOutputBase.ps1 @@ -0,0 +1,160 @@ +Set-StrictMode -Version Latest +class FileOutputBase: ListenerBase +{ + static [string] $ETCFolderPath = "Etc"; + + [string] $FilePath = ""; + [string] $FolderPath = ""; + #[string] $BasePath = ""; + hidden [string[]] $BasePaths = @(); + + FileOutputBase() + { + [Helpers]::AbstractClass($this, [FileOutputBase]); + } + + hidden [void] AddBasePath([string] $path) + { + if(-not [string]::IsNullOrWhiteSpace($path)) + { + $path = $global:ExecutionContext.InvokeCommand.ExpandString($path); + if(Test-Path -Path $path) + { + $this.BasePaths += $path; + } + } + } + + [void] SetRunIdentifier([AzSKRootEventArgument] $arguments) + { + ([ListenerBase]$this).SetRunIdentifier($arguments); + + $this.AddBasePath([ConfigurationManager]::GetAzSKSettings().OutputFolderPath); + $this.AddBasePath([ConfigurationManager]::GetAzSKConfigData().OutputFolderPath); + $this.AddBasePath([Constants]::AzSKLogFolderPath); + } + + hidden [string] CalculateFolderPath([TenantContext] $context, [string] $subFolderPath, [int] $pathIndex) + { + $outputPath = ""; + if($context -and (-not [string]::IsNullOrWhiteSpace($context.TenantName)) -and (-not [string]::IsNullOrWhiteSpace($context.tenantId))) + { + $isDefaultPath = $false; + if($pathIndex -lt $this.BasePaths.Count) + { + $basePath = $this.BasePaths.Item($pathIndex); + } + else + { + $isDefaultPath = $true; + $basePath = [Constants]::AzSKLogFolderPath; + } + + if (-not $basePath.EndsWith("\")) { + $basePath += "\"; + } + + $outputPath = $basePath + [Constants]::AzSKModuleName + "Logs\" + + $sanitizedPath = [Helpers]::SanitizeFolderName($context.TenantName); + if ([string]::IsNullOrEmpty($sanitizedPath)) { + $sanitizedPath = $context.tenantId; + } + + $runPath = $this.RunIdentifier; + $commandMetadata = $this.GetCommandMetadata(); + + if($commandMetadata) + { + $runPath += "_" + $commandMetadata.ShortName; + } + + if ([string]::IsNullOrEmpty($sanitizedPath)) { + $outputPath += ("Default\{0}\" -f $runPath); + } + else { + $outputPath += ("Org_{0}\{1}\" -f $sanitizedPath, $runPath); + } + + if (-not [string]::IsNullOrEmpty($subFolderPath)) { + $sanitizedPath = [Helpers]::SanitizeFolderName($subFolderPath); + if (-not [string]::IsNullOrEmpty($sanitizedPath)) { + $outputPath += ("{0}\" -f $sanitizedPath); + } + } + + if(-not (Test-Path $outputPath)) + { + try + { + mkdir -Path $outputPath -ErrorAction Stop | Out-Null + } + catch + { + $outputPath = ""; + if(-not $isDefaultPath) + { + $outputPath = $this.CalculateFolderPath($context, $subFolderPath, $pathIndex + 1); + } + } + } + } + return $outputPath; + } + + [string] CalculateFolderPath([TenantContext] $context, [string] $subFolderPath) + { + return $this.CalculateFolderPath($context, $subFolderPath, 0); + } + + [string] CalculateFolderPath([TenantContext] $context) + { + return $this.CalculateFolderPath($context, ""); + } + + [void] SetFolderPath([TenantContext] $context) + { + $this.SetFolderPath($context, ""); + } + + [void] SetFolderPath([TenantContext] $context, [string] $subFolderPath) + { + $this.FolderPath = $this.CalculateFolderPath($context, $subFolderPath); + } + + [string] CalculateFilePath([TenantContext] $context, [string] $fileName) + { + return $this.CalculateFilePath($context, "", $fileName); + } + + [string] CalculateFilePath([TenantContext] $context, [string] $subFolderPath, [string] $fileName) + { + $outputPath = ""; + $this.SetFolderPath($context, $subFolderPath); + if ([string]::IsNullOrEmpty($this.FolderPath)) { + return $outputPath; + } + + $outputPath = $this.FolderPath; + if (-not $outputPath.EndsWith("\")) { + $outputPath += "\"; + } + if ([string]::IsNullOrEmpty($fileName)) { + $outputPath += $(Get-Date -format "yyyyMMdd_HHmmss") + ".LOG"; + } + else { + $outputPath += $fileName; + } + return $outputPath; + } + + [void] SetFilePath([TenantContext] $context, [string] $fileName) + { + $this.SetFilePath($context, "", $fileName); + } + + [void] SetFilePath([TenantContext] $context, [string] $subFolderPath, [string] $fileName) + { + $this.FilePath = $this.CalculateFilePath($context, $subFolderPath, $fileName); + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Abstracts/FixControl/FixControlBase.ps1 b/src/AzSK.AAD/0.9.0/Framework/Abstracts/FixControl/FixControlBase.ps1 new file mode 100644 index 000000000..2c045a3a9 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Abstracts/FixControl/FixControlBase.ps1 @@ -0,0 +1,135 @@ +Set-StrictMode -Version Latest +class FixControlBase: AzSKRoot +{ + hidden [SVTConfig] $SVTConfig + hidden [PSObject] $ControlSettings + hidden [ControlParam[]] $Controls = @(); + + FixControlBase([string] $tenantId): + Base($tenantId) + { + [Helpers]::AbstractClass($this, [FixControlBase]); + } + + hidden [void] LoadSvtConfig([string] $controlsJsonFileName) + { + if ([string]::IsNullOrEmpty($controlsJsonFileName)) + { + throw [System.ArgumentException] ("JSON file name is null or empty"); + } + + $this.ControlSettings = $this.LoadServerConfigFile("ControlSettings.json"); + + if (-not $this.SVTConfig) + { + $this.SVTConfig = [ConfigurationManager]::GetSVTConfig($controlsJsonFileName); + } + } + + [bool] ValidateMaintenanceState() + { + if ($this.SVTConfig.IsMaintenanceMode) + { + $this.PublishCustomMessage(([ConfigurationManager]::GetAzSKConfigData().MaintenanceMessage -f $this.SVTConfig.FeatureName), [MessageType]::Warning); + } + return $this.SVTConfig.IsMaintenanceMode; + } + + [MessageData[]] FixAllControls() + { + [MessageData[]] $messages = @(); + if (-not $this.ValidateMaintenanceState()) + { + if($this.Controls.Count -ne 0) + { + $messages += $this.FixStarted(); + # Group and sort the list by FixControlImpact + + $this.Controls | Group-Object { $_.FixControlImpact } | Sort-Object @{ Expression = { [Enum]::Parse([FixControlImpact], $_.Name) }; Descending = $true } | + ForEach-Object { + $messages += $this.PublishCustomMessage(" `r`n[FixControlImpact: $($_.Name)] [Total: $($_.Count)]"); + + $_.Group | ForEach-Object { + $controlParam = $_; + $controlItem = $this.SVTConfig.Controls | Where-Object { $_.Id -eq $controlParam.Id } | Select-Object -First 1 + if($controlItem) + { + if($controlItem.FixControl -and (-not [string]::IsNullOrEmpty($controlItem.FixControl.FixMethodName))) + { + $messages += $this.RunFixControl($controlParam, $controlItem); + } + else + { + $messages += $this.PublishCustomMessage("The ControlId [$($controlParam.ControlID)] does not support automated fixing of control. Please follow the recommendation mentioned in the evaluation summary/csv file.", [MessageType]::Error); + } + } + else + { + $messages += $this.PublishCustomMessage("The parameter Id [$($controlParam.Id)] is not valid. Please contact support team.", [MessageType]::Error); + } + }; + $messages += $this.PublishCustomMessage([Constants]::SingleDashLine); + }; + $messages += $this.FixCompleted(); + } + else + { + $this.PublishCustomMessage("No controls are available to fix.", [MessageType]::Error); + } + } + return $messages; + } + + [MessageData[]] FixStarted() + { + return @(); + } + + [MessageData[]] FixCompleted() + { + return @(); + } + + hidden [MessageData[]] RunFixControl([ControlParam] $controlParam, [ControlItem] $controlItem) + { + [MessageData[]] $messages = @(); + + if($controlItem.Enabled -eq $false) + { + $messages += $this.PublishCustomMessage("The ControlId [$($controlParam.ControlID)] is disabled.", [MessageType]::Warning); + } + else + { + $messages += [MessageData]::new([Constants]::SingleDashLine); + $messages += $this.PublishCustomMessage("Fixing: [$($controlParam.ControlID)]"); + $methodName = $controlItem.FixControl.FixMethodName.Trim(); + if((Get-Member -InputObject $this -Name $methodName -MemberType Method | Measure-Object).Count -ne 0) + { + $controlParam.ChildResourceParams | ForEach-Object { + try + { + if([string]::IsNullOrWhiteSpace($_.ChildResourceName)) + { + $messages += $this.$methodName($_.Parameters); + } + else + { + $messages += $this.$methodName($_.Parameters, $_.ChildResourceName); + } + } + catch + { + $this.PublishException($_); + } + }; + } + else + { + $messages += $this.PublishCustomMessage("The class [$($this.GetType().Name)] does not contain a method [$methodName]. Please contact support team.", [MessageType]::Error); + } + } + + return $messages; + } + +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Abstracts/FixControl/FixServicesBase.ps1 b/src/AzSK.AAD/0.9.0/Framework/Abstracts/FixControl/FixServicesBase.ps1 new file mode 100644 index 000000000..01e1b2a27 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Abstracts/FixControl/FixServicesBase.ps1 @@ -0,0 +1,65 @@ +Set-StrictMode -Version Latest +class FixServicesBase: FixControlBase +{ + [string] $ResourceGroupName = ""; + [string] $ResourceName = ""; + + hidden [ResourceConfig] $ResourceConfig = $null; + + FixServicesBase([string] $tenantId, [ResourceConfig] $resourceConfig, [string] $resourceGroupName): + Base($tenantId) + { + $this.CreateInstance($resourceConfig, $resourceGroupName); + } + + hidden [void] CreateInstance([ResourceConfig] $resourceConfig, [string] $resourceGroupName) + { + [Helpers]::AbstractClass($this, [FixServicesBase]); + + if(-not $resourceConfig) + { + throw [System.ArgumentException] ("The argument 'resourceConfig' is null"); + } + $this.ResourceConfig = $resourceConfig; + + if([string]::IsNullOrEmpty($resourceGroupName)) + { + throw [System.ArgumentException] ("The argument 'resourceGroupName' is null or empty"); + } + $this.ResourceGroupName = $resourceGroupName; + + if([string]::IsNullOrEmpty($resourceConfig.ResourceName)) + { + throw [System.ArgumentException] ("The argument 'ResourceName' is null or empty"); + } + $this.ResourceName = $resourceConfig.ResourceName; + + if (-not $resourceConfig.ResourceTypeMapping) + { + throw [System.ArgumentException] ("No ResourceTypeMapping found"); + } + + $this.LoadSvtConfig($resourceConfig.ResourceTypeMapping.JsonFileName); + + if(-not $this.ResourceConfig.Controls) + { + throw [System.ArgumentException] ("No controls found to fix"); + } + + $this.Controls += $this.ResourceConfig.Controls; + } + + [MessageData[]] FixStarted() + { + return $this.PublishCustomMessage([Constants]::DoubleDashLine + + "`r`nStarting control fixes: [FeatureName: $($this.SVTConfig.FeatureName)] [ResourceGroupName: $($this.ResourceGroupName)] [ResourceName: $($this.ResourceName)] `r`n" + + [Constants]::SingleDashLine); + } + + [MessageData[]] FixCompleted() + { + return $this.PublishCustomMessage([Constants]::SingleDashLine + + "`r`nCompleted control fixes: [FeatureName: $($this.SVTConfig.FeatureName)] [ResourceGroupName: $($this.ResourceGroupName)] [ResourceName: $($this.ResourceName)] `r`n" + + [Constants]::DoubleDashLine, [MessageType]::Update); + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Abstracts/FixControl/FixSubscriptionBase.ps1 b/src/AzSK.AAD/0.9.0/Framework/Abstracts/FixControl/FixSubscriptionBase.ps1 new file mode 100644 index 000000000..ab33eb25e --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Abstracts/FixControl/FixSubscriptionBase.ps1 @@ -0,0 +1,56 @@ +Set-StrictMode -Version Latest +class FixSubscriptionBase: FixControlBase +{ + [string] $ResourceGroupName = ""; + [string] $ResourceName = ""; + + hidden [ResourceConfig] $ResourceConfig = $null; + + FixSubscriptionBase([string] $tenantId, [ArrayWrapper] $controls): + Base($tenantId) + { + if($controls) + { + $this.CreateInstance($controls.Values); + } + else + { + $this.CreateInstance(@()); + } + } + + hidden [void] CreateInstance([ControlParam[]] $controls) + { + [Helpers]::AbstractClass($this, [FixSubscriptionBase]); + + $typeMapping = [SVTMapping]::SubscriptionMapping; + if (-not $typeMapping) + { + throw [System.ArgumentException] ("No subscription type mapping found"); + } + + $this.LoadSvtConfig($typeMapping.JsonFileName); + + if(-not $controls) + { + throw [System.ArgumentException] ("No controls found to fix"); + } + + $this.Controls += $controls; + } + + [MessageData[]] FixStarted() + { + return $this.PublishCustomMessage([Constants]::DoubleDashLine + + "`r`nStarting control fixes: [FeatureName: $($this.SVTConfig.FeatureName)] [TenantName: $($this.TenantContext.TenantName)] [tenantId: $($this.TenantContext.tenantId)] `r`n" + + [Constants]::SingleDashLine); + } + + [MessageData[]] FixCompleted() + { + return $this.PublishCustomMessage([Constants]::SingleDashLine + + "`r`nCompleted control fixes: [FeatureName: $($this.SVTConfig.FeatureName)] [TenantName: $($this.TenantContext.TenantName)] [tenantId: $($this.TenantContext.tenantId)] `r`n" + + [Constants]::DoubleDashLine, [MessageType]::Update); + } + +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Abstracts/ListenerBase.ps1 b/src/AzSK.AAD/0.9.0/Framework/Abstracts/ListenerBase.ps1 new file mode 100644 index 000000000..dac3939f3 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Abstracts/ListenerBase.ps1 @@ -0,0 +1,63 @@ +Set-StrictMode -Version Latest +class ListenerBase: EventBase +{ + [array] $RegisteredEvents = @(); + + ListenerBase() + { + [Helpers]::AbstractClass($this, [ListenerBase]); + } + + [void] SetRunIdentifier([AzSKRootEventArgument] $arguments) + { + $data = $arguments.Messages | Select-Object -First 1 + if ($data) { + $this.RunIdentifier = $data.Message; + + # Sending Invocation context in DataObject while firing RunIdentifier event + if($data.DataObject) + { + $this.InvocationContext = $data.DataObject; + } + } + } + + [void] UnregisterEvents() + { + $unreg = 0 + $this.RegisteredEvents | Sort-Object -Descending | + ForEach-Object { + try{ + Unregister-Event -SubscriptionId $_ -Force -ErrorAction SilentlyContinue + Remove-Job -Id $_ -Force -ErrorAction SilentlyContinue + $unreg++ + } + Catch{ + #Keeping exception blank to continue execution flow + } + } + if ([EventBase]::logLvl -ge 2) {Write-Host -ForegroundColor Yellow "Unregistered all [$unreg] events for classs: $($this.GetType())" } + $this.RegisteredEvents = @(); + } + + [void] RegisterEvent([string] $sourceIdentifier, [ScriptBlock] $action) + { + $this.RegisteredEvents += (Register-EngineEvent -SourceIdentifier $sourceIdentifier -Action $action).Id; + $eid = $this.RegisteredEvents[$this.RegisteredEvents.Count-1] + if ([EventBase]::logLvl -ge 2) { Write-Host -ForegroundColor Green "RegEvt: [$eid] Type: $($this.GetType()) SrcId: $sourceIdentifier" } + } + + [void] HandleException([ScriptBlock] $script, [System.Management.Automation.PSEventArgs] $event) + { + try + { + & $script $event $this + } + catch + { + $this.PublishException($_); + } + } +} + + diff --git a/src/AzSK.AAD/0.9.0/Framework/Abstracts/SVTBase.ps1 b/src/AzSK.AAD/0.9.0/Framework/Abstracts/SVTBase.ps1 new file mode 100644 index 000000000..1d11119b1 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Abstracts/SVTBase.ps1 @@ -0,0 +1,1150 @@ +Set-StrictMode -Version Latest +class SVTBase: AzSKRoot +{ + hidden [string] $ResourceId = "" + [ResourceContext] $ResourceContext = $null; + hidden [SVTConfig] $SVTConfig + hidden [PSObject] $ControlSettings + + #hidden [ControlStateExtension] $ControlStateExt; + + hidden [ControlState[]] $ResourceState; + hidden [ControlState[]] $DirtyResourceStates; + + hidden [ControlItem[]] $ApplicableControls = $null; + hidden [ControlItem[]] $FeatureApplicableControls = $null; + [string[]] $ChildResourceNames = $null; + [System.Net.SecurityProtocolType] $currentSecurityProtocol; + [string[]] $FilterTags = @(); + [string[]] $ExcludeTags = @(); + [string[]] $ControlIds = @(); + [string[]] $ExcludeControlIds = @(); + [bool] $GenerateFixScript = $false; + [bool] $IncludeUserComments = $false; + [string] $PartialScanIdentifier = [string]::Empty + [ComplianceStateTableEntity[]] $ComplianceStateData = @(); + [PSObject[]] $ChildSvtObjects = @(); + SVTBase([string] $tenantId, [SVTResource] $svtResource): + Base($tenantId) + { + $this.CreateInstance($svtResource); + } + + SVTBase([string] $tenantId): + Base($tenantId) + { + $this.CreateInstance(); + } + + SVTBase([string] $tenantId, [string] $resourceGroupName, [string] $resourceName): + Base($tenantId) + { + $this.CreateInstance([SVTResource]@{ + ResourceGroupName = $resourceGroupName; + ResourceName = $resourceName; + }); + } + hidden [void] CreateInstance() + { + [Helpers]::AbstractClass($this, [SVTBase]); + + $this.LoadSvtConfig([SVTMapping]::SubscriptionMapping.JsonFileName); + } + + hidden [void] CreateInstance([SVTResource] $svtResource) + { + [Helpers]::AbstractClass($this, [SVTBase]); + + if(-not $svtResource) + { + throw [System.ArgumentException] ("The argument 'svtResource' is null"); + } + + # if([string]::IsNullOrEmpty($svtResource.ResourceGroupName)) + # { + # throw [System.ArgumentException] ("The argument 'ResourceGroupName' is null or empty"); + # } + + if([string]::IsNullOrEmpty($svtResource.ResourceName)) + { + throw [System.ArgumentException] ("The argument 'ResourceName' is null or empty"); + } + + if(-not $svtResource.ResourceTypeMapping) + { + $svtResource.ResourceTypeMapping = [SVTMapping]::Mapping | + Where-Object { $_.ClassName -eq $this.GetType().Name } | + Select-Object -First 1 + } + + if (-not $svtResource.ResourceTypeMapping) + { + throw [System.ArgumentException] ("No ResourceTypeMapping found"); + } + + if ([string]::IsNullOrEmpty($svtResource.ResourceTypeMapping.JsonFileName)) + { + throw [System.ArgumentException] ("JSON file name is null or empty"); + } + + $this.ResourceId = $svtResource.ResourceId; + + $this.LoadSvtConfig($svtResource.ResourceTypeMapping.JsonFileName); + + $this.ResourceContext = [ResourceContext]@{ + ResourceGroupName = $svtResource.ResourceGroupName; + ResourceName = $svtResource.ResourceName; + ResourceType = $svtResource.ResourceTypeMapping.ResourceType; + ResourceTypeName = $svtResource.ResourceTypeMapping.ResourceTypeName; + }; + $this.ResourceContext.ResourceId = $this.GetResourceId(); + } + + hidden [void] LoadSvtConfig([string] $controlsJsonFileName) + { + $this.ControlSettings = $this.LoadServerConfigFile("ControlSettings.json"); + + if (-not $this.SVTConfig) { + $this.SVTConfig = [ConfigurationManager]::GetSVTConfig($controlsJsonFileName); + + $this.SVTConfig.Controls | Foreach-Object { + + $_.Description = $global:ExecutionContext.InvokeCommand.ExpandString($_.Description) + $_.Recommendation = $global:ExecutionContext.InvokeCommand.ExpandString($_.Recommendation) + $ControlSeverity = $_.ControlSeverity + if([Helpers]::CheckMember($this.ControlSettings,"ControlSeverity.$ControlSeverity")) + { + $_.ControlSeverity = $this.ControlSettings.ControlSeverity.$ControlSeverity + } + else + { + $_.ControlSeverity = $ControlSeverity + } + if(-not [string]::IsNullOrEmpty($_.MethodName)) + { + $_.MethodName = $_.MethodName.Trim(); + } + if($this.CheckBaselineControl($_.ControlID)) + { + $_.IsBaselineControl = $true + } + } + } + } + + hidden [bool] CheckBaselineControl($controlId) + { + if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings,"BaselineControls.ResourceTypeControlIdMappingList")) + { + $baselineControl = $this.ControlSettings.BaselineControls.ResourceTypeControlIdMappingList | Where-Object {$_.ControlIds -contains $controlId} + if(($baselineControl | Measure-Object).Count -gt 0 ) + { + return $true + } + } + + if(($null -ne $this.ControlSettings) -and [Helpers]::CheckMember($this.ControlSettings,"BaselineControls.SubscriptionControlIdList")) + { + $baselineControl = $this.ControlSettings.BaselineControls.SubscriptionControlIdList | Where-Object {$_ -eq $controlId} + if(($baselineControl | Measure-Object).Count -gt 0 ) + { + return $true + } + } + return $false + } + + hidden [string] GetResourceId() + { + if ([string]::IsNullOrEmpty($this.ResourceId)) + { + if($this.ResourceContext) + { + if(-not [string]::ISNullOrEmpty($this.ResourceContext.ResourceGroupName)) + { + $resource = Get-AzResource -Name $this.ResourceContext.ResourceName -ResourceGroupName $this.ResourceContext.ResourceGroupName + + if($resource) + { + $this.ResourceId = $resource.ResourceId; + } + else + { + throw [SuppressedException] "Unable to find the Azure resource - [ResourceType: $($this.ResourceContext.ResourceType)] [ResourceGroupName: $($this.ResourceContext.ResourceGroupName)] [ResourceName: $($this.ResourceContext.ResourceName)]" + } + } + + } + else + { + $this.ResourceId = $this.TenantContext.Scope; + } + } + + return $this.ResourceId; + } + + [bool] ValidateMaintenanceState() + { + if ($this.SVTConfig.IsMaintenanceMode) { + $this.PublishCustomMessage(([ConfigurationManager]::GetAzSKConfigData().MaintenanceMessage -f $this.SVTConfig.FeatureName), [MessageType]::Warning); + } + return $this.SVTConfig.IsMaintenanceMode; + } + + hidden [ControlResult] CreateControlResult([string] $childResourceName, [VerificationResult] $verificationResult) + { + [ControlResult] $control = [ControlResult]@{ + VerificationResult = $verificationResult; + }; + + if(-not [string]::IsNullOrEmpty($childResourceName)) + { + $control.ChildResourceName = $childResourceName; + } + + [SessionContext] $sc = [SessionContext]::new(); + $sc.IsLatestPSModule = $this.RunningLatestPSModule; + $control.CurrentSessionContext = $sc; + + return $control; + } + + [ControlResult] CreateControlResult() + { + return $this.CreateControlResult("", [VerificationResult]::Manual); + } + + hidden [ControlResult] CreateControlResult([FixControl] $fixControl) + { + $control = $this.CreateControlResult(); + if($this.GenerateFixScript -and $fixControl -and $fixControl.Parameters -and ($fixControl.Parameters | Get-Member -MemberType Properties | Measure-Object).Count -ne 0) + { + $control.FixControlParameters = $fixControl.Parameters | Select-Object -Property *; + } + return $control; + } + + [ControlResult] CreateControlResult([string] $childResourceName) + { + return $this.CreateControlResult($childResourceName, [VerificationResult]::Manual); + } + + [ControlResult] CreateChildControlResult([string] $childResourceName, [ControlResult] $controlResult) + { + $control = $this.CreateControlResult($childResourceName, [VerificationResult]::Manual); + if($controlResult.FixControlParameters -and ($controlResult.FixControlParameters | Get-Member -MemberType Properties | Measure-Object).Count -ne 0) + { + $control.FixControlParameters = $controlResult.FixControlParameters | Select-Object -Property *; + } + return $control; + } + + hidden [SVTEventContext] CreateSVTEventContextObject() + { + return [SVTEventContext]@{ + FeatureName = $this.SVTConfig.FeatureName; + Metadata = [Metadata]@{ + Reference = $this.SVTConfig.Reference; + }; + + TenantContext = $this.TenantContext; + ResourceContext = $this.ResourceContext; + PartialScanIdentifier = $this.PartialScanIdentifier + + }; + } + + hidden [SVTEventContext] CreateErrorEventContext([System.Management.Automation.ErrorRecord] $exception) + { + [SVTEventContext] $arg = $this.CreateSVTEventContextObject(); + $arg.ExceptionMessage = $exception; + + return $arg; + } + + hidden [void] ControlStarted([SVTEventContext] $arg) + { + $this.PublishEvent([SVTEvent]::ControlStarted, $arg); + } + + hidden [void] ControlDisabled([SVTEventContext] $arg) + { + $this.PublishEvent([SVTEvent]::ControlDisabled, $arg); + } + + hidden [void] ControlCompleted([SVTEventContext] $arg) + { + $this.PublishEvent([SVTEvent]::ControlCompleted, $arg); + } + + hidden [void] ControlError([ControlItem] $controlItem, [System.Management.Automation.ErrorRecord] $exception) + { + $arg = $this.CreateErrorEventContext($exception); + $arg.ControlItem = $controlItem; + $this.PublishEvent([SVTEvent]::ControlError, $arg); + } + + hidden [void] EvaluationCompleted([SVTEventContext[]] $arguments) + { + $this.PublishEvent([SVTEvent]::EvaluationCompleted, $arguments); + } + + hidden [void] EvaluationStarted() + { + $this.PublishEvent([SVTEvent]::EvaluationStarted, $this.CreateSVTEventContextObject()); + } + + hidden [void] EvaluationError([System.Management.Automation.ErrorRecord] $exception) + { + $this.PublishEvent([SVTEvent]::EvaluationError, $this.CreateErrorEventContext($exception)); + } + + [SVTEventContext[]] EvaluateAllControls() + { + [SVTEventContext[]] $resourceSecurityResult = @(); + if (-not $this.ValidateMaintenanceState()) { + if($this.GetApplicableControls().Count -eq 0) + { + if($this.ResourceContext) + { + $this.PublishCustomMessage("No controls have been found to evaluate for Resource [$($this.ResourceContext.ResourceName)]", [MessageType]::Warning); + $this.PublishCustomMessage("$([Constants]::SingleDashLine)"); + } + else + { + $this.PublishCustomMessage("No controls have been found to evaluate for Subscription", [MessageType]::Warning); + } + } + else + { + $this.PostTelemetry(); + $this.EvaluationStarted(); + $resourceSecurityResult += $this.GetAutomatedSecurityStatus(); + $resourceSecurityResult += $this.GetManualSecurityStatus(); + $this.PostEvaluationCompleted($resourceSecurityResult); + $this.EvaluationCompleted($resourceSecurityResult); + } + } + return $resourceSecurityResult; + } + + + + [SVTEventContext[]] ComputeApplicableControlsWithContext() + { + [SVTEventContext[]] $contexts = @(); + if (-not $this.ValidateMaintenanceState()) { + $controls = $this.GetApplicableControls(); + if($controls.Count -gt 0) + { + foreach($control in $controls) { + [SVTEventContext] $singleControlResult = $this.CreateSVTEventContextObject(); + $singleControlResult.ControlItem = $control; + $contexts += $singleControlResult; + } + } + } + return $contexts; + } + [void] PostTelemetry() + { + # Setting the protocol for databricks + if([Helpers]::CheckMember($this.ResourceContext, "ResourceType") -and $this.ResourceContext.ResourceType -eq "Microsoft.Databricks/workspaces") + { + $this.currentSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + } + $this.PostFeatureControlTelemetry() + } + [void] PostFeatureControlTelemetry() + { + #todo add check for latest module version + if($this.RunningLatestPSModule -and ($this.FeatureApplicableControls | Measure-Object).Count -gt 0) + { + [CustomData] $customData = [CustomData]::new(); + $customData.Name = "FeatureControlTelemetry"; + $ResourceObject = "" | Select ResourceContext, Controls, ChildResourceNames; + $ResourceObject.ResourceContext = $this.ResourceContext; + $ResourceObject.Controls = $this.FeatureApplicableControls; + $ResourceObject.ChildResourceNames = $this.ChildResourceNames; + $customData.Value = $ResourceObject; + $this.PublishCustomData($customData); + } + } + + [SVTEventContext[]] FetchStateOfAllControls() + { + [SVTEventContext[]] $resourceSecurityResult = @(); + if (-not $this.ValidateMaintenanceState()) { + if($this.GetApplicableControls().Count -eq 0) + { + $this.PublishCustomMessage("No security controls match the input criteria specified", [MessageType]::Warning); + } + else + { + $this.EvaluationStarted(); + $resourceSecurityResult += $this.GetControlsStateResult(); + $this.EvaluationCompleted($resourceSecurityResult); + } + } + return $resourceSecurityResult; + } + + [ControlItem[]] ApplyServiceFilters([ControlItem[]] $controls) + { + return $controls; + } + + hidden [ControlItem[]] GetApplicableControls() + { + #Lazy load the list of the applicable controls + if($null -eq $this.ApplicableControls) + { + $this.ApplicableControls = @(); + $this.FeatureApplicableControls = @(); + $filterControlsById = @(); + $filteredControls = @(); + + #Apply service filters based on default set of controls + $this.FeatureApplicableControls += $this.ApplyServiceFilters($this.SVTConfig.Controls); + + if($this.ControlIds.Count -ne 0) + { + $filterControlsById += $this.FeatureApplicableControls | Where-Object { $this.ControlIds -Contains $_.ControlId }; + } + else + { + $filterControlsById += $this.FeatureApplicableControls + } + + if($this.ExcludeControlIds.Count -ne 0) + { + $filterControlsById = $filterControlsById | Where-Object { $this.ExcludeControlIds -notcontains $_.ControlId }; + } + + if(($this.FilterTags | Measure-Object).Count -ne 0 -or ($this.ExcludeTags | Measure-Object).Count -ne 0) + { + if(($filterControlsById | Measure-Object).Count -gt 0){ + $filterControlsById | ForEach-Object { + Set-Variable -Name control -Value $_ -Scope Local + Set-Variable -Name filterMatch -Value $false -Scope Local + Set-Variable -Name excludeMatch -Value $false -Scope Local + $control.Tags | ForEach-Object { + Set-Variable -Name cTag -Value $_ -Scope Local + + if(($this.FilterTags | Measure-Object).Count -ne 0 ` + -and ($this.FilterTags | Where-Object { $_ -like $cTag} | Measure-Object).Count -ne 0) + { + $filterMatch = $true + } + elseif(($this.FilterTags | Measure-Object).Count -eq 0) + { + $filterMatch = $true + } + if(($this.ExcludeTags | Measure-Object).Count -ne 0 ` + -and ($this.ExcludeTags | Where-Object { $_ -like $cTag} | Measure-Object).Count -ne 0) + { + $excludeMatch = $true + } + } + + if(($filterMatch -and -not $excludeMatch) ` + -or (-not $filterMatch -and -not $excludeMatch)) + { + $filteredControls += $control + } + } + } + } + else + { + $filteredControls += $filterControlsById; + } + $this.ApplicableControls = $filteredControls; + } + return $this.ApplicableControls; + } + + hidden [SVTEventContext[]] GetManualSecurityStatus() + { + [SVTEventContext[]] $manualControlsResult = @(); + try + { + $this.GetApplicableControls() | Where-Object { $_.Automated -eq "No" -and $_.Enabled -eq $true } | + ForEach-Object { + $controlItem = $_; + [SVTEventContext] $arg = $this.CreateSVTEventContextObject(); + + $arg.ControlItem = $controlItem; + [ControlResult] $control = [ControlResult]@{ + VerificationResult = [VerificationResult]::Manual; + }; + + [SessionContext] $sc = [SessionContext]::new(); + $sc.IsLatestPSModule = $this.RunningLatestPSModule; + $control.CurrentSessionContext = $sc; + + $arg.ControlResults += $control + + $this.PostProcessData($arg); + + $manualControlsResult += $arg; + } + } + catch + { + $this.EvaluationError($_); + } + + return $manualControlsResult; + } + + hidden [SVTEventContext[]] GetAutomatedSecurityStatus() + { + [SVTEventContext[]] $automatedControlsResult = @(); + $this.DirtyResourceStates = @(); + try + { + $this.GetApplicableControls() | Where-Object { $_.Automated -ne "No" -and (-not [string]::IsNullOrEmpty($_.MethodName)) } | + ForEach-Object { + $eventContext = $this.RunControl($_); + if($null -ne $eventContext -and $eventcontext.ControlResults.Length -gt 0) + { + $automatedControlsResult += $eventContext; + } + }; + } + catch + { + $this.EvaluationError($_); + } + + return $automatedControlsResult; + } + hidden [SVTEventContext[]] GetControlsStateResult() + { + [SVTEventContext[]] $automatedControlsResult = @(); + $this.DirtyResourceStates = @(); + try + { + $this.GetApplicableControls() | + ForEach-Object { + $eventContext = $this.FetchControlState($_); + #filter controls if there is no state found + if($eventContext) + { + $eventContext.ControlResults = $eventContext.ControlResults | Where-Object{$_.AttestationStatus -ne [AttestationStatus]::None} + if($eventContext.ControlResults) + { + $automatedControlsResult += $eventContext; + } + } + }; + } + catch + { + $this.EvaluationError($_); + } + + return $automatedControlsResult; + } + hidden [SVTEventContext] RunControl([ControlItem] $controlItem) + { + [SVTEventContext] $singleControlResult = $this.CreateSVTEventContextObject(); + $singleControlResult.ControlItem = $controlItem; + + $this.ControlStarted($singleControlResult); + if($controlItem.Enabled -eq $false) + { + $this.ControlDisabled($singleControlResult); + } + else + { + $azskScanResult = $this.CreateControlResult($controlItem.FixControl); + try + { + $methodName = $controlItem.MethodName; + #$this.CurrentControlItem = $controlItem; + $singleControlResult.ControlResults += $this.$methodName($azskScanResult); + } + catch + { + $azskScanResult.VerificationResult = [VerificationResult]::Error + $azskScanResult.AddError($_); + $singleControlResult.ControlResults += $azskScanResult + $this.ControlError($controlItem, $_); + } + $this.PostProcessData($singleControlResult); + + # Check for the control which requires elevated permission to modify 'Recommendation' so that user can know it is actually automated if they have the right permission + if($singleControlResult.ControlItem.Automated -eq "Yes") + { + $singleControlResult.ControlResults | + ForEach-Object { + $currentItem = $_; + if($_.VerificationResult -eq [VerificationResult]::Manual -and $singleControlResult.ControlItem.Tags.Contains([Constants]::OwnerAccessTagName)) + { + $singleControlResult.ControlItem.Recommendation = [Constants]::RequireOwnerPermMessage + $singleControlResult.ControlItem.Recommendation + } + } + } + } + + $this.ControlCompleted($singleControlResult); + + return $singleControlResult; + } + + # Policy compliance methods begin + # hidden [ControlResult] ComputeFinalScanResult([ControlResult] $azskScanResult, [ControlResult] $policyScanResult) + # { + # if($policyScanResult.VerificationResult -ne [VerificationResult]::Failed -and $azskScanResult.VerificationResult -ne [VerificationResult]::Passed) + # { + # return $azskScanResult + # } + # else + # { + # return $policyScanResult; + # } + # } + + # Policy compliance methods end + + # hidden [SVTEventContext] FetchControlState([ControlItem] $controlItem) + # { + # [SVTEventContext] $singleControlResult = $this.CreateSVTEventContextObject(); + # $singleControlResult.ControlItem = $controlItem; + + # $controlState = @(); + # $controlStateValue = @(); + # try + # { + # $resourceStates = $this.GetResourceState(); + # if(($resourceStates | Measure-Object).Count -ne 0) + # { + # $controlStateValue += $resourceStates | Where-Object { $_.InternalId -eq $singleControlResult.ControlItem.Id }; + # $controlStateValue | ForEach-Object { + # $currentControlStateValue = $_; + # if($null -ne $currentControlStateValue) + # { + # #assign expiry date + # $expiryIndays=$this.CalculateExpirationInDays($singleControlResult,$currentControlStateValue); + # if($expiryIndays -ne -1) + # { + # $currentControlStateValue.State.ExpiryDate = ($currentControlStateValue.State.AttestedDate.AddDays($expiryIndays)).ToString("MM/dd/yyyy"); + # } + # $controlState += $currentControlStateValue; + # } + # } + # } + # } + # catch + # { + # $this.EvaluationError($_); + # } + # if(($controlState|Measure-Object).Count -gt 0) + # { + # $this.ControlStarted($singleControlResult); + # if($controlItem.Enabled -eq $false) + # { + # $this.ControlDisabled($singleControlResult); + # } + # else + # { + # $controlResult = $this.CreateControlResult($controlItem.FixControl); + # $singleControlResult.ControlResults += $controlResult; + # $singleControlResult.ControlResults | + # ForEach-Object { + # try + # { + # $currentItem = $_; + + # if($controlState.Count -ne 0) + # { + # # Process the state if it's available + # $childResourceState = $controlState | Where-Object { $_.ChildResourceName -eq $currentItem.ChildResourceName } | Select-Object -First 1; + # if($childResourceState) + # { + # $currentItem.StateManagement.AttestedStateData = $childResourceState.State; + # $currentItem.AttestationStatus = $childResourceState.AttestationStatus; + # $currentItem.ActualVerificationResult = $childResourceState.ActualVerificationResult; + # $currentItem.VerificationResult = [VerificationResult]::NotScanned + # } + # } + # } + # catch + # { + # $this.EvaluationError($_); + # } + # }; + + # } + # $this.ControlCompleted($singleControlResult); + # } + + # return $singleControlResult; + # } + hidden [void] PostEvaluationCompleted([SVTEventContext[]] $ControlResults) + { + # If ResourceType is Databricks, reverting security protocol + if([Helpers]::CheckMember($this.ResourceContext, "ResourceType") -and $this.ResourceContext.ResourceType -eq "Microsoft.Databricks/workspaces") + { + [Net.ServicePointManager]::SecurityProtocol = $this.currentSecurityProtocol + } + #$this.UpdateControlStates($ControlResults); + } + + # hidden [void] UpdateControlStates([SVTEventContext[]] $ControlResults) + # { + # if($null -ne $this.ControlStateExt -and $this.ControlStateExt.HasControlStateWriteAccessPermissions() -and ($ControlResults | Measure-Object).Count -gt 0 -and ($this.ResourceState | Measure-Object).Count -gt 0) + # { + # $effectiveResourceStates = @(); + # if(($this.DirtyResourceStates | Measure-Object).Count -gt 0) + # { + # $this.ResourceState | ForEach-Object { + # $controlState = $_; + # if(($this.DirtyResourceStates | Where-Object { $_.InternalId -eq $controlState.InternalId -and $_.ChildResourceName -eq $controlState.ChildResourceName } | Measure-Object).Count -eq 0) + # { + # $effectiveResourceStates += $controlState; + # } + # } + # } + # else + # { + # #If no dirty states found then no action needed. + # return; + # } + + # #get the uniqueid from the first control result. Here we can take first as it would come here for each resource. + # $id = $ControlResults[0].GetUniqueId(); + + # $this.ControlStateExt.SetControlState($id, $effectiveResourceStates, $true) + # } + # } + + hidden [void] PostProcessData([SVTEventContext] $eventContext) + { + $tempHasRequiredAccess = $true; + $controlState = @(); + $controlStateValue = @(); + try + { + # Get policy compliance if org-level flag is enabled and policy is found + #TODO: set flag in a variable once and reuse it + + if([FeatureFlightingManager]::GetFeatureStatus("EnableAzurePolicyBasedScan",$($this.TenantContext.tenantId)) -eq $true) + { + if(-not [string]::IsNullOrWhiteSpace($eventContext.ControlItem.PolicyDefinitionGuid)) + { + #create default controlresult + $policyScanResult = $this.CreateControlResult($eventContext.ControlItem.FixControl); + #update default controlresult with policy compliance state + $policyScanResult = $this.CheckPolicyCompliance($eventContext.ControlItem, $policyScanResult); + #todo: currently excluding child controls + if($eventContext.ControlResults.Count -eq 1 -and $Null -ne $policyScanResult) + { + $finalScanResult = $this.ComputeFinalScanResult($eventContext.ControlResults[0],$policyScanResult) + $eventContext.ControlResults[0] = $finalScanResult + } + } + } + + $this.GetDataFromSubscriptionReport($eventContext); + + # $resourceStates = $this.GetResourceState() + # if(($resourceStates | Measure-Object).Count -ne 0) + # { + # $controlStateValue += $resourceStates | Where-Object { $_.InternalId -eq $eventContext.ControlItem.Id }; + # $controlStateValue | ForEach-Object { + # $currentControlStateValue = $_; + # if($null -ne $currentControlStateValue) + # { + # if($this.IsStateActive($eventContext, $currentControlStateValue)) + # { + # $controlState += $currentControlStateValue; + # } + # else + # { + # #add to the dirty state list so that it can be removed later + # $this.DirtyResourceStates += $currentControlStateValue; + # } + # } + # } + # } + # elseif($null -eq $resourceStates) + # { + # $tempHasRequiredAccess = $false; + # } + } + catch + { + $this.EvaluationError($_); + } + + $eventContext.ControlResults | + ForEach-Object { + try + { + $currentItem = $_; + # Copy the current result to Actual Result field + $currentItem.ActualVerificationResult = $currentItem.VerificationResult; + + #Logic to append the control result with the permissions metadata + [SessionContext] $sc = $currentItem.CurrentSessionContext; + # $sc.Permissions.HasAttestationWritePermissions = $this.ControlStateExt.HasControlStateWriteAccessPermissions(); + # $sc.Permissions.HasAttestationReadPermissions = $this.ControlStateExt.HasControlStateReadAccessPermissions(); + # marking the required access as false if there was any error reading the attestation data + $sc.Permissions.HasRequiredAccess = $sc.Permissions.HasRequiredAccess -and $tempHasRequiredAccess; + + # Disable the fix control feature + if(-not $this.GenerateFixScript) + { + $currentItem.EnableFixControl = $false; + } + + if($currentItem.StateManagement.CurrentStateData -and $currentItem.StateManagement.CurrentStateData.DataObject -and $eventContext.ControlItem.DataObjectProperties) + { + $currentItem.StateManagement.CurrentStateData.DataObject = [Helpers]::SelectMembers($currentItem.StateManagement.CurrentStateData.DataObject, $eventContext.ControlItem.DataObjectProperties); + } + + if($controlState.Count -ne 0) + { + # Process the state if its available + $childResourceState = $controlState | Where-Object { $_.ChildResourceName -eq $currentItem.ChildResourceName } | Select-Object -First 1; + if($childResourceState) + { + # Skip passed ones from State Management + if($currentItem.ActualVerificationResult -ne [VerificationResult]::Passed) + { + #compare the states + if(($childResourceState.ActualVerificationResult -eq $currentItem.ActualVerificationResult) -and $childResourceState.State) + { + $currentItem.StateManagement.AttestedStateData = $childResourceState.State; + + # Compare dataobject property of State + if($null -ne $childResourceState.State.DataObject) + { + if($currentItem.StateManagement.CurrentStateData -and $null -ne $currentItem.StateManagement.CurrentStateData.DataObject) + { + $currentStateDataObject = [Helpers]::ConvertToJsonCustom($currentItem.StateManagement.CurrentStateData.DataObject) | ConvertFrom-Json + + try + { + # Objects match, change result based on attestation status + if($eventContext.ControlItem.AttestComparisionType -and $eventContext.ControlItem.AttestComparisionType -eq [ComparisionType]::NumLesserOrEqual) + { + if([Helpers]::CompareObject($childResourceState.State.DataObject, $currentStateDataObject, $true,$eventContext.ControlItem.AttestComparisionType)) + { + $this.ModifyControlResult($currentItem, $childResourceState); + } + + } + else + { + if([Helpers]::CompareObject($childResourceState.State.DataObject, $currentStateDataObject, $true)) + { + $this.ModifyControlResult($currentItem, $childResourceState); + } + } + } + catch + { + $this.EvaluationError($_); + } + } + } + else + { + if($currentItem.StateManagement.CurrentStateData) + { + if($null -eq $currentItem.StateManagement.CurrentStateData.DataObject) + { + # No object is persisted, change result based on attestation status + $this.ModifyControlResult($currentItem, $childResourceState); + } + } + else + { + # No object is persisted, change result based on attestation status + $this.ModifyControlResult($currentItem, $childResourceState); + } + } + } + } + else + { + #add to the dirty state list so that it can be removed later + $this.DirtyResourceStates += $childResourceState + } + } + } + } + catch + { + $this.EvaluationError($_); + } + }; + } + + # State Machine implementation of modifying verification result + hidden [void] ModifyControlResult([ControlResult] $controlResult, [ControlState] $controlState) + { + # No action required if Attestation status is None OR verification result is Passed + if($controlState.AttestationStatus -ne [AttestationStatus]::None -or $controlResult.VerificationResult -ne [VerificationResult]::Passed) + { + $controlResult.AttestationStatus = $controlState.AttestationStatus; + $controlResult.VerificationResult = [Helpers]::EvaluateVerificationResult($controlResult.VerificationResult, $controlState.AttestationStatus); + } + } + + # hidden [ControlState[]] GetResourceState() + # { + # if($null -eq $this.ResourceState) + # { + # $this.ResourceState = @(); + # if($this.ControlStateExt -and $this.ControlStateExt.HasControlStateReadAccessPermissions()) + # { + # $resourceStates = $this.ControlStateExt.GetControlState($this.GetResourceId()) + # if($null -ne $resourceStates) + # { + # $this.ResourceState += $resourceStates + # } + # else + # { + # return $null; + # } + # } + # } + + # return $this.ResourceState; + # } + + #Function to validate attestation data expiry validation + hidden [bool] IsStateActive([SVTEventContext] $eventcontext,[ControlState] $controlState) + { + try + { + $expiryIndays = $this.CalculateExpirationInDays([SVTEventContext] $eventcontext,[ControlState] $controlState); + #Validate if expiry period is passed + if($expiryIndays -ne -1 -and $controlState.State.AttestedDate.AddDays($expiryIndays) -lt [DateTime]::UtcNow) + { + return $false + } + else + { + $controlState.State.ExpiryDate = ($controlState.State.AttestedDate.AddDays($expiryIndays)).ToString("MM/dd/yyyy"); + return $true + } + } + catch{ + #if any exception occurs while getting/validating expiry period, return true. + $this.EvaluationError($_); + return $true + } + } + + hidden [int] CalculateExpirationInDays([SVTEventContext] $eventcontext,[ControlState] $controlState) + { + try + { + #Get controls expiry period. Default value is zero + $controlAttestationExpiry = $eventcontext.controlItem.AttestationExpiryPeriodInDays + $controlSeverity = $eventcontext.controlItem.ControlSeverity + $controlSeverityExpiryPeriod = 0 + $defaultAttestationExpiryInDays = [Constants]::DefaultControlExpiryInDays; + $expiryInDays=-1; + if(($eventcontext.ControlResults |Measure-Object).Count -gt 0) + { + $isControlInGrace=$eventcontext.ControlResults.IsControlInGrace; + } + else + { + $isControlInGrace=$true; + } + if([Helpers]::CheckMember($this.ControlSettings,"AttestationExpiryPeriodInDays") ` + -and [Helpers]::CheckMember($this.ControlSettings.AttestationExpiryPeriodInDays,"Default") ` + -and $this.ControlSettings.AttestationExpiryPeriodInDays.Default -gt 0) + { + $defaultAttestationExpiryInDays = $this.ControlSettings.AttestationExpiryPeriodInDays.Default + } + #Expiry in the case of WillFixLater or StateConfirmed/Recurring Attestation state will be based on Control Severity. + if($controlState.AttestationStatus -eq [AttestationStatus]::NotAnIssue -or $controlState.AttestationStatus -eq [AttestationStatus]::NotApplicable) + { + $expiryInDays=$defaultAttestationExpiryInDays; + } + else + { + # Expire WillFixLater if GracePeriod has expired + if(-not($isControlInGrace) -and $controlState.AttestationStatus -eq [AttestationStatus]::WillFixLater) + { + $expiryInDays=0; + } + else + { + if($controlAttestationExpiry -ne 0) + { + $expiryInDays = $controlAttestationExpiry + } + elseif([Helpers]::CheckMember($this.ControlSettings,"AttestationExpiryPeriodInDays")) + { + $controlsev = $this.ControlSettings.ControlSeverity.PSobject.Properties | Where-Object Value -eq $controlSeverity | Select-Object -First 1 + $controlSeverity = $controlsev.name + #Check if control severity has expiry period + if([Helpers]::CheckMember($this.ControlSettings.AttestationExpiryPeriodInDays.ControlSeverity,$controlSeverity) ) + { + $expiryInDays = $this.ControlSettings.AttestationExpiryPeriodInDays.ControlSeverity.$controlSeverity + } + #If control item and severity does not contain expiry period, assign default value + else + { + $expiryInDays = $defaultAttestationExpiryInDays + } + } + #Return -1 when expiry is not defined + else + { + $expiryInDays = -1 + } + } + } + } + catch{ + #if any exception occurs while getting/validating expiry period, return -1. + $this.EvaluationError($_); + $expiryInDays = -1 + } + return $expiryInDays + } + + hidden AddResourceMetadata([PSObject] $metadataObj) + { + + [hashtable] $resourceMetadata = New-Object -TypeName Hashtable; + $metadataObj.psobject.properties | + ForEach-Object { + $resourceMetadata.Add($_.name, $_.value) + } + + $this.ResourceContext.ResourceMetadata = $resourceMetadata + + } + + + hidden [SVTResource] CreateSVTResource([string] $ConnectionResourceId,[string] $ResourceGroupName, [string] $ConnectionResourceName, [string] $ResourceType, [string] $Location, [string] $MappingName) + { + $svtResource = [SVTResource]::new(); + $svtResource.ResourceId = $ConnectionResourceId; + $svtResource.ResourceGroupName = $ResourceGroupName; + $svtResource.ResourceName = $ConnectionResourceName + $svtResource.ResourceType = $ResourceType; # + $svtResource.Location = $Location; + $svtResource.ResourceTypeMapping = ([SVTMapping]::Mapping | + Where-Object { $_.ResourceTypeName -eq $MappingName } | + Select-Object -First 1); + + return $svtResource; + } + + hidden [void] GetDataFromSubscriptionReport($singleControlResult) + { + try + { + $azskConfig = [ConfigurationManager]::GetAzSKConfigData(); + $settingStoreComplianceSummaryInUserSubscriptions = [ConfigurationManager]::GetAzSKSettings().StoreComplianceSummaryInUserSubscriptions; + #return if feature is turned off at server config + if(-not $azskConfig.StoreComplianceSummaryInUserSubscriptions -and -not $settingStoreComplianceSummaryInUserSubscriptions) {return;} + + if(($this.ComplianceStateData | Measure-Object).Count -gt 0) + { + $ResourceData = @(); + $PersistedControlScanResult=@(); + + #$ResourceScanResult=$ResourceData.ResourceScanResult + [ControlResult[]] $controlsResults = @(); + $singleControlResult.ControlResults | ForEach-Object { + $currentControl=$_ + $partsToHash = $singleControlResult.ControlItem.Id; + if(-not [string]::IsNullOrWhiteSpace($currentControl.ChildResourceName)) + { + $partsToHash = $partsToHash + ":" + $currentControl.ChildResourceName; + } + $rowKey = [Helpers]::ComputeHash($partsToHash.ToLower()); + + $matchedControlResult = $this.ComplianceStateData | Where-Object { $_.RowKey -eq $rowKey} + + # initialize default values + $currentControl.FirstScannedOn = [DateTime]::UtcNow + if($currentControl.ActualVerificationResult -ne [VerificationResult]::Passed) + { + $currentControl.FirstFailedOn = [DateTime]::UtcNow + } + if($null -ne $matchedControlResult -and ($matchedControlResult | Measure-Object).Count -gt 0) + { + $currentControl.UserComments = $matchedControlResult.UserComments + $currentControl.FirstFailedOn = [datetime] $matchedControlResult.FirstFailedOn + $currentControl.FirstScannedOn = [datetime] $matchedControlResult.FirstScannedOn + } + + $scanFromDays = [System.DateTime]::UtcNow.Subtract($currentControl.FirstScannedOn) + + $currentControl.MaximumAllowedGraceDays = $this.CalculateGraceInDays($singleControlResult); + + # Setting isControlInGrace Flag + if($scanFromDays.Days -le $currentControl.MaximumAllowedGraceDays) + { + $currentControl.IsControlInGrace = $true + } + else + { + $currentControl.IsControlInGrace = $false + } + + $controlsResults+=$currentControl + } + $singleControlResult.ControlResults=$controlsResults + } + } + catch + { + $this.PublishException($_); + } + } + + + [int] hidden CalculateGraceInDays([SVTEventContext] $context) + { + + $controlResult=$context.ControlResults; + $computedGraceDays=15; + $ControlBasedGraceExpiryInDays=0; + $currentControlItem=$context.controlItem; + $controlSeverity=$currentControlItem.ControlSeverity; + if([Helpers]::CheckMember($this.ControlSettings,"NewControlGracePeriodInDays")) + { + if([Helpers]::CheckMember($this.ControlSettings,"ControlSeverity")) + { + $controlsev = $this.ControlSettings.ControlSeverity.PSobject.Properties | Where-Object Value -eq $controlSeverity | Select-Object -First 1 + $controlSeverity = $controlsev.name + $computedGraceDays=$this.ControlSettings.NewControlGracePeriodInDays.ControlSeverity.$ControlSeverity; + } + else + { + $computedGraceDays=$this.ControlSettings.NewControlGracePeriodInDays.ControlSeverity.$ControlSeverity; + } + } + if($null -ne $currentControlItem.GraceExpiryDate) + { + if($currentControlItem.GraceExpiryDate -gt [DateTime]::UtcNow ) + { + $ControlBasedGraceExpiryInDays=$currentControlItem.GraceExpiryDate.Subtract($controlResult.FirstScannedOn).Days + if($ControlBasedGraceExpiryInDays -gt $computedGraceDays) + { + $computedGraceDays = $ControlBasedGraceExpiryInDays + } + } + } + + return $computedGraceDays; + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Abstracts/SVTCommandBase.ps1 b/src/AzSK.AAD/0.9.0/Framework/Abstracts/SVTCommandBase.ps1 new file mode 100644 index 000000000..4a5b3085f --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Abstracts/SVTCommandBase.ps1 @@ -0,0 +1,298 @@ +using namespace System.Management.Automation +Set-StrictMode -Version Latest +# Base class for SVT classes being called from PS commands +# Provides functionality to fire important events at command call +class SVTCommandBase: CommandBase { + [string[]] $ExcludeTags = @(); + [string[]] $ControlIds = @(); + [string[]] $ExcludeControlIds = @(); + [string] $ControlIdString = ""; + [string] $ExcludeControlIdString = ""; + [bool] $UsePartialCommits; + [bool] $UseBaselineControls; + [PSObject] $CentralStorageAccount; + [string] $PartialScanIdentifier = [string]::Empty; + #hidden [ControlStateExtension] $ControlStateExt; + hidden [bool] $UserHasStateAccess = $false; + [bool] $GenerateFixScript = $false; + [bool] $IncludeUserComments = $false; + [AttestationOptions] $AttestationOptions; + hidden [string] $AttestationUniqueRunId; + + + SVTCommandBase([string] $tenantId, [InvocationInfo] $invocationContext): + Base($tenantId, $invocationContext) { + [Helpers]::AbstractClass($this, [SVTCommandBase]); + $this.CheckAndDisableAzureRMTelemetry() + $this.AttestationUniqueRunId = $(Get-Date -format "yyyyMMdd_HHmmss"); + #Fetching the resourceInventory once for each SVT command execution + [ResourceInventory]::Clear(); + } + + hidden [SVTEventContext] CreateSVTEventContextObject() { + return [SVTEventContext]@{ + TenantContext = $this.TenantContext; + PartialScanIdentifier = $this.PartialScanIdentifier + }; + } + + hidden [void] ClearSingletons() + { + } + + hidden [void] CommandStarted() { + + $this.ClearSingletons(); + + [SVTEventContext] $arg = $this.CreateSVTEventContextObject(); + $this.InitializeControlState(); + $versionMessage = $this.CheckModuleVersion(); + if ($versionMessage) { + $arg.Messages += $versionMessage; + } + + if ($null -ne $this.AttestationOptions -and $this.AttestationOptions.AttestControls -eq [AttestControls]::NotAttested -and $this.AttestationOptions.IsBulkClearModeOn) { + throw [SuppressedException] ("The 'BulkClear' option does not apply to 'NotAttested' controls.`n") + } + #check to limit multi controlids in the bulk attestation mode + $ctrlIds = $this.ConvertToStringArray($this.ControlIdString); + if ($null -ne $this.AttestationOptions -and (-not [string]::IsNullOrWhiteSpace($this.AttestationOptions.JustificationText) -or $this.AttestationOptions.IsBulkClearModeOn) -and ($ctrlIds.Count -gt 1 -or $this.UseBaselineControls)) { + if($this.UseBaselineControls) + { + throw [SuppressedException] ("UseBaselineControls flag should not be passed in case of Bulk attestation. This results in multiple controls. `nBulk attestation mode supports only one controlId at a time.`n") + } + else + { + throw [SuppressedException] ("Multiple controlIds specified. `nBulk attestation mode supports only one controlId at a time.`n") + } + } + + #Create necessary resources to save compliance data in user's subscription + $this.PublishEvent([SVTEvent]::CommandStarted, $arg); + } + + [void] PostCommandStartedAction() + { + $isPolicyInitiativeEnabled = [FeatureFlightingManager]::GetFeatureStatus("EnableAzurePolicyBasedScan",$($this.TenantContext.tenantId)) + if($isPolicyInitiativeEnabled) + { + $this.PostPolicyComplianceTelemetry() + } + } + [void] PostPolicyComplianceTelemetry() + { + [CustomData] $customData = [CustomData]::new(); + $customData.Name = "PolicyComplianceTelemetry"; + $customData.Value = $this.TenantContext.tenantId; + $this.PublishCustomData($customData); + } + hidden [void] CommandError([System.Management.Automation.ErrorRecord] $exception) { + [SVTEventContext] $arg = $this.CreateSVTEventContextObject(); + $arg.ExceptionMessage = $exception; + + $this.PublishEvent([SVTEvent]::CommandError, $arg); + $this.CheckAndEnableAzureRMTelemetry() + } + + hidden [void] CommandCompleted([SVTEventContext[]] $arguments) { + $this.PublishEvent([SVTEvent]::CommandCompleted, $arguments); + $this.CheckAndEnableAzureRMTelemetry() + } + + [string] EvaluateControlStatus() { + return ([CommandBase]$this).InvokeFunction($this.RunAllControls); + } + + # Dummy function declaration to define the function signature + # Function is supposed to override in derived class + hidden [SVTEventContext[]] RunAllControls() { + return @(); + } + + hidden [void] SetSVTBaseProperties([PSObject] $svtObject) { + $svtObject.FilterTags = $this.ConvertToStringArray($this.FilterTags); + $svtObject.ExcludeTags = $this.ConvertToStringArray($this.ExcludeTags); + $svtObject.ControlIds += $this.ControlIds; + $svtObject.ControlIds += $this.ConvertToStringArray($this.ControlIdString); + $svtObject.ExcludeControlIds += $this.ExcludeControlIds; + $svtObject.ExcludeControlIds += $this.ConvertToStringArray($this.ExcludeControlIdString); + $svtObject.GenerateFixScript = $this.GenerateFixScript; + $svtObject.InvocationContext = $this.InvocationContext; + # ToDo: Assumption: usercomment will only work when storage report feature flag is enable + #$resourceId = $svtObject.GetResourceId(); + #$svtObject.ComplianceStateData = $this.FetchComplianceStateData($resourceId); + + #Include Server Side Exclude Tags + $svtObject.ExcludeTags += [ConfigurationManager]::GetAzSKConfigData().DefaultControlExculdeTags + + #Include Server Side Filter Tags + $svtObject.FilterTags += [ConfigurationManager]::GetAzSKConfigData().DefaultControlFiltersTags + + #Set Partial Unique Identifier + if($svtObject.ResourceContext) + { + $svtObject.PartialScanIdentifier =$this.PartialScanIdentifier + } + + # ToDo: Utilize exiting functions + $this.InitializeControlState(); + #$svtObject.ControlStateExt = $this.ControlStateExt; + } + + hidden [void] InitializeControlState() { + # if (-not $this.ControlStateExt) { + # $this.ControlStateExt = [ControlStateExtension]::new($this.TenantContext, $this.InvocationContext); + # $this.ControlStateExt.UniqueRunId = $this.AttestationUniqueRunId + # $this.ControlStateExt.Initialize($false); + # $this.UserHasStateAccess = $this.ControlStateExt.HasControlStateReadAccessPermissions(); + # } + } + + [void] PostCommandCompletedAction([SVTEventContext[]] $arguments) { + #TODO: Attestation for AAD controls? + # if ($this.AttestationOptions -ne $null -and $this.AttestationOptions.AttestControls -ne [AttestControls]::None) { + # try { + # [SVTControlAttestation] $svtControlAttestation = [SVTControlAttestation]::new($arguments, $this.AttestationOptions, $this.TenantContext, $this.InvocationContext); + # #The current context user would be able to read the storage blob only if he has minimum of contributor access. + # if ($svtControlAttestation.controlStateExtension.HasControlStateReadAccessPermissions()) { + # if (-not [string]::IsNullOrWhiteSpace($this.AttestationOptions.JustificationText) -or $this.AttestationOptions.IsBulkClearModeOn) { + # $this.PublishCustomMessage([Constants]::HashLine + "`n`nStarting Control Attestation workflow in bulk mode...`n`n"); + # } + # else { + # $this.PublishCustomMessage([Constants]::HashLine + "`n`nStarting Control Attestation workflow...`n`n"); + # } + # [MessageData] $data = [MessageData]@{ + # Message = ([Constants]::SingleDashLine + "`nWarning: `nPlease use utmost discretion when attesting controls. In particular, when choosing to not fix a failing control, you are taking accountability that nothing will go wrong even though security is not correctly/fully configured. `nAlso, please ensure that you provide an apt justification for each attested control to capture the rationale behind your decision.`n"); + # MessageType = [MessageType]::Warning; + # }; + # $this.PublishCustomMessage($data) + # $response = "" + # while ($response.Trim() -ne "y" -and $response.Trim() -ne "n") { + # if (-not [string]::IsNullOrEmpty($response)) { + # Write-Host "Please select appropriate option." + # } + # $response = Read-Host "Do you want to continue (Y/N)" + # } + # if ($response.Trim() -eq "y") { + # $svtControlAttestation.StartControlAttestation(); + # } + # else { + # $this.PublishCustomMessage("Exiting the control attestation process.") + # } + # } + # else { + # [MessageData] $data = [MessageData]@{ + # Message = "You don't have the required permissions to perform control attestation. If you'd like to perform control attestation, please request your subscription owner to grant you 'Contributor' access to the '$([ConfigurationManager]::GetAzSKConfigData().AzSKRGName)' resource group."; + # MessageType = [MessageType]::Error; + # }; + # $this.PublishCustomMessage($data) + # } + # } + # catch { + # $this.CommandError($_); + # } + # } + } + + hidden [void] CheckAndDisableAzureRMTelemetry() + { + #Disable AzureRM telemetry setting until scan is completed. + #This has been added to improve the performarnce of scan commands + #Telemetry will be re-enabled once scan is completed + # BUGBUG Write-Warning("Disabling AzureRm/Az telemetry - investigate why needed?") + $dataCollectionPath = "$env:APPDATA\Windows Azure Powershell\AzurePSDataCollectionProfile.json" + if(Test-Path -Path $dataCollectionPath) + { + $dataCollectionProfile = Get-Content -path $dataCollectionPath | ConvertFrom-Json + if($dataCollectionProfile.enableAzureDataCollection) + { + #Keep settings in + $AzureRMDataCollectionSettingFolderpath= [Constants]::AzSKAppFolderPath + "\AzureRMDataCollectionSettings" + if(-not (Test-Path -Path $AzureRMDataCollectionSettingFolderpath)) + { + mkdir -Path $AzureRMDataCollectionSettingFolderpath -Force + } + + $AzureRMDataCollectionFilePath = $AzureRMDataCollectionSettingFolderpath + "\AzurePSDataCollectionProfile.json" + if(-not (Test-Path -Path $AzureRMDataCollectionFilePath)) + { + Copy-Item $dataCollectionPath $AzureRMDataCollectionFilePath + } + Disable-AzDataCollection | Out-Null + } + } + } + + hidden [void] CheckAndEnableAzureRMTelemetry() + { + #Enabled AzureRM telemetry which got disabled at the start of command + $AzureRMDataCollectionSettingFilepath= [Constants]::AzSKAppFolderPath + "\AzureRMDataCollectionSettings\AzurePSDataCollectionProfile.json" + if(Test-Path -Path $AzureRMDataCollectionSettingFilepath) + { + $dataCollectionProfile = Get-Content -path $AzureRMDataCollectionSettingFilepath | ConvertFrom-Json + if($dataCollectionProfile -and $dataCollectionProfile.enableAzureDataCollection) + { + Enable-AzDataCollection | Out-Null + } + } + + } + + hidden [void] RemoveOldAzSDKRG() + { + $scanSource = [AzSKSettings]::GetInstance().GetScanSource(); + if($scanSource -eq "SDL" -or [string]::IsNullOrWhiteSpace($scanSource)) + { + $olderRG = Get-AzResourceGroup -Name $([OldConstants]::AzSDKRGName) -ErrorAction SilentlyContinue + if($null -ne $olderRG) + { + $resources = Get-AzResource -ResourceGroupName $([OldConstants]::AzSDKRGName) + try { + $azsdkRGScope = "/subscriptions/$($this.TenantContext.tenantId)/resourceGroups/$([OldConstants]::AzSDKRGName)" + $resourceLocks = @(); + $resourceLocks += Get-AzResourceLock -Scope $azsdkRGScope -ErrorAction Stop + if($resourceLocks.Count -gt 0) + { + $resourceLocks | ForEach-Object { + Remove-AzResourceLock -LockId $_.LockId -Force -ErrorAction Stop + } + } + + if(($resources | Measure-Object).Count -gt 0) + { + $otherResources = $resources | Where-Object { -not ($_.Name -like "$([OldConstants]::StorageAccountPreName)*")} + if(($otherResources | Measure-Object).Count -gt 0) + { + Write-Host "WARNING: Found non DevOps Kit resources under older RG [$([OldConstants]::AzSDKRGName)] as shown below:" -ForegroundColor Yellow + $otherResources + Write-Host "We are about to delete the older resource group including all the resources inside." -ForegroundColor Yellow + $option = Read-Host "Do you want to continue (Y/N) ?"; + $option = $option.Trim(); + While($option -ne "y" -and $option -ne "n") + { + Write-Host "Provide correct option (Y/N)." + $option = Read-Host "Do you want to continue (Y/N) ?"; + $option = $option.Trim(); + } + if($option -eq "y") + { + Remove-AzResourceGroup -Name $([OldConstants]::AzSDKRGName) -Force -AsJob + } + } + else + { + Remove-AzResourceGroup -Name $([OldConstants]::AzSDKRGName) -Force -AsJob + } + } + else + { + Remove-AzResourceGroup -Name $([OldConstants]::AzSDKRGName) -Force -AsJob + } + } + catch { + #eat exception + } + } + } + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/AzSK.AAD.Settings.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/AzSK.AAD.Settings.json new file mode 100644 index 000000000..92de082d2 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Configurations/AzSK.AAD.Settings.json @@ -0,0 +1,8 @@ +{ + "UseOnlinePolicyStore": true, + "OnlinePolicyStoreUrl": "https://azsdkossep.azureedge.net/$Version/$FileName", + "EnableAADAuthForOnlinePolicyStore": false, + "UsageTelemetryLevel": "Anonymous", + "LocalControlTelemetryKey": "00000000-0000-0000-0000-000000000000", + "LocalEnableControlTelemetry": false +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/AzSk.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/AzSk.json new file mode 100644 index 000000000..d6aee5de9 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Configurations/AzSk.json @@ -0,0 +1,43 @@ +{ + "MaintenanceMessage": "WARNING: We are making some improvements to the {0} module. It is currently unavailable but will be back in action soon.", + "AzSKRGName": "AzSKRG", + "AzSKRepoURL": "#RepositoryUrl#", + "CASetupRunbookURL": "#CASetupRunbookURL#", + "PublicPSGalleryUrl": "https://www.powershellgallery.com", + "SubscriptionMandatoryTags": [ + "Mandatory" + ], + "DefaultControlExculdeTags": [ "Information" ], + "DefaultControlFiltersTags": [], + "ERvNetResourceGroupNames": "", + "AzSKApiBaseURL": "#ApiBaseURL#", + "PublishVulnDataToApi": false, + "ControlTelemetryKey": "00000000-0000-0000-0000-000000000000", + "EnableControlTelemetry": false, + "PolicyMessage": "Running AzSK cmdlet using a generic (org-neutral) policy...", + "UpdateCompatibleCCVersion": "1.0.0", + "AzSKCAMinReqdRunbookVersion": "2.1709.0", + "AzSKAlertsMinReqdVersion": "2.1709.0", + "AzSKARMPolMinReqdVersion": "2.1709.0", + "AzSKASCMinReqdVersion": "2.1709.0", + "InstallationCommand": "Install-Module -Name AzSK -Scope CurrentUser -AllowClobber -Force", + "OutputFolderPath": "", + "BackwardCompatibleVersionCount": 2, + "AzSKCARunbookVersion": "3.1803.0", + "ConfigSchemaBaseVersion": "3.1803.0", + "RunbookScanAgentBaseVersion": "1.0.0", + "AllowSelfSignedWebhookCertificate": false, + "CAScanIntervalInHours": 24, + "EnableDevOpsKitSetupCheck": false, + "AzSKLocation": "eastus2", + "PrivacyAcceptedSources": [], + "UpdateToLatestVersion": false, + "AzSKConfigURL": "https://azsdkossep.azureedge.net/1.0.0/AzSK.AAD.Pre.json", + "IsAlertMonitoringEnabled": false, + "SupportDL": "azsksupext@microsoft.com", + "PolicyOrgName": "org-neutral", + "StoreComplianceSummaryInUserSubscriptions": false, + "SchemaTemplateURL": "https://azsdkossep.azureedge.net/schemas/3.1803.0/", + "AzSKInitiativeName":"AzSKInitiative-Preview", + "EnableAzurePolicyBasedScan" : false +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/FeatureFlighting.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/FeatureFlighting.json new file mode 100644 index 000000000..cd230ac85 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Configurations/FeatureFlighting.json @@ -0,0 +1,14 @@ +{ + "Version": "3.1810.0", + "Features": [ + { + "Name": "EnableAzurePolicyBasedScan", + "Description" : "", + "Sources" : ["*"], + "EnabledForSubs": [], + "DisabledForSubs": [], + "UnderPreview" : false, + "IsEnabled": false + } + ] +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.Application.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.Application.json new file mode 100644 index 000000000..34026164b --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.Application.json @@ -0,0 +1,165 @@ +{ + "FeatureName": "Application", + "Reference": "aka.ms/azsktcp/Application", + "IsMaintenanceMode": false, + "Controls": [ + { + "ControlID": "AAD_Application_Remove_Test_Demo_Apps", + "Description": "Old test/demo apps should be removed from the tenant", + "Id": "App120", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckOldTestDemoApps", + "Rationale": "Demo apps are usually short-term projects that do not go through the full engineering process and due diligence required for enterprise apps. As a result, it is important to constantly review and prune demo app entries from the tenant.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthZ", + "RBAC" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Application_ReturnURLs_Use_HTTPS", + "Description": "All return URLs configured for an application must be HTTPS endpoints", + "Id": "App130", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckReturnURLsAreHTTPS", + "Rationale": "Return URLs of an application are particularly sensitive because many authentication flows involve posting the token to the returnURL after successful authentication. If such a URL does not use HTTPS, it leads to disclosure of the token on the network in clear text.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthZ", + "RBAC" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Application_Review_Orphaned_Apps", + "Description": "Do not permit orphaned apps (i.e., apps with no owners) in the tenant", + "Id": "App140", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "CheckOrphanedApp", + "Rationale": "From a governance standpoint, it is important that every application has one or more owners who are responsible for the upkeep of the application's record in the tenant, rotating credentials, etc.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthZ", + "RBAC" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Application_Require_FTE_Owner", + "Description": "At least one of the owners of an app must be an FTE", + "Id": "App150", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "CheckAppFTEOwner", + "Rationale": "Guest users in a tenant are often transient. Ensuring that at least one FTE owner is accountable for managing the app, rotating credentials, etc. leads to better app governance.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthZ", + "RBAC" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Application_Minimize_Resource_Access_Requested", + "Description": "Apps should request the least permissions needed to various resources", + "Id": "App160", + "ControlSeverity": "Medium", + "Automated": "No", + "MethodName": "TBD-Later", + "Rationale": "Ensuring that an app requests only those permissions that it needs to function properly in keeping with the principle of least privilege ensures that in the event of a compromise, the damage can be contained.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthZ", + "RBAC" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Application_HomePage_Use_HTTPS", + "Description": "The home page URL for an application must be an HTTPS endpoint", + "Id": "App170", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckHomePageIsHTTPS", + "Rationale": "Using HTTPS ensures that sensitive data is not disclosed during transit and that the application's clients are not spoofed by rogue endpoint posing as the application.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "DP" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Application_LogoutURLs_Use_HTTPS", + "Description": "The logout URL configured for an application must be an HTTPS endpoint", + "Id": "App180", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckLogoutURLIsHTTPS", + "Rationale": "The logout URL for an application is used during authentication flows. Not using an HTTPS URL for this may lead to disclosure of authentication info/tokens.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN", + "DP" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Application_Must_Have_Privacy_Disclosure", + "Description": "All enterprise apps must use a privacy disclosure statement", + "Id": "App190", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "CheckPrivacyDisclosure", + "Rationale": "Adding an appropriate and uniform privacy disclosure for all enterprise apps helps users make correct privacy-related choices when deciding to use the applications. This is also a regulatory requirement in most jurisdictions.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "Privacy" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Application_Must_Restrict_To_Tenant", + "Description": "Enterprise (line of business) apps should be tenant scope only", + "Id": "App200", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckAppIsCurrentTenantOnly", + "Rationale": "Line of business (LOB) applications are usually written to meet a specific company's business needs. Such applications should be restricted to the current tenant only (i.e., the tenant where they are registered).", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated" + ], + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.Device.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.Device.json new file mode 100644 index 000000000..d27bcb98d --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.Device.json @@ -0,0 +1,23 @@ +{ + "FeatureName": "Device", + "Reference": "aka.ms/azsktcp/device", + "IsMaintenanceMode": false, + "Controls": [ + { + "ControlID": "AAD_Device_Review_Stale_Devices", + "Description": "Review and remove stale devices from the directory", + "Id": "Device110", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "CheckStaleDevices", + "Rationale": "Devices that have not been active for long are likely to also be out of compliance in terms of patches and security configuration. Such devices should be regularly reviewed and removed from the tenant (or forced to become compliant) as appropriate.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Manual" + ], + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.Group.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.Group.json new file mode 100644 index 000000000..71b987924 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.Group.json @@ -0,0 +1,43 @@ +{ + "FeatureName": "Group", + "Reference": "aka.ms/azsktcp/group", + "IsMaintenanceMode": false, + "Controls": [ + { + "ControlID": "AAD_Group_Use_Security_Enabled", + "Description": "All AAD groups must be security enabled (TBD)", + "Id": "Group110", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckGroupsIsSecurityEnabled", + "Rationale": "TBD. Need to discuss/review this further.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Manual", + "AuthZ", + "RBAC" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Group_Require_FTE_Owner", + "Description": "Group must have at least one non-guest (native) owner", + "Id": "Group120", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "CheckGroupHasNonGuestOwner", + "Rationale": "Guest users in a tenant can be transient. Ensuring that at least one FTE owner is accountable for managing a group, approving/reviewing membership, etc. leads to better governance.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Manual", + "AuthZ", + "RBAC" + ], + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.ServicePrincipal.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.ServicePrincipal.json new file mode 100644 index 000000000..a46505770 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.ServicePrincipal.json @@ -0,0 +1,58 @@ +{ + "FeatureName": "ServicePrincipal", + "Reference": "aka.ms/azsktcp/serviceprincipal", + "IsMaintenanceMode": false, + "Controls": [ + { + "ControlID": "AAD_ServicePrincipal_Use_Cert_Credentials", + "Description": "SPNs must not use password creds - use cert creds instead", + "Id": "SPN110", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckSPNPasswordCredentials", + "Rationale": "Password credentials tend to be easier to compromise via various attacks. They are also symmetric leading to attack vectors on both ends of the flow. Use of certificate credentials alleviates these shortcomings.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN" + ], + "Enabled": true + }, + { + "ControlID": "AAD_ServicePrincipal_Review_Legacy_SPN", + "Description": "SPNs of type legacy should be carefully reviewed", + "Id": "SPN120", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "ReviewLegacySPN", + "Rationale": "The 'Legacy' SPN type is only for backward compatibility. Ensure that all such entries are carefully reviewed and purged where appropriate.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN" + ], + "Enabled": true + }, + { + "ControlID": "AAD_ServicePrincipal_Check_Key_Expiry", + "Description": "SPN key credentials should be renewed before expiry", + "Id": "SPN130", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckCertNearingExpiry", + "Rationale": "SPN credentials should be rotated in a timely manner to ensure availability of the app/service that is using the SPN.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN" + ], + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.Tenant.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.Tenant.json new file mode 100644 index 000000000..5b44b2827 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.Tenant.json @@ -0,0 +1,370 @@ +{ + "FeatureName": "Tenant", + "Reference": "aka.ms/azsktcp/tenant", + "IsMaintenanceMode": false, + "Controls": [ + { + "ControlID": "AAD_Tenant_RBAC_Grant_Limited_Access_To_Guests", + "Description": "Guests must not be granted full access to the directory", + "Id": "Tenant110", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckGuestsHaveLimitedAccess", + "Rationale": "Guest users are normally external users who have been invited to the tenant for conducting specific activities. In keeping with the principle of least privilege, Guest users should be allowed limited access to the directory.", + "Recommendation": "Refer: https://docs.microsoft.com/ TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "RBAC" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_RBAC_Dont_Permit_Guests_To_Invite_Guests", + "Description": "Guests must not be allowed to invite other guests", + "Id": "Tenant111", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckGuestsIfCanInvite", + "Rationale": "Privileges granted to Guest members need to be limited. Allowing Guests to invite other guests dilutes the least privilege desired for such users.", + "Recommendation": "Refer: https://docs.microsoft.com/ TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "RBAC" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_MFA_Required_For_Admins", + "Description": "Admins must use baseline MFA policy", + "Id": "Tenant120", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckBaselineMFAPolicyForAdmins", + "Rationale": "Multi-factor authentication significantly reduces the likelihood of account compromise via various password-stealing/cracking attacks. While enabling this is recommended for all users, it is something that tenant admins must absolutely use because a comrpomise of a single admin password effectively renders the entire directory to the mercy of the attacker.", + "Recommendation": "Go to..TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_Apps_Dont_Allow_Users_To_Create_Apps", + "Description": "Do not permit users to create apps in tenant", + "Id": "Tenant130", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckUserPermissionsToCreateApps", + "Rationale": "An application in the tenant can introduce pathways through which tenant data can be accessed by users of the application. Care needs to be exercised in ensuring that only carefully scrutinized applications are created (and subsequently maintained) in the tenant. As a default, it is better to not permit regular users to create applications.", + "Recommendation": "Go to..TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthZ" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_RBAC_Dont_Allow_Users_To_Invite_Guests", + "Description": "Do not permit users to invite guests to the tenant", + "Id": "Tenant140", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckUserPermissionToInviteGuests", + "Rationale": "Guest users are created in the tenant for enabling certain limited access scenarios (e.g., to facilitate collaboration in a specific project, etc.). Due governance must be exercised over creation and management of Guest accounts. As a default, it is a good practice to not permit regular users to invite Guests.", + "Recommendation": "Go to..TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthZ" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_SSPR_Require_Min_Questions_To_Reset", + "Description": "At least 3 questions should be required for password reset", + "Id": "Tenant150", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "CheckMinQuestionsForSSPR", + "Rationale": "It is important to ensure that a password reset cannot be carried out by someone posing to be a specific user. By involving multiple fact-checking questions, high levels of assurance can be reached before a password reset is permitted.", + "Recommendation": "Go to..TODO-sspr", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN", + "SSPR" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_SSPR_Enable_User_Notification_On_Password_Reset", + "Description": "Users must be notified upon password reset", + "Id": "Tenant160", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "CheckUserNotificationUponSSPR", + "Rationale": "Attempts to reset password (whether successful or not) should be considered a sensitive activity. It is important to notify the user at their email account in the tenant about this. This can alert the user in a timely manner if the password reset was not initiated by them.", + "Recommendation": "Go to..TODO-sspr", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN", + "SSPR" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_SSPR_Enable_Admin_Notify_On_Admin_Password_Reset", + "Description": "All admins must be notified upon any admin password reset", + "Id": "Tenant170", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckAdminNotificationUponSSPR", + "Rationale": "A password reset flow initiated by an admin is a highly sensitive activity. All admins should be notified about it (whether the attempt was successful or not). A timely notification to other admins can help salvage a situation where an attacker is attempting a password reset posing as one of the admins.", + "Recommendation": "Go to..TODO-sspr", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN", + "SSPR" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_Misc_Set_Security_Contact_Info", + "Description": "Security compliance notification phone and email must be set", + "Id": "Tenant180", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "CheckTenantSecurityContactInfoIsSet", + "Rationale": "Setting up security contact notification details ensures that in the event of a security incident, responsible parties can be reached quickly.", + "Recommendation": "Refer: https://docs.microsoft.com/ TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "RBAC" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_Device_Require_MFA_For_Join", + "Description": "Enable 'require MFA' for joining devices to tenant", + "Id": "Tenant190", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "CheckRequireMFAForJoin", + "Rationale": "Joining devices to a tenant should be treated as a sensitive activity. Requiring multi-factor authentication ensures that there is a higher level of assurance and accountability involved in the process.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Manual", + "AuthZ", + "RBAC" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_Device_Set_Max_Per_User_Limit", + "Description": "Set a max device limit for users in the tenant", + "Id": "Tenant200", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "CheckMaxDeviceLimitSet", + "Rationale": "If users do not have any restriction on the number of devices they can add, it leads to bloat and collection of stale entries. Moreover, forcing a reasonable limit also ensures that users regularly removed outdated and potential weaker security platform devies from the directory when they add newer ones.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Manual", + "AuthZ", + "RBAC" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_MFA_Review_Bypassed_Users", + "Description": "Review list of current 'MFA-bypassed' users in the tenant", + "Id": "Tenant180", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "MFAReviewBypassedUsers", + "Rationale": "When multi-factor authentication is required for users across the tenant, any exceptions should be carefully scrutinized and kept limited in number and time. This is because in that interval, such user accounts represent a risk to the tenant because of the higher exposure to password-theft attacks.", + "Recommendation": "Go to..TODO-mfa", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_MFA_Allow_Users_To_Notify_About_Fraud", + "Description": "Allow users to send notifications about possible fraud", + "Id": "Tenant190", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "MFACheckUsersCanNotifyFraud", + "Rationale": "Security is every tenant members responsibility. Allowing users to send notification about possible fraudulent activity encourages their participation in keeping the tenant secure.", + "Recommendation": "Go to..TODO-mfa", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_SSPR_Require_Min_AuthN_Methods", + "Description": "Require at least two authentication methods for password reset", + "Id": "Tenant200", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "SSPRMinAuthNMethodsRequired", + "Rationale": "It is important to ensure that a password reset cannot be carried out by someone posing to be a specific user. By requiring at least two different methods of verification, a higher level of assurance can be reached before a password reset is permitted.", + "Recommendation": "Go to..TODO-sspr", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN", + "SSPR" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_Apps_Regulate_Data_Access_Approval", + "Description": "Do not allow users to approve tenant data access for external apps", + "Id": "Tenant210", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckTenantDataAccessForApps", + "Rationale": "Third-party apps often request permission to access data about users in the tenant. It is important to perform due diligence before this permission is granted to such apps. Do not allow regular users to grant this permission to apps.", + "Recommendation": "Go to..TODO-apps-da", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthZ" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_RBAC_Keep_Min_Global_Admins", + "Description": "Include at least three members in global admin role", + "Id": "Tenant220", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckEnoughGlobalAdmins", + "Rationale": "TODO-rbac-min-3-admins.", + "Recommendation": "The global (company) admin role is super critical in the context of an AAD tenant. It is important to ensure that there is enough redundancy to cater to any kind of exigency. Ensuring that at least 3 different people can perform the activities corresponding to this role makes for good contingency planning in the context of tenant management and administration.", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthZ" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_RBAC_Dont_Have_Guests_As_Global_Admins", + "Description": "Guest users must not be made members of global admin role", + "Id": "Tenant230", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckNoGuestsInGlobalAdminRole", + "Rationale": "Guest users are normally external users who have been invited to the tenant for conducting specific activities. In keeping with the principle of least privilege, Guest users should be allowed limited access to the directory. In particular, Guest should not be made members of any directory administration roles...in the particular 'Global Admin' role.", + "Recommendation": "Go to..TODO-RBAC-no-guest-admins", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_AuthN_Use_Custom_Banned_Passwords", + "Description": "Ensure that custom banned passwords list is configured for use", + "Id": "Tenant240", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "CheckCustomBannedPasswordConfig", + "Rationale": "Although AAD uses a common 'banned passwords' list, user accounts in your tenant will be more secure if you configure additional passwords that are locality/region specific. The 'custom banned passwords' feature supports this requirement.", + "Recommendation": "Go to..TODO-RBAC-custom-banned-pswd-config", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_AuthN_Enforce_Banned_Passwords_OnPrem", + "Description": "Ensure that banned password check is enabled on-prem and set to 'Enforce' level", + "Id": "Tenant250", + "ControlSeverity": "Medium", + "Automated": "Yes", + "MethodName": "CheckOnPremBannedPasswordsEnforced", + "Rationale": "Use of banned passwords should be barred regardless of whether the user sets a password on-prem or in the cloud. Using the 'Enforce' mode as opposed to 'Audit' ensures that users will not be able to set banned passwords on-prem.", + "Recommendation": "Go to..TODO-AuthN-on-prem-banned-pswd", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_Privacy_Configure_Valid_Privacy_Contact", + "Description": "Ensure that tenant-wide privacy contact email is set to a valid (current) non-guest user", + "Id": "Tenant260", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckPrivacyContactIsValid", + "Rationale": "A correct tenant-wide privacy contact setting ensures that internal/external users are aware of who should be contacted for resolution/clarification of privacy issues. This is also a regulatory requirement in most jurisdictions.", + "Recommendation": "Go to..TODO-Priv-contact-mail-valid", + "Tags": [ + "SDL", + "TCP", + "Automated", + "Privacy" + ], + "Enabled": true + }, + { + "ControlID": "AAD_Tenant_Privacy_Configure_Valid_Privacy_Statement", + "Description": "Ensure that a privacy statement is configured and points to a valid URL", + "Id": "Tenant270", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckPrivacyStatementIsValid", + "Rationale": "The privacy disclosure/statement helps internal and external users understand how their personal data is processed by in the tenant/directory environment. This is also a regulatory requirement in most jurisdictions.", + "Recommendation": "Go to..TODO-Priv-contact-mail-valid", + "Tags": [ + "SDL", + "TCP", + "Automated", + "Privacy" + ], + "Enabled": true + } + ] + } \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.User.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.User.json new file mode 100644 index 000000000..ccc7de57f --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AAD/AAD.User.json @@ -0,0 +1,58 @@ +{ + "FeatureName": "User", + "Reference": "aka.ms/azsktcp/user", + "IsMaintenanceMode": false, + "Controls": [ + { + "ControlID": "AAD_User_DirSync_Setting_Should_Match_Tenant", + "Description": "A user's dirsync-enabled setting must match the tenant level setting", + "Id": "User110", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckUserDirSyncSetting", + "Rationale": "When a tenant is setup with dir-sync, users are usually created on-premise and synchronized outbound. In such a case, seeing a user object with dirsync setting that does not match the tenant's setting is likely an anomaly and needs scrutiny.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN" + ], + "Enabled": true + }, + { + "ControlID": "AAD_User_Do_Not_Disable_Password_Expiration", + "Description": "Do not disable password expiration policy for users", + "Id": "User120", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckPasswordExpiration", + "Rationale": "Users with password expiration disabled represent a long term risk to the tenant in the event of password compromise. Ensuring that password expiration is enabled for everyone ensures that the window of attack is limited.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN" + ], + "Enabled": true + }, + { + "ControlID": "AAD_User_Do_Not_Disable_Strong_Password", + "Description": "Do not disable strong password policy for users", + "Id": "User130", + "ControlSeverity": "High", + "Automated": "Yes", + "MethodName": "CheckStrongPassword", + "Rationale": "Strong passwords are harder to compromise. When strong passwords are disabled for a user, their account becomes vulnerable to various brute-force password guessing/cracking attacks.", + "Recommendation": "Refer: TODO", + "Tags": [ + "SDL", + "TCP", + "Automated", + "AuthN" + ], + "Enabled": true + } +] +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AllResourceTypes.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AllResourceTypes.json new file mode 100644 index 000000000..1de2c7e55 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/AllResourceTypes.json @@ -0,0 +1,46 @@ +[ + "microsoft.insights/components", + "Microsoft.Storage/storageAccounts", + "Microsoft.Web/sites", + "Microsoft.Compute/virtualMachines", + "Microsoft.Sql/servers", + "Microsoft.ServiceBus/namespaces", + "microsoft.insights/webtests", + "Microsoft.Network/virtualNetworks", + "Microsoft.OperationsManagement/solutions", + "Microsoft.Logic/workflows", + "Microsoft.Network/trafficmanagerprofiles", + "Microsoft.Cache/Redis", + "Microsoft.KeyVault/vaults", + "Microsoft.DataFactory/dataFactories", + "Microsoft.Network/loadBalancers", + "Microsoft.Eventhub/namespaces", + "Microsoft.StreamAnalytics/streamingjobs", + "Microsoft.BizTalkServices/BizTalk", + "Microsoft.AppService/apiapps", + "Microsoft.Automation/automationAccounts", + "Microsoft.DocumentDb/databaseAccounts", + "Microsoft.AppService/gateways", + "Microsoft.MachineLearning/Workspaces", + "Microsoft.NotificationHubs/namespaces/notificationHubs", + "Microsoft.ApiManagement/service", + "Microsoft.Cdn/profiles", + "Microsoft.Search/searchServices", + "Microsoft.RecoveryServices/vaults", + "Microsoft.Network/expressRouteCircuits", + "Microsoft.CognitiveServices/accounts", + "Microsoft.DataLakeStore/accounts", + "Microsoft.ServiceFabric/clusters", + "microsoft.backup/BackupVault", + "Microsoft.HDInsight/clusters", + "Microsoft.PowerBI/workspaceCollections", + "Microsoft.Batch/batchAccounts", + "Microsoft.DataLakeAnalytics/accounts", + "Microsoft.ClassicCompute/domainNames", + "Microsoft.Web/connectionGateways", + "Microsoft.BotService/botServices", + "Microsoft.ContainerInstance/containerGroups", + "Microsoft.ContainerRegistry/registries", + "Microsoft.Databricks/workspaces", + "Microsoft.ContainerService/ManagedClusters" +] diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/ControlSettings.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/ControlSettings.json new file mode 100644 index 000000000..2df0645a6 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/ControlSettings.json @@ -0,0 +1,64 @@ +{ + "Diagnostics_RetentionPeriod_Min": 365, + "Diagnostics_RetentionPeriod_Forever": 0, + + "UniversalIPRange": "0.0.0.0-255.255.255.255", + "IPRangeStartIP": "0.0.0.0", + "IPRangeEndIP": "255.255.255.255", + + "MinGlobalAdmins": 3, + + "DefaultValidAttestationStates": [ "NotAnIssue", "WillFixLater", "WillNotFix" ], + "NewControlGracePeriodInDays": { + "Default": 60, + "ControlSeverity": { + "Critical": 7, + "High": 30, + "Medium": 60, + "Low": 90 + } + }, + "AttestationPeriodInDays": { + "Default": 90, + "ControlSeverity": { + "Critical": 7, + "High": 30, + "Medium": 60, + "Low": 90 + } + }, + + "ResultComplianceInDays": { + "DefaultControls": 3, + "OwnerAccessControls": 90 + }, + + "ControlSeverity": { + "Critical": "Critical", + "High": "High", + "Medium": "Medium", + "Low": "Low" + }, + + "Tenant":{ + "RecommendedMinGlobalAdmins": 3, + "RecommendedMaxDevicePerUserLimit": 20, + "SSPRMinAuthNMethodsRequired": 2 + }, + "User":{ + "TOD": 180 + }, + "Device":{ + "InactiveDeviceLimitInDays": 180 + }, + "Group":{ + "TOD": 180 + }, + "Application":{ + "TestDemoPoCNames": ["Test", "Demo", "PoC", "Pilot", "Temp"], + "TODO": 180 + }, + "ServicePrincipal":{ + "ApproachingExpiryThresholdInDays": 30 + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/ControlStats.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/ControlStats.json new file mode 100644 index 000000000..4ac9c1e73 Binary files /dev/null and b/src/AzSK.AAD/0.9.0/Framework/Configurations/SVT/ControlStats.json differ diff --git a/src/AzSK.AAD/0.9.0/Framework/Configurations/ServerConfigMetadata.json b/src/AzSK.AAD/0.9.0/Framework/Configurations/ServerConfigMetadata.json new file mode 100644 index 000000000..95edabf5b --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Configurations/ServerConfigMetadata.json @@ -0,0 +1,3 @@ +{ + "OnlinePolicyList": [] +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Core/PrivacyNotice.ps1 b/src/AzSK.AAD/0.9.0/Framework/Core/PrivacyNotice.ps1 new file mode 100644 index 000000000..d2aa55999 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Core/PrivacyNotice.ps1 @@ -0,0 +1,46 @@ +Set-StrictMode -Version Latest +class PrivacyNotice { + static [void] ValidatePrivacyAcceptance() + { + $appSettings = [ConfigurationManager]::GetLocalAzSKSettings(); + $source = "SDL" + + if(-not $appSettings.PrivacyNoticeAccepted) + { + $azskConfig = [ConfigurationManager]::GetAzSKConfigData(); + if(-not [string]::IsNullOrWhiteSpace($appSettings.OMSSource)) + { + $source = $appSettings.OMSSource; + } + if(($azskConfig.PrivacyAcceptedSources | Measure-Object).Count -gt 0 -and ($azskConfig.PrivacyAcceptedSources -contains $source)) + { + $appSettings.PrivacyNoticeAccepted = $true + $appSettings.UsageTelemetryLevel = "Anonymous" + [ConfigurationManager]::UpdateAzSKSettings($appSettings) + return; + } + Write-Host " `nAzSK: EULA and Privacy Disclosure: `nPlease review the following:`n`tEULA (http://aka.ms/azskeula)`n`tPrivacy Disclosure (http://aka.ms/azskpd)`n" -ForegroundColor Yellow; + $input = "" + while ($input -ne "y" -and $input -ne "n") { + if (-not [string]::IsNullOrEmpty($input)) { + Write-Host "Please select an appropriate option.`n" + } + $input = Read-Host "Enter 'Y' if you agree and 'N' if you don't (Y/N)" + $input = $input.Trim() + Write-Host "`n" + } + if ($input -eq "y") { + $appSettings.PrivacyNoticeAccepted = $true + $appSettings.UsageTelemetryLevel = "Anonymous" + } + if ($input -eq "n") { + $result = $false + $appSettings.PrivacyNoticeAccepted = $false + $appSettings.UsageTelemetryLevel = "None" + throw ([SuppressedException]::new(("We are sorry to see you go!"), [SuppressedExceptionType]::Generic)) + } + Write-Host "Your response has been recorded.`n" -ForegroundColor Green + [ConfigurationManager]::UpdateAzSKSettings($appSettings) + } + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.Application.ps1 b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.Application.ps1 new file mode 100644 index 000000000..94e170bf6 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.Application.ps1 @@ -0,0 +1,209 @@ +Set-StrictMode -Version Latest +class Application: SVTBase +{ + hidden [PSObject] $ResourceObject; + + Application([string] $tenantId, [SVTResource] $svtResource): Base($tenantId,$svtResource) + { + + $objId = $svtResource.ResourceId + $this.ResourceObject = Get-AzureADObjectByObjectId -ObjectIds $objId + } + + hidden [PSObject] GetResourceObject() + { + return $this.ResourceObject; + } + + hidden [ControlResult] CheckOldTestDemoApps([ControlResult] $controlResult) + { + $demoAppNames = $this.ControlSettings.Application.TestDemoPoCNames + $demoAppsRegex = [string]::Join('|', $demoAppNames) + + $app = $this.GetResourceObject() + $appName = $app.DisplayName + + if ($appName -eq $null -or -not ($appName -imatch $demoAppsRegex)) + { + $controlResult.AddMessage([VerificationResult]::Passed, + "No demo/test/pilot apps found."); + } + else + { + $controlResult.AddMessage([VerificationResult]::Verify, + "Found one or more demo/test apps. Review and cleanup.","(TODO) Review apps that are not in use."); + } + return $controlResult; + } + + hidden [ControlResult] CheckReturnURLsAreHTTPS([ControlResult] $controlResult) + { + $app = $this.GetResourceObject() + $ret = $false + if($app.replyURLs -eq $null -or $app.replyURLs.Count -eq 0) + { + $ret = $true + } + else + { + $nonHttpURLs = @() + foreach ($url in $app.replyURLs) + { + if ($url.tolower().startswith("http:")) + { + $nonHttpURLs += $url + } + } + + if ($nonHttpURLs.Count -eq 0) + { + $ret = $true + } + else + { + $controlResult.AddMessage("Found $($nonHttpURLs.Count) non-HTTPS URLs."); + } + } + + if ($ret -eq $true) + { + $controlResult.AddMessage([VerificationResult]::Passed, + "No non-HTTPS URLs in replyURLs."); + } + else + { + $controlResult.AddMessage([VerificationResult]::Failed, + "Found one or more non-HTTPS URLs in replyURLs.","(TODO) Please review and change them to HTTPS."); + } + + return $controlResult; + } + + hidden [ControlResult] CheckHomePageIsHTTPS([ControlResult] $controlResult) + { + $app = $this.GetResourceObject() + + if ((-not [String]::IsNullOrEmpty($app.HomePage)) -and $app.Homepage.ToLower().startswith('http:')) + { + $controlResult.AddMessage([VerificationResult]::Failed, + "Homepage url [$($app.HomePage)] for app [$($app.DisplayName)] is not HTTPS."); + } + <# elseif ([String]::IsNullOrEmpty($app.HomePage)) #TODO: Given API apps/functions etc. should we enforce this? + { + $controlResult.AddMessage([VerificationResult]::Verify, + "Homepage url not set for app: [$($app.DisplayName)]."); + } #> + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + "Homepage url for app [$($app.DisplayName)] is empty/HTTPS: [$($app.HomePage)]."); + } + + return $controlResult; + } + + hidden [ControlResult] CheckLogoutURLIsHTTPS([ControlResult] $controlResult) + { + $app = $this.GetResourceObject() + + if ((-not [String]::IsNullOrEmpty($app.LogoutUrl)) -and $app.LogoutURL.ToLower().startswith('http:')) + { + $controlResult.AddMessage([VerificationResult]::Failed, + "Logout url [$($app.LogoutUrl)] for app [$($app.DisplayName)] is not HTTPS."); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + "Logout url for app [$($app.DisplayName)] is empty/HTTPS: [$($app.LogoutUrl)]."); + } + + return $controlResult; + } + + hidden [ControlResult] CheckPrivacyDisclosure([ControlResult] $controlResult) + { + $app = $this.GetResourceObject() + + if ([String]::IsNullOrEmpty($app.InformationalUrls.Privacy) -or (-not ($app.InformationalUrls.Privacy -match [Constants]::RegExForValidURL))) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("App [$($app.DisplayName)] does not have a privacy disclosure URL set.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("App [$($app.DisplayName)] has a privacy disclosure URL: [$($app.InformationalUrls.Privacy)].")); + } + return $controlResult + } + + + hidden [ControlResult] CheckAppIsCurrentTenantOnly([ControlResult] $controlResult) + { + $app = $this.GetResourceObject() + + #Currently there are 2 places this might be set, AvailableToOtherTenants setting or SignInAudience = "AzureADMultipleOrgs" (latter is new) + if ( ($app.AvailableToOtherTenants -eq $true) -or + (-not [String]::IsNullOrEmpty($app.SignInAudience)) -and ($app.SignInAudience -ne "AzureADMyOrg")) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("The app [$($app.DisplayName)] is not limited to current enterprise tenant.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("App [$($app.DisplayName)] is limited to current enterprise tenant.")); + } + return $controlResult + } + + + hidden [ControlResult] CheckOrphanedApp([ControlResult] $controlResult) + { + $app = $this.GetResourceObject() + + $owners = [array] (Get-AzureADApplicationOwner -ObjectId $app.ObjectId) + if ($owners -eq $null -or $owners.Count -eq 0) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("App [$($app.DisplayName)] has no owner configured.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("App [$($app.DisplayName)] has an owner configured.")); + } + return $controlResult; + } + + hidden [ControlResult] CheckAppFTEOwner([ControlResult] $controlResult) + { + $app = $this.GetResourceObject() + + $owners = [array] (Get-AzureADApplicationOwner -ObjectId $app.ObjectId) + if ($owners -eq $null -or $owners.Count -eq 0) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("App [$($app.DisplayName)] has no owner configured.")); + } + elseif ($owners.Count -gt 0) + { + $bFTE = $false + $owners | % { + #If one of the users is non-Guest (== 'Member'), we are good. + if ($_.UserType -ne 'Guest') {$bFTE = $true} + } + if ($bFTE) + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("One or more owners of app [$($app.DisplayName)] are FTEs.")); + } + else { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("All owners of app: [$($app.DisplayName)] are 'Guest' users. At least one FTE owner should be added.")); + } + } + return $controlResult; + } + +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.Device.ps1 b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.Device.ps1 new file mode 100644 index 000000000..f82009718 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.Device.ps1 @@ -0,0 +1,38 @@ +Set-StrictMode -Version Latest +class Device: SVTBase +{ + hidden [PSObject] $ResourceObject; + #static [int] $InactiveDaysLimit = 180; #BUGBUG: statics ok? (in-session tenant change?) + Device([string] $tenantId, [SVTResource] $svtResource): Base($tenantId, $svtResource) + { + $objId = $svtResource.ResourceId + $this.ResourceObject = Get-AzureADDevice -ObjectId $objId + } + + hidden [PSObject] GetResourceObject() + { + return $this.ResourceObject; + } + + hidden [ControlResult] CheckStaleDevices([ControlResult] $controlResult) + { + $d = $this.GetResourceObject() + + $lastLoginDateTime = $d[0].ApproximateLastLogonTimeStamp + $inactiveDaysLimit = $this.ControlSettings.Device.InactiveDeviceLimitInDays; + $inactiveThreshold = ([DateTime]::Today).AddDays(-$inactiveDaysLimit) + if($lastLoginDateTime -lt $inactiveThreshold) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Device [$($d.DisplayName)] appears to be a stale entry. Last login was at: $lastLoginDateTime.`nConsider removing it from the directory.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Device appears to be active (not stale). Last login: $lastLoginDateTime")); + } + + return $controlResult; + + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.Group.ps1 b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.Group.ps1 new file mode 100644 index 000000000..05a5bae59 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.Group.ps1 @@ -0,0 +1,69 @@ +Set-StrictMode -Version Latest +class Group: SVTBase +{ + hidden [PSObject] $ResourceObject; + Group([string] $tenantId, [SVTResource] $svtResource): Base($tenantId, $svtResource) + { + $objId = $svtResource.ResourceId + $this.ResourceObject = Get-AzureADGroup -ObjectId $objId + } + + hidden [PSObject] GetResourceObject() + { + return $this.ResourceObject; + } + + hidden [ControlResult] CheckGroupsIsSecurityEnabled([ControlResult] $controlResult) + { + $g = $this.GetResourceObject() + + if($g.SecurityEnabled -eq $false) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Group object is not security enabled.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Group object is security enabled.")); + } + return $controlResult; + } + + hidden [ControlResult] CheckGroupHasNonGuestOwner([ControlResult] $controlResult) + { + $g = $this.GetResourceObject() + $go = [array] (Get-AzureADGroupOwner -ObjectId $g.ObjectId) + + #TODO: may need more logic (e.g., can Groups or SPNs be 'Group Owners'?) + $ret = $false + + if ($go -ne $null -and $go.Count -ne 0) + { + $go | % { + $o = $_ + if ($o.ObjectType -eq 'User' -and $o.UserType -ne 'Guest') + { + $ret = $true #Pass only if we find at least one non-Guest user + } + } + } + else + { + #Group has no owners...fail! + $ret = $false + } + + if ($ret -eq $true) + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Found at least one non-guest owner for group: $($g.DisplayName).")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Did not find at least one non-guest owner for group: $($g.DisplayName).")); + } + return $controlResult; + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.ServicePrincipal.ps1 b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.ServicePrincipal.ps1 new file mode 100644 index 000000000..1d0e0c89c --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.ServicePrincipal.ps1 @@ -0,0 +1,122 @@ +Set-StrictMode -Version Latest +class ServicePrincipal: SVTBase +{ + hidden [PSObject] $ResourceObject; + hidden [String] $SPNName; + ServicePrincipal([string] $tenantId, [SVTResource] $svtResource): Base($tenantId, $svtResource) + { + #$this.GetResourceObject(); + $objId = $svtResource.ResourceId + + $this.ResourceObject = Get-AzureADObjectByObjectId -ObjectIds $objId + $this.SPNName = $this.ResourceObject.DisplayName + + } + + hidden [PSObject] GetResourceObject() + { + return $this.ResourceObject; + } + + hidden [ControlResult] CheckSPNPasswordCredentials([ControlResult] $controlResult) + { + $spn = $this.GetResourceObject() + + if ($spn.PasswordCredentials.Count -gt 0) + { + $nPswd = $spn.PasswordCredentials.Count + + + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Found $nPswd password credentials on SPN: $($this.SPNName).")); + + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Did not find any password credentials on SPN.")); + } + return $controlResult; + } + + hidden [ControlResult] ReviewLegacySPN([ControlResult] $controlResult) + { + $spn = $this.GetResourceObject() + + if ($spn.ServicePrincipalType -eq 'Legacy') + { + $controlResult.AddMessage([VerificationResult]::Verify, + [MessageData]::new("Found an SPN of type 'Legacy'. Please review: $($this.SPNName)")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("SPN is not of type 'Legacy'.")); + } + return $controlResult; + } + + hidden [ControlResult] CheckCertNearingExpiry([ControlResult] $controlResult) + { + $spn = $this.GetResourceObject() + + $spk = [array] $spn.KeyCredentials + + if ($spk -eq $null -or $spk.Count -eq 0) + { + #No key creds, pass the control. + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("SPN [$($spn.DisplayName)] does not have a key credential configured. Passing control by default.")); + + } + else + { + $renew = @() + $expireDays = $this.ControlSettings.ServicePrincipal.ApproachingExpiryThresholdInDays; + $expiringSoon = ([DateTime]::Today).AddDays($expireDays) + $needToRenew = $false + $spk | % { + $k = $_ + if ($k.EndDate -le $expiringSoon) + { + $renew += $k.KeyId + $needToRenew = $true + } + } + + if ($needToRenew -eq $true) #found some key close to expiry + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("One or more keys of SPN [$($spn.DisplayName)] have expired or are nearing expiry (<$expireDays days).")); + + $renewList = $renew -join ", " + $controlResult.AddMessage([MessageData]::new("KeyIds nearing expiry:`n`t$renewList")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("None of the configured keys for SPN [$($spn.DisplayName)] are nearing expiry (<$expireDays days).")); + } + } + return $controlResult; + } + + <# + hidden [ControlResult] TBD([ControlResult] $controlResult) + { + $spn = $this.GetResourceObject() + + if ($spn.xyz) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Please review: $($this.SPNName)")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("PassMsg.")); + } + return $controlResult; + } + #> +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.Tenant.ps1 b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.Tenant.ps1 new file mode 100644 index 000000000..38de3435d --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.Tenant.ps1 @@ -0,0 +1,695 @@ +Set-StrictMode -Version Latest +class Tenant: SVTBase +{ + hidden [PSObject] $AADPermissions; + + hidden [PSObject] $CASettings; + hidden [PSObject] $AdminMFASettings; + hidden [PSObject] $B2BSettings; + hidden [PSObject] $MFASettings; + hidden [PSObject] $SSPRSettings; + hidden [PSObject] $EnterpriseAppSettings; + hidden [PSObject] $MFABypassList; + hidden [PSObject] $AuthNPasswordPolicySettings; + #static [int] $RecommendedMaxDevicePerUserLimit = 20; + hidden [PSObject] $DeviceSettings; + + hidden static [PSObject] $TenantDetails; + hidden static [bool] $isDirSyncEnabled = $false; + + Tenant([string] $tenantId, [SVTResource] $svtResource): Base($tenantId, $svtResource) + { + $this.GetAADSettings() + } + + hidden GetAADSettings() + { + $this.PublishCustomMessage("`r`nQuerying tenant API endpoints. This may take a few seconds...`r`nYou may see error messages in case you don't have access to all APIs."); + + if ([Tenant]::TenantDetails -eq $null) + { + [Tenant]::TenantDetails = Get-AzureADTenantDetail + [Tenant]::isDirSyncEnabled = [Tenant]::TenantDetails.DirSyncEnabled + } + + if ($this.AADPermissions -eq $null) + { + $this.AADPermissions = [WebRequestHelper]::InvokeAADAPI("/api/Permissions") + } + + if ($this.CASettings -eq $null) + { + $this.CASettings = [WebRequestHelper]::InvokeAADAPI("/api/PasswordReset/PasswordResetPolicies") + } + + if ($this.AdminMFASettings -eq $null) + { + $this.AdminMFASettings = [WebRequestHelper]::InvokeAADAPI("/api/BaselinePolicies/RequireMfaForAdmins") + } + + if ($this.MFASettings -eq $null) + { + $this.MFASettings = [WebRequestHelper]::InvokeAADAPI("/api/MultiFactorAuthentication/TenantModel") + } + + if ($this.B2BSettings -eq $null) + { + $this.B2BSettings = [WebRequestHelper]::InvokeAADAPI("/api/Directories/B2BDirectoryProperties") + } + + if ($this.DeviceSettings -eq $null) + { + $this.DeviceSettings = [WebRequestHelper]::InvokeAADAPI("/api/DeviceSetting") + } + + if ($this.MFABypassList -eq $null) + { + $this.MFABypassList = [WebRequestHelper]::InvokeAADAPI("/api/MultifactorAuthentication/BypassedUser") + } + + + if ($this.SSPRSettings -eq $null) + { + $this.SSPRSettings = [WebRequestHelper]::InvokeAADAPI("/api/PasswordReset/PasswordResetPolicies") + } + + if ($this.AuthNPasswordPolicySettings -eq $null) + { + $this.AuthNPasswordPolicySettings = [WebRequestHelper]::InvokeAADAPI("/api/AuthenticationMethods/PasswordPolicy") + } + + if ($this.EnterpriseAppSettings -eq $null) + { + $this.EnterpriseAppSettings = [WebRequestHelper]::InvokeAADAPI("/api/EnterpriseApplications/UserSettings") + } + + if ([Tenant]::TenantDetails -eq $null -or + $this.AADPermissions -eq $null -or + $this.CASettings -eq $null -or + $this.AdminMFASettings -eq $null -or + $this.MFASettings -eq $null -or + $this.B2BSettings -eq $null -or + $this.DeviceSettings -eq $null -or + $this.MFABypassList -eq $null -or + $this.SSPRSettings -eq $null -or + $this.AuthNPasswordPolicySettings -eq $null -or + $this.EnterpriseAppSettings -eq $null + ) + { + Write-Host -ForegroundColor Yellow "`nYou may not have sufficient permission to evaluate all controls.`nStatus for controls that could not be evaluated will show as 'Manual' in the report." + } + } + static [bool] IsDirectorySyncEnabled() + { + #We need to check this because it may not get set if tenant controls are not being scanned. + if ([Tenant]::TenantDetails -eq $null) + { + [Tenant]::TenantDetails = Get-AzureADTenantDetail + [Tenant]::isDirSyncEnabled = [Tenant]::TenantDetails.DirSyncEnabled + } + return [Tenant]::isDirSyncEnabled + } + + [ControlItem[]] ApplyServiceFilters([ControlItem[]] $controls) + { + if($controls.Count -eq 0) + { + return $controls; + } + + $result = $controls; + + $sspr = $this.CASettings + + #If we definitively determine that SSPR is not enabled for this tenant, exclude SSPR-specific controls + if ($sspr -ne $null -and $sspr.EnablementType -eq 0) + { + $result = $result | Where-Object {$_.Tags -notcontains "SSPR"} + } + + return $result; + } + + hidden [ControlResult] CheckTenantSecurityContactInfoIsSet([ControlResult] $controlResult) + { + $td = [Tenant]::TenantDetails + + $result = $false + $missing = "" + try { + #Check that at least 1 email and at least 1 phone number are set. + $bEmail = ($td.SecurityComplianceNotificationMails.Count -gt 0 -and -not [string]::IsNullOrEmpty($td.SecurityComplianceNotificationMails[0])) + $bPhone = ($td.SecurityComplianceNotificationPhones.Count -gt 0 -and -not [string]::IsNullOrEmpty($td.SecurityComplianceNotificationPhones[0])) + if ($bEmail -and $bPhone ) + { + $result = $true + } + else { + $missing = if (-not $bEmail) {"`n`tSecurityComplianceNotificationMails "} else {""} + $missing += if (-not $bPhone) {"`n`tSecurityComplianceNotificationPhone"} else {""} + } + } + catch { + $controlResult.AddMessage([VerificationResult]::Error, [MessageData]::new("Error reading Security Compliance Notification settings. Perhaps your AAD SKU does not support them.")); + } + + if ($result -eq $false) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Security compliance notification are not correctly set for the tenant.")); + $controlResult.AddMessage("The following are missing: $missing") + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Security compliance notification phone/email are both set as expected.")); + } + + return $controlResult; + } + + hidden [ControlResult] CheckCustomBannedPasswordConfig([ControlResult] $controlResult) + { + + $pps = $this.AuthNPasswordPolicySettings + + if ($pps -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + elseif($pps.enforceCustomBannedPasswords -eq $false -or ($pps.customBannedPasswords | Measure-Object).Count -eq 0) #Custom banned passwords not used? + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Custom banned passwords setting is disabled or banned password list is empty.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Custom banned passwords are enabled and list is correctly configured.")); + } + return $controlResult; + } + + hidden [ControlResult] CheckOnPremBannedPasswordsEnforced([ControlResult] $controlResult) + { + $pps = $this.AuthNPasswordPolicySettings + + if ($pps -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + elseif($pps.enableBannedPasswordCheckOnPremises -eq $false -or $pps.bannedPasswordCheckOnPremisesMode -ne 1) #Check on-prem enforcement of banned passwords + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Banned passwords check is not enabled for on-prem or mode is not set to 'Enforced'.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Banned passwords check is correctly enabled with mode set to 'Enforced'.")); + } + return $controlResult; + } + + hidden [ControlResult] CheckGuestsHaveLimitedAccess([ControlResult] $controlResult) + { + $b2b = $this.B2BSettings + + if ($b2b -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + elseif($b2b.restrictDirectoryAccess -ne $true) #Guests permissions are limited? + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Guest account directory permissions are not restricted.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Guest account permissions are restricted.")); + } + return $controlResult; + } + + hidden [ControlResult] CheckGuestsIfCanInvite([ControlResult] $controlResult) + { + $b2b = $this.B2BSettings + if ($b2b -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + elseif($b2b.limitedAccessCanAddExternalUsers -eq $true) #Guests can invite? + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Guest have privilege to invite other guests.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Guest do not have the privilege to invite other guests.")); + } + return $controlResult; + } + + hidden [ControlResult] CheckBaselineMFAPolicyForAdmins([ControlResult] $controlResult) + { + $adminSettings = $this.AdminMFASettings + if ($adminSettings -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + elseif($adminSettings.enable -eq $false -or $adminSettings.state -eq 0) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("MFA is set as 'not required' for admin accounts.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("MFA is set as 'required' for admin accounts.")); + } + return $controlResult; + } + + hidden [ControlResult] MFACheckUsersCanNotifyFraud([ControlResult] $controlResult) + { + $mfa = $this.MFASettings + if ($mfa -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + elseif($mfa.enableFraudAlert -eq $true) #Users can notify about fraud + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Users have the permission to raise fraud alerts.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Users do not have the permission to raise fraud alerts.")); + + } + return $controlResult; + } + + hidden [ControlResult] MFAReviewBypassedUsers([ControlResult] $controlResult) + { + $bp = $this.MFABypassList + if ($bp -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); #BUGBUG: Empty BP list case? + } + elseif($bp.Count -eq 0) #No users on bypass list + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("No users found on the MFA bypass list.")); + } + else + { + $bpUsers = @() + $bp | % {$bpUsers += $_.Username} + $bpUsersList = $bpUsers -join ", " + $controlResult.AddMessage([VerificationResult]::Verify, + [MessageData]::new("Found the following users on MFA bypass list. Please review.`n`t $bpUsersList" )); + + } + return $controlResult; + } + + + hidden [ControlResult] CheckUserPermissionsToCreateApps([ControlResult] $controlResult) + { + $aadPerms = $this.AADPermissions + if ($aadPerms -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + elseif ($aadPerms.allowedActions.application.Contains('create')) #has to match case + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Regular users have privilege to create new apps.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Regular users do not have privilege to create new apps.")); + } + + return $controlResult; + } + + hidden [ControlResult] CheckEnoughGlobalAdmins([ControlResult] $controlResult) + { + + $ca = Get-AzureAdDirectoryRole -Filter "DisplayName eq 'Company Administrator'" + $rm = @() + + try + { + $rm = @(Get-AzureADDirectoryRoleMember -ObjectId $ca.ObjectId) + } + catch + { + $rm = $null + } + + $recommendedMinGlobalAdmins = $this.ControlSettings.Tenant.RecommendedMinGlobalAdmins + if ($rm -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + elseif ($rm.Count -le $recommendedMinGlobalAdmins) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Only [$($rm.Count)] global administrator(s) found.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Found [$($rm.Count)] global administrators.")); + } + return $controlResult; + } + + hidden [ControlResult] CheckNoGuestsInGlobalAdminRole([ControlResult] $controlResult) + { + #TODO: Move this to common/.ctor similar to API calls. + #TODO: Expand this to other privileged roles (Security Admin, etc. - see AccountHelper) + #TODO: This and other RBAC checks should cover PIM-eligible members. + $ca = Get-AzureAdDirectoryRole -Filter "DisplayName eq 'Company Administrator'" + $rm = @() + + try + { + $rm = @(Get-AzureADDirectoryRoleMember -ObjectId $ca.ObjectId) + } + catch + { + $rm = $null + } + + if ($rm -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + else + { + $foundGuests = $false + $guests = @() + + $rm | % {if ($_.ObjectType -eq 'User' -and $_.UserType -eq 'Guest') {$foundGuests = $true; $guests += "$($_.DisplayName) ($($_.ObjectId))"}} + $guestList = $guests -join "`n`t" + if ($foundGuests) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Found the following 'Guest' users in Global Admin role: `n`t$guestList.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Did not find any 'Guest' member in Global Admin role.")); + } + } + return $controlResult; + } + + + hidden [ControlResult] CheckTenantDataAccessForApps([ControlResult] $controlResult) + { + $eas = $this.EnterpriseAppSettings + + if ($eas -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); #BUGBUG: Empty BP list case? + } + elseif($eas.usersCanAllowAppsToAccessData -eq $true) #Users can approve apps to access tenant data + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Users are permitted to approve app access to tenant data without admin consent.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Users are not permitted to approve app access to tenant data without admin consent." )); + + } + return $controlResult; + } + + + hidden [ControlResult] CheckUserPermissionToInviteGuests([ControlResult] $controlResult) + { + $aadPerms = $this.AADPermissions + + if ($aadPerms -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + elseif($aadPerms.allowedActions.user.Contains('inviteguest')) #has to match case + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Regular users have privilege to invite guests.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Regular users do not have privilege to invite guests.")); + } + return $controlResult; + } + + hidden [ControlResult] CheckMinQuestionsForSSPR([ControlResult] $controlResult) + { + $sspr = $this.CASettings + if ($sspr -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + elseif ($sspr.numberOfQuestionsToReset -lt 3) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Found that less than 3 questions are required for password reset.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Found that 3 or more questions are required for password reset.")); + } + return $controlResult; + } + + hidden [ControlResult] CheckUserNotificationUponSSPR([ControlResult] $controlResult) + { + $sspr = $this.CASettings + if ($sspr -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + elseif($sspr.notifyUsersOnPasswordReset -ne $true) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("User notification not configured for password resets.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("User notification is configured for password resets.")); + } + return $controlResult; + } + + hidden [ControlResult] CheckAdminNotificationUponSSPR([ControlResult] $controlResult) + { + $sspr = $this.CASettings + if ($sspr -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + elseif($sspr.notifyOnAdminPasswordReset -ne $true) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Notification to all admins not configured for admin password resets.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Notification to all admins is configured for admin password resets.")); + } + return $controlResult; + } + + + + hidden [ControlResult] SSPRMinAuthNMethodsRequired([ControlResult] $controlResult) + { + $sspr = $this.SSPRSettings + $minAuthNMethodsRequired = $this.ControlSettings.Tenant.SSPRMinAuthNMethodsRequired + if ($sspr -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); #BUGBUG: Empty BP list case? + } + elseif($sspr.numberOfAuthenticationMethodsRequired -ge $minAuthNMethodsRequired) + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("More than one authentication methods are required to reset password.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Ensure that at least two methods are required during a self-service password reset." )); + + } + return $controlResult; + } + + hidden [ControlResult] CheckRequireMFAForJoin([ControlResult] $controlResult) + { + $ds = $this.DeviceSettings + + if ($ds -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + else + { + if (-not $ds.requireMfaSetting) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Please enable MFA as a requirement for joining devices to the tenant.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("MFA is enabled as a requirement for joining new devices to the tenant.")); + } + } + return $controlResult; + } + + hidden [ControlResult] CheckMaxDeviceLimitSet([ControlResult] $controlResult) + { + $ds = $this.DeviceSettings + + if ($ds -eq $null) + { + $controlResult.AddMessage([VerificationResult]::Manual, + [MessageData]::new("Unable to evaluate control. You may not have sufficient permission")); + } + else + { + $recommendedMaxDevicePerUserLimit = $this.ControlSettings.Tenant.RecommendedMaxDevicePerUserLimit + if ($ds.maxDeviceNumberPerUserSetting -gt $recommendedMaxDevicePerUserLimit) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Max device per user limit is not set or too high. Recommended: ["+ $recommendedMaxDevicePerUserLimit + "].")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Max device per user limit is set at [$($ds.maxDeviceNumberPerUserSetting)].")); + } + } + return $controlResult; + } + + hidden [ControlResult] CheckPrivacyContactIsValid([ControlResult] $controlResult) + { + $td = [Tenant]::TenantDetails + + $ret = $false + $msg = "No privacy profile configured for tenant." + + if ( ($td.PrivacyProfile | Measure-Object).Count -ne 0 ) + { + if (-not [String]::IsNullOrEmpty($td.privacyProfile.contactEmail)) + { + $privacyContact = $td.privacyProfile.contactEmail + $pcUser = $null + try + { + $pcUser = Get-AzureAdUser -ObjectId $privacyContact + } + catch + { + #Filter-based searches do not 'throw', they just return $null if not found. + $pcUser = Get-AzureADUser -Filter "UserPrincipalName eq '$privacyContact'" + + if ($pcUser -eq $null) + { + $pcUser = Get-AzureADUser -Filter "Mail eq '$privacyContact'" + } + #TODO: worth another try? $pcUser = Get-AzureAdUser -SearchString ($privacyContact.Substring(0,$privacyContact.IndexOf('@'))) + } + + if ($pcUser -eq $null) + { + $msg ="Could not resolve privacy contact setting to an actual user: [$privacyContact]" + } + else + { + #PCM "Checking if User: $($pcUser.DisplayName) is member" + if (-not ($pcUser.AccountEnabled -and $pcUser.UserType -eq 'Member')) + { + $msg = "User [$($pcUser.DisplayName) ($privacyContact)] set as the privacy contact is either disabled or a non-member (i.e., guest) user." + } + else + { + $ret = $true #only success point in the method. + $msg = "Found valid (non-guest) privacy contact set as user: [$($pcUser.DisplayName) ($privacyContact)]." + } + } + } + } + + if ($ret -eq $false) + { + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new($msg)); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new($msg)); + } + return $controlResult; + } + + hidden [ControlResult] CheckPrivacyStatementIsValid([ControlResult] $controlResult) + { + $td = [Tenant]::TenantDetails + + if ( ($td.PrivacyProfile | Measure-Object).Count -eq 0 -or + [String]::IsNullOrEmpty($td.privacyProfile.statementUrl) -or + (-not ($td.privacyProfile.statementUrl -match [Constants]::RegExForValidURL)) + ) + { + + $controlResult.AddMessage([VerificationResult]::Failed, + [MessageData]::new("Privacy profile is incorrectly configured.")); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + [MessageData]::new("Privacy profile is correctly configured. Privacy Statement URL: [$($td.privacyProfile.statementUrl)]")); + } + return $controlResult; + } +}#class \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.User.ps1 b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.User.ps1 new file mode 100644 index 000000000..1b0705cd3 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AAD/AAD.User.ps1 @@ -0,0 +1,75 @@ +Set-StrictMode -Version Latest +class User: SVTBase +{ + hidden [PSObject] $ResourceObject; + + User([string] $tenantId, [SVTResource] $svtResource): Base($tenantId, $svtResource) + { + $objId = $svtResource.ResourceId + $this.ResourceObject = Get-AzureADUser -ObjectId $objId + } + + hidden [PSObject] GetResourceObject() + { + return $this.ResourceObject; + } + + hidden [ControlResult] CheckPasswordExpiration([ControlResult] $controlResult) + { + $u = $this.GetResourceObject(); + $pp = $u.PasswordPolicies + if($pp -ne $null -and $pp -match 'DisablePasswordExpiration' ) + { + $controlResult.AddMessage([VerificationResult]::Failed, + "User [$($u.DisplayName)] has 'password expiration' disabled. Please review!"); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + "User does not have password expiration disabled."); + } + return $controlResult; + } + + hidden [ControlResult] CheckStrongPassword([ControlResult] $controlResult) + { + $u = $this.GetResourceObject(); + $pp = $u.PasswordPolicies + if($pp -ne $null -and $pp -match 'DisableStrongPassword' ) + { + $controlResult.AddMessage([VerificationResult]::Failed, + "User [$($u.DisplayName)] has 'strong password' disabled. Please review!"); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + "User does not have 'strong password' disabled."); + } + return $controlResult; + } + + + hidden [ControlResult] CheckUserDirSyncSetting([ControlResult] $controlResult) + { + $u = $this.GetResourceObject(); + + #Flag users that were created 'cloud-only' if the tenant is enabled for dir-sync. + if ( [Tenant]::IsDirectorySyncEnabled() -and (-not $u.DirSyncEnabled -eq $true)) + { + $controlResult.AddMessage([VerificationResult]::Verify, + "User [$($u.DisplayName)] appears to be a 'cloud only' user although you have dir-sync enabled for the tenant. Please review!"); + } + elseif ( -not [Tenant]::IsDirectorySyncEnabled() -and ($u.DirSyncEnabled -eq $true)) + { + $controlResult.AddMessage([VerificationResult]::Verify, + "User [$($u.DisplayName)] has DirSync flag set to true even though dir-sync enabled is not enabled for the tenant. Please review!"); + } + else + { + $controlResult.AddMessage([VerificationResult]::Passed, + "User object dir-sync setting matches tenant settings ."); + } + + return $controlResult; + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AADResourceResolver.ps1 b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AADResourceResolver.ps1 new file mode 100644 index 000000000..f1b10b7e9 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/AADResourceResolver.ps1 @@ -0,0 +1,245 @@ +Set-StrictMode -Version Latest + +class AADResourceResolver: Resolver +{ + [SVTResource[]] $SVTResources = @(); + [string] $ResourcePath; + [string] $tenantId; + [int] $SVTResourcesFoundCount=0; + [bool] $scanTenant; + [int] $MaxObjectsToScan; + [string[]] $ObjectTypesToScan; + hidden static [string[]] $AllTypes = @("Application", "Device", "Group", "ServicePrincipal", "User"); + AADResourceResolver([string]$tenantId, [bool] $bScanTenant): Base($tenantId) + { + if ([string]::IsNullOrEmpty($tenantId)) + { + $this.tenantId = ([AccountHelper]::GetCurrentAADContext()).TenantId + } + else + { + $this.tenantId = $tenantId + } + $this.scanTenant = $bScanTenant + } + + [void] SetScanParameters([string[]] $objTypesToScan, $maxObj) + { + $this.MaxObjectsToScan = $maxObj + + if ($objTypesToScan.Contains("All")) + { + if ($objTypesToScan.Count -ne 1) + { + throw ([SuppressedException]::new("The objectType 'All' cannot be used in combination with other types.", [SuppressedExceptionType]::InvalidOperation)) + } + $this.ObjectTypesToScan = [AADResourceResolver]::AllTypes + } + elseif ($objTypesToScan.Contains("None")) + { + if ($objTypesToScan.Count -ne 1) + { + throw ([SuppressedException]::new("The objectType 'None' cannot be used in combination with other types.", [SuppressedExceptionType]::InvalidOperation)) + } + $this.ObjectTypesToScan = $objTypesToScan + } + else + { + $this.ObjectTypesToScan = $objTypesToScan + } + } + + [bool] NeedToScanType([string] $objType) + { + return $this.ObjectTypesToScan -contains $objType + } + + [void] LoadResourcesForScan() + { + $tenantInfoMsg = [AccountHelper]::GetCurrentTenantInfo(); + #Write-Host -ForegroundColor Green $tenantInfoMsg #TODO: Need to do with PublishCustomMessage...just before #-of-resources...etc.? + $this.PublishCustomMessage([Constants]::DoubleDashLine + "`r`n$tenantInfoMsg`r`n" + [Constants]::DoubleDashLine, [MessageType]::Update ) + + #TODO: TBD - for use later... + $bAdmin = [AccountHelper]::IsUserInAPermanentAdminRole(); + + #scanTenant is used to determine is the scan is tenant wide or just within the scope of the current (logged-in) user. + if ($this.scanTenant) + { + $svtResource = [SVTResource]::new(); + $svtResource.ResourceName = $this.tenantContext.TenantName; + $svtResource.ResourceType = "AAD.Tenant"; + $svtResource.ResourceId = $this.tenantId + $svtResource.ResourceTypeMapping = ([SVTMapping]::AADResourceMapping | + Where-Object { $_.ResourceType -eq $svtResource.ResourceType } | + Select-Object -First 1) + $this.SVTResources +=$svtResource + } + + $currUser = [AccountHelper]::GetCurrentSessionUserObjectId(); + + $userOwnedObjects = @() + + try { #BUGBUG: Investigate why this crashes in the Live tenant (even if user-created-objects exist...which should show up as 'user-owned' by default!) + $userOwnedObjects = [array] (Get-AzureADUserOwnedObject -ObjectId $currUser) + } + catch { #As a workaround, we take user-created objects, which seems to work (strange!) + $userCreatedObjects = [array] (Get-AzureADUserCreatedObject -ObjectId $currUser) + $userOwnedObjects = $userCreatedObjects + } + #TODO Explore delta between 'user-created' v. 'user-owned' for Apps/SPNs + + $maxObj = $this.MaxObjectsToScan + + if ($this.NeedToScanType("Application")) + { + $appObjects = @() + if ($this.scanTenant) + { + $appObjects = [array] (Get-AzureADApplication -Top $maxObj) + } + else { + $appObjects = [array] ($userOwnedObjects | ?{$_.ObjectType -eq 'Application'}) + } + + $appTypeMapping = ([SVTMapping]::AADResourceMapping | + Where-Object { $_.ResourceType -eq 'AAD.Application' } | + Select-Object -First 1) + + #TODO: Set to 3 for preview release. A user can use a larger value if they want via the 'MaxObj' cmdlet param. + $maxObj = $this.MaxObjectsToScan + + $nObj = $maxObj + foreach ($obj in $appObjects) { + $svtResource = [SVTResource]::new(); + $svtResource.ResourceName = $obj.DisplayName; + $svtResource.ResourceGroupName = "" #If blank, the column gets skipped in CSV file. + #TODO: If rgName == "" then all LOGs end up in root folder alongside CSV, README.txt. May need to have a reasonable 'mock' RGName. + $svtResource.ResourceType = "AAD.Application"; + $svtResource.ResourceId = $obj.ObjectId + $svtResource.ResourceTypeMapping = $appTypeMapping + $this.SVTResources +=$svtResource + if (--$nObj -eq 0) { break;} + } + } + + if ($this.NeedToScanType("ServicePrincipal")) + { + $spnObjects = @() + if ($this.scanTenant) + { + $spnObjects = [array] (Get-AzureADServicePrincipal -Top $maxObj) + } + else { + $spnObjects = [array] ($userOwnedObjects | ?{$_.ObjectType -eq 'ServicePrincipal'}) + } + + $spnTypeMapping = ([SVTMapping]::AADResourceMapping | + Where-Object { $_.ResourceType -eq 'AAD.ServicePrincipal' } | + Select-Object -First 1) + + $nObj = $maxObj + foreach ($obj in $spnObjects) { + $svtResource = [SVTResource]::new(); + $svtResource.ResourceName = $obj.DisplayName; + $svtResource.ResourceGroupName = "" #If blank, the column gets skipped in CSV file. + $svtResource.ResourceType = "AAD.ServicePrincipal"; + $svtResource.ResourceId = $obj.ObjectId + $svtResource.ResourceTypeMapping = $spnTypeMapping + $this.SVTResources +=$svtResource + if (--$nObj -eq 0) { break;} + } #TODO odd that above query does not show user created 'Group' objects. + } + + if ($this.NeedToScanType("Device")) + { + $deviceObjects = @() + if ($this.scanTenant) + { + $deviceObjects = [array] (Get-AzureADDevice -Top $maxObj) + } + else { + $DeviceObjects = [array] (Get-AzureADUserOwnedDevice -ObjectId $currUser) + } + + $deviceTypeMapping = ([SVTMapping]::AADResourceMapping | + Where-Object { $_.ResourceType -eq 'AAD.Device' } | + Select-Object -First 1) + + $nObj = $maxObj + foreach ($obj in $deviceObjects) { + $svtResource = [SVTResource]::new(); + $svtResource.ResourceName = $obj.DisplayName; + $svtResource.ResourceGroupName = "" #If blank, the column gets skipped in CSV file. + $svtResource.ResourceType = "AAD.Device"; + $svtResource.ResourceId = $obj.ObjectId + $svtResource.ResourceTypeMapping = $deviceTypeMapping + $this.SVTResources +=$svtResource + if (--$nObj -eq 0) { break;} + } #TODO odd that above query does not show user created 'Group' objects. + } + + + if ($this.NeedToScanType("User")) + { + + $userObjects = @() + if ($this.scanTenant) + { + $userObjects = [array] (Get-AzureADUser -Top $maxObj) + } + else { + $userObjects = [array] (Get-AzureADUser -ObjectId $currUser) + } + + $userTypeMapping = ([SVTMapping]::AADResourceMapping | + Where-Object { $_.ResourceType -eq 'AAD.User' } | + Select-Object -First 1) + + $nObj = $maxObj + foreach ($obj in $userObjects) { + $svtResource = [SVTResource]::new(); + $svtResource.ResourceName = $obj.DisplayName; + $svtResource.ResourceGroupName = "" #If blank, the column gets skipped in CSV file. + $svtResource.ResourceType = "AAD.User"; + $svtResource.ResourceId = $obj.ObjectId + $svtResource.ResourceTypeMapping = $userTypeMapping + $this.SVTResources +=$svtResource + if (--$nObj -eq 0) { break;} + } + } + + + if ($this.NeedToScanType("Group")) + { + + + $grpObjects = @() + if ($this.scanTenant) + { + $grpObjects = [array] (Get-AzureADGroup -Top $maxObj) + } + else { + $grpObjects = [array] ($userOwnedObjects | ?{$_.ObjectType -eq 'Group'}) + } + + $grpTypeMapping = ([SVTMapping]::AADResourceMapping | + Where-Object { $_.ResourceType -eq 'AAD.Group' } | + Select-Object -First 1) + + $nObj = $maxObj + foreach ($obj in $grpObjects) { + $svtResource = [SVTResource]::new(); + $svtResource.ResourceName = $obj.DisplayName; + $svtResource.ResourceGroupName = "" #If blank, the column gets skipped in CSV file. + $svtResource.ResourceType = "AAD.Group"; + $svtResource.ResourceId = $obj.ObjectId + $svtResource.ResourceTypeMapping = $grpTypeMapping + $this.SVTResources +=$svtResource + if (--$nObj -eq 0) { break;} + } #TODO Why does this not show user created 'Group' objects in live tenant? + } + + $this.SVTResourcesFoundCount = $this.SVTResources.Count + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Core/SVT/Resolver.ps1 b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/Resolver.ps1 new file mode 100644 index 000000000..2af714348 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/Resolver.ps1 @@ -0,0 +1,9 @@ + +Class Resolver : AzSKRoot { + + # Indicates to fetch all resource groups + Resolver([string] $tenantId):Base($tenantId) + { + + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Core/SVT/SVTControlAttestation.ps1 b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/SVTControlAttestation.ps1 new file mode 100644 index 000000000..6447a2d1b --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/SVTControlAttestation.ps1 @@ -0,0 +1,525 @@ +using namespace System.Management.Automation +Set-StrictMode -Version Latest +class SVTControlAttestation +{ + [SVTEventContext[]] $ControlResults = $null + hidden [bool] $dirtyCommitState = $false; + hidden [bool] $abortProcess = $false; + #hidden [ControlStateExtension] $controlStateExtension = $null; + hidden [AttestControls] $AttestControlsChoice; + hidden [bool] $bulkAttestMode = $false; + [AttestationOptions] $attestOptions; + hidden [PSObject] $ControlSettings ; + hidden [TenantContext] $TenantContext; + hidden [InvocationInfo] $InvocationContext; + + SVTControlAttestation([SVTEventContext[]] $ctrlResults, [AttestationOptions] $attestationOptions, [TenantContext] $TenantContext, [InvocationInfo] $invocationContext) + { + $this.TenantContext = $TenantContext; + $this.InvocationContext = $invocationContext; + $this.ControlResults = $ctrlResults; + $this.AttestControlsChoice = $attestationOptions.AttestControls; + $this.attestOptions = $attestationOptions; + # $this.controlStateExtension = [ControlStateExtension]::new($this.TenantContext, $this.InvocationContext) + # $this.controlStateExtension.UniqueRunId = $(Get-Date -format "yyyyMMdd_HHmmss"); + # $this.controlStateExtension.Initialize($true) + $this.ControlSettings=$ControlSettingsJson = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json"); + } + + [AttestationStatus] GetAttestationValue([string] $AttestationCode) + { + switch($AttestationCode.ToUpper()) + { + "1" { return [AttestationStatus]::NotAnIssue;} + "2" { return [AttestationStatus]::WillNotFix;} + "3" { return [AttestationStatus]::WillFixLater;} + "4" { return [AttestationStatus]::NotApplicable;} + "5" { return [AttestationStatus]::StateConfirmed;} + "9" { + $this.abortProcess = $true; + return [AttestationStatus]::None; + } + Default { return [AttestationStatus]::None;} + } + return [AttestationStatus]::None + } + + [ControlState] ComputeEffectiveControlState([ControlState] $controlState, [string] $ControlSeverity, [bool] $isSubscriptionControl, [SVTEventContext] $controlItem, [ControlResult] $controlResult) + { + Write-Host "$([Constants]::SingleDashLine)" -ForegroundColor Cyan + Write-Host "ControlId : $($controlState.ControlId)`nControlSeverity : $ControlSeverity`nDescription : $($controlItem.ControlItem.Description)`nCurrentControlStatus : $($controlState.ActualVerificationResult)`n" + if(-not $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess) + { + Write-Host "Skipping attestation process for this control. You do not have required permissions to evaluate this control." -ForegroundColor Yellow + if($controlItem.ControlItem.Tags.Contains("KeySecretPermissions")) + { + Write-Host "(Please note that you must have access permissions to the keys & secrets in the key vault for successful attestation of this control)" -ForegroundColor Yellow + } + Write-Host ([Constants]::CoAdminElevatePermissionMsg) -ForegroundColor Yellow + return $controlState; + } + if(-not $this.isControlAttestable($controlItem, $controlResult)) + { + Write-Host "This control cannot be attested by policy. Please follow the steps in 'Recommendation' for the control in order to fix the control and minimize exposure to attacks." -ForegroundColor Yellow + return $controlState; + } + $userChoice = "" + $isPrevAttested = $false; + if($controlResult.AttestationStatus -ne [AttestationStatus]::None) + { + $isPrevAttested = $true; + } + $tempCurrentStateObject = $null; + if($null -ne $controlResult.StateManagement -and $null -ne $controlResult.StateManagement.CurrentStateData) + { + $tempCurrentStateObject = $controlResult.StateManagement.CurrentStateData; + } + + #display the current state only if the state object is not empty + if($null -ne $tempCurrentStateObject -and $null -ne $tempCurrentStateObject.DataObject) + { + Write-Host "Configuration data to be attested:" -ForegroundColor Cyan + Write-Host "$([Helpers]::ConvertToPson($tempCurrentStateObject.DataObject))" + } + + if($isPrevAttested -and ($this.AttestControlsChoice -eq [AttestControls]::All -or $this.AttestControlsChoice -eq [AttestControls]::AlreadyAttested)) + { + #Compute the effective attestation status for support backward compatibility + $tempAttestationStatus = $controlState.AttestationStatus + #ToDo: Check in DB if 'NotFixed' exists; Capture Timestamp; Remove following if condition code + if($controlState.AttestationStatus -eq [AttestationStatus]::NotFixed) + { + $tempAttestationStatus = [AttestationStatus]::WillNotFix; + } + while($userChoice -ne '0' -and $userChoice -ne '1' -and $userChoice -ne '2' -and $userChoice -ne '9' ) + { + Write-Host "Existing attestation details:" -ForegroundColor Cyan + Write-Host "Attestation Status: $tempAttestationStatus`nVerificationResult: $($controlState.EffectiveVerificationResult)`nAttested By : $($controlState.State.AttestedBy)`nJustification : $($controlState.State.Justification)`n" + Write-Host "Please select an action from below: `n[0]: Skip`n[1]: Attest`n[2]: Clear Attestation" -ForegroundColor Cyan + $userChoice = Read-Host "User Choice" + if(-not [string]::IsNullOrWhiteSpace($userChoice)) + { + $userChoice = $userChoice.Trim(); + } + } + } + else + { + while($userChoice -ne '0' -and $userChoice -ne '1' -and $userChoice -ne '9' ) + { + Write-Host "Please select an action from below: `n[0]: Skip`n[1]: Attest" -ForegroundColor Cyan + $userChoice = Read-Host "User Choice" + if(-not [string]::IsNullOrWhiteSpace($userChoice)) + { + $userChoice = $userChoice.Trim(); + } + } + } + $Justification="" + $Attestationstate="" + $message = "" + [PSObject] $ValidAttestationStatesHashTable = $this.ComputeEligibleAttestationStates($controlItem, $controlResult); + [String[]]$ValidAttestationKey = @(0) + #Sort attestation status based on key value + if($null -ne $ValidAttestationStatesHashTable) + { + $ValidAttestationStatesHashTable | ForEach-Object { + $message += "`n[{0}]: {1}" -f $_.Value,$_.Name; + $ValidAttestationKey += $_.Value + } + } + switch ($userChoice.ToUpper()){ + "0" #None + { + + } + "1" #Attest + { + $attestationState = "" + while($attestationState -notin [String[]]($ValidAttestationKey) -and $attestationState -ne '9' ) + { + Write-Host "`nPlease select an attestation status from below: `n[0]: Skip$message" -ForegroundColor Cyan + $attestationState = Read-Host "User Choice" + $attestationState = $attestationState.Trim(); + } + $attestValue = $this.GetAttestationValue($attestationState); + if($attestValue -ne [AttestationStatus]::None) + { + $controlState.AttestationStatus = $attestValue; + } + elseif($this.abortProcess) + { + return $null; + } + elseif($attestValue -eq [AttestationStatus]::None) + { + return $controlState; + } + + if($controlState.AttestationStatus -ne [AttestationStatus]::None) + { + $Justification = "" + while([string]::IsNullOrWhiteSpace($Justification)) + { + $Justification = Read-Host "Justification" + try + { + $SanitizedJustification = [System.Text.UTF8Encoding]::ASCII.GetString([System.Text.UTF8Encoding]::ASCII.GetBytes($Justification)); + $Justification = $SanitizedJustification; + } + catch + { + # If the justification text is empty then prompting message again to provide justification text. + } + if([string]::IsNullOrWhiteSpace($Justification)) + { + Write-Host "`nEmpty space or blank justification is not allowed." + } + } + $this.dirtyCommitState = $true + } + $controlState.EffectiveVerificationResult = [Helpers]::EvaluateVerificationResult($controlState.ActualVerificationResult,$controlState.AttestationStatus); + + $controlState.State = $tempCurrentStateObject + + if($null -eq $controlState.State) + { + $controlState.State = [StateData]::new(); + } + + $controlState.State.AttestedBy = [AccountHelper]::GetCurrentSessionUser(); + $controlState.State.AttestedDate = [DateTime]::UtcNow; + $controlState.State.Justification = $Justification + + break; + } + "2" #Clear Attestation + { + $this.dirtyCommitState = $true + #Clears the control state. This overrides the previous attested controlstate. + $controlState.State = $null; + $controlState.EffectiveVerificationResult = $controlState.ActualVerificationResult + $controlState.AttestationStatus = [AttestationStatus]::None + } + "9" #Abort + { + $this.abortProcess = $true; + return $null; + } + Default + { + + } + } + + return $controlState; + } + + [ControlState] ComputeEffectiveControlStateInBulkMode([ControlState] $controlState, [string] $ControlSeverity, [bool] $isSubscriptionControl, [SVTEventContext] $controlItem, [ControlResult] $controlResult) + { + Write-Host "$([Constants]::SingleDashLine)" -ForegroundColor Cyan + Write-Host "ControlId : $($controlState.ControlId)`nControlSeverity : $ControlSeverity`nDescription : $($controlItem.ControlItem.Description)`nCurrentControlStatus : $($controlState.ActualVerificationResult)`n" + if(-not $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess) + { + Write-Host "Skipping attestation process for this control. You do not have required permissions to evaluate this control." -ForegroundColor Yellow + if($controlItem.ControlItem.Tags.Contains("KeySecretPermissions")) + { + Write-Host "(Please note that you must have access permissions to the keys & secrets in the key vault for successful attestation of this control)" -ForegroundColor Yellow + } + Write-Host ([Constants]::CoAdminElevatePermissionMsg) -ForegroundColor Yellow + return $controlState; + } + $userChoice = "" + if($null -ne $this.attestOptions -and $this.attestOptions.IsBulkClearModeOn) + { + if($controlState.AttestationStatus -ne [AttestationStatus]::None) + { + $this.dirtyCommitState = $true + #Compute the effective attestation status for support backward compatibility + $tempAttestationStatus = $controlState.AttestationStatus + if($controlState.AttestationStatus -eq [AttestationStatus]::NotFixed) + { + $tempAttestationStatus = [AttestationStatus]::WillNotFix; + } + Write-Host "Existing attestation details:" -ForegroundColor Cyan + Write-Host "Attestation Status: $tempAttestationStatus`nVerificationResult: $($controlState.EffectiveVerificationResult)`nAttested By : $($controlState.State.AttestedBy)`nJustification : $($controlState.State.Justification)`n" + } + #Clears the control state. This overrides the previous attested controlstate. + $controlState.State = $null; + $controlState.EffectiveVerificationResult = $controlState.ActualVerificationResult + $controlState.AttestationStatus = [AttestationStatus]::None + return $controlState; + } + $ValidAttestationStatesHashTable = $this.ComputeEligibleAttestationStates($controlItem, $controlResult); + #Checking if control is attestable + if($this.isControlAttestable($controlItem, $controlResult)) + { # Checking if the attestation state provided in command parameter is valid for the control + if( $this.attestOptions.AttestationStatus -in $ValidAttestationStatesHashTable.Name) + { + + $controlState.AttestationStatus = $this.attestOptions.AttestationStatus; + $controlState.EffectiveVerificationResult = [Helpers]::EvaluateVerificationResult($controlState.ActualVerificationResult,$controlState.AttestationStatus); + + if($null -ne $controlResult.StateManagement -and $null -ne $controlResult.StateManagement.CurrentStateData) + { + $controlState.State = $controlResult.StateManagement.CurrentStateData; + } + + if($null -eq $controlState.State) + { + $controlState.State = [StateData]::new(); + } + $this.dirtyCommitState = $true + $controlState.State.AttestedBy = [AccountHelper]::GetCurrentSessionUser(); + $controlState.State.AttestedDate = [DateTime]::UtcNow; + $controlState.State.Justification = $this.attestOptions.JustificationText + } + #if attestation state provided in command parameter is not valid for the control then print warning + else + { + $outvalidSet=$ValidAttestationStatesHashTable.Name -join "," ; + Write-Host "The chosen attestation state is not applicable to this control. Valid attestation choices are: $outvalidSet" -ForegroundColor Yellow; + return $controlState ; + } + } + #If control is not attestable then print warning + else + { + Write-Host "This control cannot be attested by policy. Please follow the steps in 'Recommendation' for the control in order to fix the control and minimize exposure to attacks." -ForegroundColor Yellow; + } + return $controlState; + } + + [void] StartControlAttestation() + { + + + try + { + #user provided justification text would be available only in bulk attestation mode. + if($null -ne $this.attestOptions -and (-not [string]::IsNullOrWhiteSpace($this.attestOptions.JustificationText) -or $this.attestOptions.IsBulkClearModeOn)) + { + $this.bulkAttestMode = $true; + Write-Host "$([Constants]::SingleDashLine)" -ForegroundColor Yellow + } + else + { + Write-Host ("$([Constants]::SingleDashLine)`nNote: Enter 9 during any stage to exit the attestation workflow. This will abort attestation process for the current resource and remaining resources.`n$([Constants]::SingleDashLine)") -ForegroundColor Yellow + } + + if($null -eq $this.ControlResults) + { + Write-Host "No control results found." -ForegroundColor Yellow + } + $this.abortProcess = $false; + #filtering the controls - Removing all the passed controls + #Step1 Group By IDs + + $filteredControlResults = @() + $filteredControlResults += $this.ControlResults | Group-Object { $_.GetUniqueId() } + + if((($filteredControlResults | Measure-Object).Count -eq 1 -and ($filteredControlResults[0].Group | Measure-Object).Count -gt 0 -and $null -ne $filteredControlResults[0].Group[0].ResourceContext) ` + -or ($filteredControlResults | Measure-Object).Count -gt 1) + { + Write-Host "No. of candidate resources for the attestation: $($filteredControlResults.Count)" -ForegroundColor Cyan + } + + #show warning if the keys count is greater than certain number. + $counter = 0 + #start iterating resource after resource + foreach($resource in $filteredControlResults) + { + $resourceValueKey = $resource.Name + $this.dirtyCommitState = $false; + $resourceValue = $resource.Group; + $isSubscriptionScan = $false; + $counter = $counter + 1 + if(($resourceValue | Measure-Object).Count -gt 0) + { + $tenantId = $resourceValue[0].TenantContext.tenantId + if($null -ne $resourceValue[0].ResourceContext) + { + $ResourceId = $resourceValue[0].ResourceContext.ResourceId + Write-Host $([String]::Format([Constants]::ModuleAttestStartHeading, $resourceValue[0].FeatureName, $resourceValue[0].ResourceContext.ResourceGroupName, $resourceValue[0].ResourceContext.ResourceName, $counter, $filteredControlResults.Count)) -ForegroundColor Cyan + } + else + { + $isSubscriptionScan = $true; + Write-Host $([String]::Format([Constants]::ModuleAttestStartHeadingSub, $resourceValue[0].FeatureName, $resourceValue[0].TenantContext.TenantName, $resourceValue[0].TenantContext.tenantId)) -ForegroundColor Cyan + } + + [ControlState[]] $resourceControlStates = @() + $count = 0; + [SVTEventContext[]] $filteredControlItems = @() + $resourceValue | ForEach-Object { + $controlItem = $_; + $matchedControlItem = $false; + if(($controlItem.ControlResults | Measure-Object).Count -gt 0) + { + [ControlResult[]] $matchedControlResults = @(); + $controlItem.ControlResults | ForEach-Object { + $controlResult = $_ + if($controlResult.ActualVerificationResult -ne [VerificationResult]::Passed) + { + if($this.AttestControlsChoice -eq [AttestControls]::All) + { + $matchedControlItem = $true; + $matchedControlResults += $controlResult; + $count++; + } + elseif($this.AttestControlsChoice -eq [AttestControls]::AlreadyAttested -and $controlResult.AttestationStatus -ne [AttestationStatus]::None) + { + $matchedControlItem = $true; + $matchedControlResults += $controlResult; + $count++; + } + elseif($this.AttestControlsChoice -eq [AttestControls]::NotAttested -and $controlResult.AttestationStatus -eq [AttestationStatus]::None) + { + $matchedControlItem = $true; + $matchedControlResults += $controlResult; + $count++; + } + } + } + } + if($matchedControlItem) + { + $controlItem.ControlResults = $matchedControlResults; + $filteredControlItems += $controlItem; + } + } + if($count -gt 0) + { + Write-Host "No. of controls that need to be attested: $count" -ForegroundColor Cyan + + foreach( $controlItem in $filteredControlItems) + { + $controlId = $controlItem.ControlItem.ControlID + $controlSeverity = $controlItem.ControlItem.ControlSeverity + $controlResult = $null; + $controlStatus = ""; + $isPrevAttested = $false; + if(($controlItem.ControlResults | Measure-Object).Count -gt 0) + { + foreach( $controlResult in $controlItem.ControlResults) + { + $controlStatus = $controlResult.ActualVerificationResult; + + [ControlState] $controlState = [ControlState]::new($controlId,$controlItem.ControlItem.Id,$controlResult.ChildResourceName,$controlStatus,"1.0"); + if($null -ne $controlResult.StateManagement -and $null -ne $controlResult.StateManagement.AttestedStateData) + { + $controlState.State = $controlResult.StateManagement.AttestedStateData + } + + $controlState.AttestationStatus = $controlResult.AttestationStatus + $controlState.EffectiveVerificationResult = $controlResult.VerificationResult + $controlState.HashId = [Helpers]::ComputeHash($resourceValueKey.ToLower()); + $controlState.ResourceId = $resourceValueKey; + if($this.bulkAttestMode) + { + $controlState = $this.ComputeEffectiveControlStateInBulkMode($controlState, $controlSeverity, $isSubscriptionScan, $controlItem, $controlResult) + } + else + { + $controlState = $this.ComputeEffectiveControlState($controlState, $controlSeverity, $isSubscriptionScan, $controlItem, $controlResult) + } + $resourceControlStates +=$controlState; + if($this.abortProcess) + { + Write-Host "Aborted the attestation workflow." -ForegroundColor Yellow + return; + } + } + } + Write-Host $([Constants]::SingleDashLine) -ForegroundColor Cyan + } + } + else + { + Write-Host "No attestable controls found.`n$([Constants]::SingleDashLine)" -ForegroundColor Yellow + } + + #remove the entries which doesn't have any state + #$resourceControlStates = $resourceControlStates | Where-Object {$_.State} + #persist the value back to state + if($this.dirtyCommitState) + { + if(($resourceControlStates | Measure-Object).Count -gt 0) + { + Write-Host "Attestation summary for this resource:" -ForegroundColor Cyan + $output = @() + $resourceControlStates | ForEach-Object { + $out = "" | Select-Object ControlId, EvaluatedResult, EffectiveResult, AttestationChoice + $out.ControlId = $_.ControlId + $out.EvaluatedResult = $_.ActualVerificationResult + $out.EffectiveResult = $_.EffectiveVerificationResult + $out.AttestationChoice = $_.AttestationStatus.ToString() + $output += $out + } + Write-Host ($output | Format-Table ControlId, EvaluatedResult, EffectiveResult, AttestationChoice | Out-String) -ForegroundColor Cyan + } + + Write-Host "Committing the attestation details for this resource..." -ForegroundColor Cyan + #$this.controlStateExtension.SetControlState($resourceValueKey, $resourceControlStates, $false) + Write-Host "Commit succeeded." -ForegroundColor Cyan + } + + if($null -ne $resourceValue[0].ResourceContext) + { + $ResourceId = $resourceValue[0].ResourceContext.ResourceId + Write-Host $([String]::Format([Constants]::CompletedAttestAnalysis, $resourceValue[0].FeatureName, $resourceValue[0].ResourceContext.ResourceGroupName, $resourceValue[0].ResourceContext.ResourceName)) -ForegroundColor Cyan + } + else + { + $isSubscriptionScan = $true; + Write-Host $([String]::Format([Constants]::CompletedAttestAnalysisSub, $resourceValue[0].FeatureName, $resourceValue[0].TenantContext.TenantName, $resourceValue[0].TenantContext.tenantId)) -ForegroundColor Cyan + } + } + + } + } + finally + { + #[Helpers]::CleanupLocalFolder([Constants]::AzSKAppFolderPath + "\Temp\$($this.controlStateExtension.UniqueRunId)"); + } + } + + [bool] isControlAttestable([SVTEventContext] $controlItem, [ControlResult] $controlResult) + { + # If None is found in array along with other attestation status, 'None' will get precedence. + if(($controlItem.ControlItem.ValidAttestationStates | Measure-Object).Count -gt 0 -and ($controlItem.ControlItem.ValidAttestationStates | Where-Object { $_.Trim() -eq [AttestationStatus]::None } | Measure-Object).Count -gt 0) + { + return $false + } + else + { + return $true + } + } + + [PSObject] ComputeEligibleAttestationStates([SVTEventContext] $controlItem, [ControlResult] $controlResult) + { + [System.Collections.ArrayList] $ValidAttestationStates = $null + #Default attestation state + if($null -ne $this.ControlSettings.DefaultValidAttestationStates){ + $ValidAttestationStates = $this.ControlSettings.DefaultValidAttestationStates | Select-Object -Unique + } + #Additional attestation state + if($null -ne $controlItem.ControlItem.ValidAttestationStates) + { + $ValidAttestationStates += $controlItem.ControlItem.ValidAttestationStates | Select-Object -Unique + } + $ValidAttestationStates = $ValidAttestationStates.Trim() | Select-Object -Unique + #if control not in grace, disable WillFixLater option + if(-not $controlResult.IsControlInGrace) + { + if(($ValidAttestationStates | Where-Object { $_ -eq [AttestationStatus]::WillFixLater} | Measure-Object).Count -gt 0) + { + $ValidAttestationStates.Remove("WillFixLater") + } + } + $ValidAttestationStatesHashTable = [Constants]::AttestationStatusHashMap.GetEnumerator() | Where-Object { $_.Name -in $ValidAttestationStates } | Sort-Object value + return $ValidAttestationStatesHashTable; + } + +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Core/SVT/SVTStatusReport.ps1 b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/SVTStatusReport.ps1 new file mode 100644 index 000000000..cef0a12ff --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/SVTStatusReport.ps1 @@ -0,0 +1,242 @@ +# Set-StrictMode -Version Latest +# class SVTStatusReport : SVTCommandBase +# { +# [SVTResourceResolver] $ServicesResolver = $null; + +# SVTStatusReport([string] $tenantId, [InvocationInfo] $invocationContext, [SVTResourceResolver] $resolver): +# Base($tenantId, $invocationContext) +# { +# if(-not $resolver) +# { +# throw [System.ArgumentException] ("The argument 'resolver' is null"); +# } + +# $this.ServicesResolver = $resolver; +# $this.ServicesResolver.LoadAzureResources(); +# } + +# hidden [SVTEventContext[]] RunAllControls() +# { +# [SVTEventContext[]] $result = @(); + +# # Run all Subscription security controls +# try +# { +# $this.PublishCustomMessage(" `r`n" + [Constants]::DoubleDashLine + "`r`nStarted Subscription security controls`r`n" + [Constants]::DoubleDashLine); +# $sscore = [SubscriptionSecurityStatus]::new($this.TenantContext.tenantId, $this.InvocationContext); +# if ($sscore) +# { +# # Just copy all the tags without validation. Validation will be done internally +# $sscore.FilterTags = $this.FilterTags; +# $sscore.ExcludeTags = $this.ExcludeTags; +# $sscore.ControlIdString = $this.ControlIdString; +# $sscore.ExcludeControlIdString = $this.ExcludeControlIdString; +# $sscore.GenerateFixScript = $this.GenerateFixScript; +# $sscore.AttestationOptions = $this.AttestationOptions; + +# $result += $sscore.RunAllControls(); +# $this.PublishCustomMessage([Constants]::DoubleDashLine + "`r`nCompleted Subscription security controls`r`n" + [Constants]::DoubleDashLine, [MessageType]::Update); +# } +# } +# catch +# { +# $this.CommandError($_); +# } + +# # Run all Azure services security controls +# try +# { +# $this.PublishCustomMessage(" `r`n" + [Constants]::DoubleDashLine + "`r`nStarted Azure services security controls`r`n" + [Constants]::DoubleDashLine); +# $secStatus = [ServicesSecurityStatus]::new($this.TenantContext.tenantId, $this.InvocationContext, $this.ServicesResolver); + +# if ($secStatus) +# { +# # Just copy all the tags without validation. Validation will be done internally +# $secStatus.FilterTags = $this.FilterTags; +# $secStatus.ExcludeTags = $this.ExcludeTags; +# $secStatus.ControlIdString = $this.ControlIdString; +# $secStatus.ExcludeControlIdString = $this.ExcludeControlIdString; +# $secStatus.GenerateFixScript = $this.GenerateFixScript; +# $secStatus.AttestationOptions = $this.AttestationOptions; + +# $result += $secStatus.RunAllControls(); +# $this.PublishCustomMessage([Constants]::DoubleDashLine + "`r`nCompleted Azure services security controls`r`n" + [Constants]::DoubleDashLine, [MessageType]::Update); +# } +# } +# catch +# { +# $this.CommandError($_); +# } + +# return $result; +# } + +# hidden [SVTEventContext[]] FetchAttestationInfo() +# { +# [SVTEventContext[]] $result = @(); + +# # Fetch state of all Subscription security controls +# try +# { +# $this.PublishCustomMessage(" `r`n" + [Constants]::DoubleDashLine + "`r`nGetting attestation info for Subscription level controls`r`n" + [Constants]::DoubleDashLine); +# $sscore = [SubscriptionSecurityStatus]::new($this.TenantContext.tenantId, $this.InvocationContext); +# if ($sscore) +# { +# # Just copy all the tags without validation. Validation will be done internally +# $sscore.ControlIdString = $this.ControlIdString; +# $sscore.AttestationOptions = $this.AttestationOptions; +# $result += $sscore.FetchAttestationInfo(); +# if(($result|Measure-object).count -gt 0) +# { +# $this.PublishCustomMessage([Constants]::DoubleDashLine + "`r`nCompleted Subscription level controls`r`n" + [Constants]::DoubleDashLine, [MessageType]::Update); +# } +# elseif([string]::IsNullOrWhiteSpace($sscore.ControlIdString)) +# { +# $this.PublishCustomMessage([Constants]::SingleDashLine + "`r`nNo attestation data found for Subscription level controls`r`n" + [Constants]::DoubleDashLine, [MessageType]::Update) +# } +# } +# } +# catch +# { +# $this.CommandError($_); +# } + +# # Fetch state of all Azure services security controls +# try +# { +# $this.PublishCustomMessage(" `r`n" + [Constants]::DoubleDashLine + "`r`nGetting attestation info for Azure services controls`r`n" + [Constants]::DoubleDashLine); +# $secStatus = [ServicesSecurityStatus]::new($this.TenantContext.tenantId, $this.InvocationContext, $this.ServicesResolver); + +# if ($secStatus) +# { +# # Just copy all the tags without validation. Validation will be done internally + +# $secStatus.ControlIdString = $this.ControlIdString; +# #$secStatus.GenerateFixScript = $this.GenerateFixScript; +# $secStatus.AttestationOptions = $this.AttestationOptions; +# $secStatusResult = $secStatus.FetchAttestationInfo() +# if(($secStatusResult|Measure-Object).Count -gt 0) +# { +# $result += $secStatusResult +# $this.PublishCustomMessage([Constants]::DoubleDashLine + "`r`nCompleted Azure services controls`r`n" + [Constants]::DoubleDashLine, [MessageType]::Update); +# } +# else +# { +# $this.PublishCustomMessage([Constants]::SingleDashLine + "`r`nNo attestation data found for Azure services controls`r`n" + [Constants]::DoubleDashLine, [MessageType]::Update) +# } +# } +# } +# catch +# { +# $this.CommandError($_); +# } +# #display summary +# if(($result|Measure-Object).Count -gt 0) +# { +# $this.DisplayAttetstationStatistics($result) +# } +# else +# { + +# } +# return $result; + +# } +# hidden [void] DisplayAttetstationStatistics([SVTEventContext[]] $Result) +# { +# $this.PublishCustomMessage("`r`n"+[Constants]::DoubleDashLine+"`r`nSummary of attestation details:`r`n`r`n"); +# $this.DisplayAttestationStatusWiseControlsCount($Result); +# $this.DisplaySeverityWiseControlsCount($Result); +# $this.DisplayControlIdWiseCount($Result) +# $this.DisplayExpiryDateWiseControlsCount($Result); +# } +# hidden [void] DisplayAttestationStatusWiseControlsCount([SVTEventContext[]] $Result) +# { +# $subCoreResult = $Result|Where-Object{!$_.IsResource()}; +# $resResult = $Result|Where-Object{$_.IsResource()}; +# if(($subCoreResult|Measure-Object).Count -gt 0) +# { +# $subCoreGroup = $subCoreResult.ControlResults|Group-Object ActualVerificationResult,AttestationStatus | ForEach{ +# [pscustomobject]@{ +# 'ActualVerificationResult'=$_.Group[0].ActualVerificationResult +# 'AttestationStatus'=$_.Group[0].AttestationStatus +# 'ControlsCount'=$_.count} +# } +# $this.PublishCustomMessage([Constants]::SingleDashLine+"`r`nSubscription controls:`r`n"+($subCoreGroup|out-string)) +# $this.PublishCustomMessage([Constants]::SingleDashLine) +# } +# if(($resResult|Measure-Object).Count -gt 0) +# { +# $resGroup = $resResult.ControlResults|Group-Object ActualVerificationResult,AttestationStatus | ForEach{ +# [pscustomobject]@{ +# 'ActualVerificationResult'=$_.Group[0].ActualVerificationResult +# 'AttestationStatus'=$_.Group[0].AttestationStatus +# 'ControlsCount'=$_.count} +# } +# $this.PublishCustomMessage("Azure Services controls:`r`n"+($resGroup|out-string)) +# $this.PublishCustomMessage([Constants]::DoubleDashLine) +# } + +# } +# hidden [void] DisplaySeverityWiseControlsCount([SVTEventContext[]] $Result) +# { +# $groupResult = $Result.ControlItem| Group ControlSeverity | ForEach{ +# [pscustomobject]@{ +# 'ControlSeverity'=$_.name +# 'ControlsCount'=$_.count} +# } +# $this.PublishCustomMessage("Distribution of attested controls by severity:`r`n"+($groupResult|out-string)) +# $this.PublishCustomMessage([Constants]::DoubleDashLine); +# } +# hidden [void] DisplayControlIdWiseCount([SVTEventContext[]] $Result) +# { +# $groupResult = $Result.ControlItem| Group ControlId | ForEach{ +# [pscustomobject]@{ +# 'ControlId'=$_.name +# 'ControlsCount'=$_.count} +# } +# $this.PublishCustomMessage("Distribution of controls that have been attested:`r`n"+($groupResult|out-string)); +# $this.PublishCustomMessage([Constants]::DoubleDashLine); + +# } +# hidden [void] DisplayExpiryDateWiseControlsCount([SVTEventContext[]] $Result) +# { +# $subCoreResult = $Result|Where-Object{!$_.IsResource()}; +# $resResult = $Result|Where-Object{$_.IsResource()}; +# $expiringSubControls = @() +# $expiringStateResources = @() +# if(($subCoreResult|Measure-Object).Count -gt 0) +# { +# $subControlsWithExpDate = $subCoreResult | Where-Object{ $_.ControlResults|Where-Object{![string]::IsNullOrWhiteSpace($_.StateManagement.AttestedStateData.ExpiryDate)}} +# $expiringSubControls= $subControlsWithExpDate | Where-Object{ $_.ControlResults | Where-Object{([datetime]$_.StateManagement.AttestedStateData.ExpiryDate - $(Get-Date).ToUniversalTime()).TotalDays -le 30}} +# } +# if(($resResult|Measure-Object).Count -gt 0) +# { +# $resourcesWithExpDate = $resResult | Where-Object{ $_.ControlResults|Where-Object{![string]::IsNullOrWhiteSpace($_.StateManagement.AttestedStateData.ExpiryDate)}} +# $expiringStateResources = $resourcesWithExpDate | Where-Object{ $_.ControlResults | Where-Object{([datetime]$_.StateManagement.AttestedStateData.ExpiryDate - $(Get-Date).ToUniversalTime()).TotalDays -le 30}} +# } +# if(($expiringSubControls|Measure-Object).Count -gt 0 -or ($expiringStateResources|Measure-Object).Count -gt 0) +# { +# $expiringSubControls15Days= $expiringSubControls | Where-Object{ $_.ControlResults | Where-Object{([datetime]$_.StateManagement.AttestedStateData.ExpiryDate - $(Get-Date).ToUniversalTime()).TotalDays -le 15}} +# $expiringStateResources15Days = $expiringStateResources | Where-Object{ $_.ControlResults | Where-Object{([datetime]$_.StateManagement.AttestedStateData.ExpiryDate - $(Get-Date).ToUniversalTime()).TotalDays -le 15}} +# $expiringSubControls7Days= $expiringSubControls | Where-Object{ $_.ControlResults | Where-Object{([datetime]$_.StateManagement.AttestedStateData.ExpiryDate - $(Get-Date).ToUniversalTime()).TotalDays -le 7}} +# $expiringStateResources7Days = $expiringStateResources | Where-Object{ $_.ControlResults | Where-Object{([datetime]$_.StateManagement.AttestedStateData.ExpiryDate - $(Get-Date).ToUniversalTime()).TotalDays -le 7}} + +# $this.PublishCustomMessage("Summary of controls expiring in near future:`r`n`r`nDays CountOfSubscriptionControls CountOfAzureServicesControls`r`n"+[Constants]::SingleDashLine); +# if(($expiringSubControls7Days|Measure-Object).Count -gt 0 -or ($expiringStateResources7Days|Measure-Object).Count -gt 0) +# { +# $this.PublishCustomMessage("07`t`t$(($expiringSubControls7Days|Measure-Object).Count)`t`t`t`t`t`t`t$(($expiringStateResources7Days|Measure-Object).Count)"); +# } +# if(($expiringSubControls15Days|Measure-Object).Count -gt 0 -or ($expiringStateResources15Days|Measure-Object).Count -gt 0) +# { +# $this.PublishCustomMessage("15`t`t$(($expiringSubControls15Days|Measure-Object).Count)`t`t`t`t`t`t`t$(($expiringStateResources15Days|Measure-Object).Count)`t`t`t`t"); +# } +# $this.PublishCustomMessage("30`t`t$(($expiringSubControls|Measure-Object).Count)`t`t`t`t`t`t`t$(($expiringStateResources|Measure-Object).Count)`t`t`t`t`r`n`r`n"); +# $this.PublishCustomMessage("Recommendation: Check Attestation report to get details of expiring controls and fix/attest them before expiry.",[MessageType]::Warning); +# } +# else +# { +# $this.PublishCustomMessage([Constants]::SingleDashLine+"`r`n`r`nCount of Controls expiring in the next 30 days: 0`r`n"); +# } +# } +# } diff --git a/src/AzSK.AAD/0.9.0/Framework/Core/SVT/ServicesSecurityStatus.ps1 b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/ServicesSecurityStatus.ps1 new file mode 100644 index 000000000..13ae3797b --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Core/SVT/ServicesSecurityStatus.ps1 @@ -0,0 +1,307 @@ +Set-StrictMode -Version Latest +class ServicesSecurityStatus: SVTCommandBase +{ + [Resolver] $Resolver = $null; + [bool] $IsPartialCommitScanActive = $false; + [bool] $IsPrivilegedUser = $false; + + ServicesSecurityStatus([string] $tenantId, [InvocationInfo] $invocationContext, [Resolver] $resolver): + Base($tenantId, $invocationContext) + { + if(-not $resolver) + { + throw [System.ArgumentException] ("The argument 'resolver' is null"); + } + + $this.Resolver = $resolver; + $this.Resolver.LoadResourcesForScan(); + + #BaseLineControlFilter with control ids + $this.UsePartialCommits =$invocationContext.BoundParameters["UsePartialCommits"]; + $this.UseBaselineControls = $invocationContext.BoundParameters["UseBaselineControls"]; + $this.CentralStorageAccount = $invocationContext.BoundParameters["CentralStorageAccount"]; + #[PartialScanManager]::ClearInstance(); + #$this.BaselineFilterCheck(); + #$this.UsePartialCommitsCheck(); + } + + [SVTEventContext[]] ComputeApplicableControls() + { + #[PartialScanManager] $partialScanMngr = [PartialScanManager]::GetInstance(); + # if a scan is active - don't update the control inventory + [SVTEventContext[]] $result = @(); + if($this.IsPartialCommitScanActive) + { + return $result; + } + $automatedResources = @(); + $automatedResources += ($this.Resolver.SVTResources | Where-Object { $_.ResourceTypeMapping }); + try + { + foreach($resource in $automatedResources) { + try + { + $svtClassName = $resource.ResourceTypeMapping.ClassName; + $svtObject = $null; + try + { + $svtObject = New-Object -TypeName $svtClassName -ArgumentList $this.TenantContext.tenantId, $resource + } + catch + { + $this.CommandError($_.Exception.InnerException.ErrorRecord); + } + if($svtObject) + { + $this.SetSVTBaseProperties($svtObject); + $result += $svtObject.ComputeApplicableControlsWithContext(); + } + } + catch + { + $this.CommandError($_); + } + #[ListenerHelper]::RegisterListeners(); + + } + $svtClassName = [SVTMapping]::SubscriptionMapping.ClassName; + $svtObject = $null; + try + { + $svtObject = New-Object -TypeName $svtClassName -ArgumentList $this.TenantContext.tenantId + } + catch + { + $this.CommandError($_.Exception.InnerException.ErrorRecord); + } + if($svtObject) + { + $this.SetSVTBaseProperties($svtObject); + $result += $svtObject.ComputeApplicableControlsWithContext(); + } + + } + catch + { + $this.CommandError($_); + } + $this.PublishEvent([SVTEvent]::WriteInventory, $result); + + if ($null -ne $result) + { + [RemoteApiHelper]::PostApplicableControlSet($result); + $this.PublishCustomMessage("Completed sending control inventory."); + } + else { + $this.PublishCustomMessage("There is an active scan going on. Please try later."); + } + return $result; + } + + hidden [SVTEventContext[]] RunForAllResources([string] $methodNameToCall, [bool] $runNonAutomated, [PSObject] $resourcesList) + { + if ([string]::IsNullOrWhiteSpace($methodNameToCall)) + { + throw [System.ArgumentException] ("The argument 'methodNameToCall' is null. Pass the reference of method to call. e.g.: [YourClass]::new().YourMethod"); + } + + [SVTEventContext[]] $result = @(); + + if(($resourcesList | Measure-Object).Count -eq 0) + { + $this.PublishCustomMessage("No security controls/resources match the input criteria specified. `nPlease rerun the command using a different set of criteria."); + #TODO: why not: $this.WriteMessage("No security controls/resources match the input criteria specified. `nPlease rerun the command using a different set of criteria.",[MessageType]::Warning) + return $result; + } + $this.PublishCustomMessage("Number of resources: $($this.resolver.SVTResourcesFoundCount)"); + $automatedResources = @(); + + $automatedResources += ($resourcesList | Where-Object { $_.ResourceTypeMapping }); + + # Resources skipped from scan using excludeResourceName or -ExcludeResourceGroupNames parameters + if([Helpers]::CheckMember($this.resolver,"ExcludedResourceGroupNames") -or [Helpers]::CheckMember($this.resolver,"ExcludedResources")) + { + $ExcludedResourceGroups=$this.resolver.ExcludedResourceGroupNames + $ExcludedResources=$this.resolver.ExcludedResources ; + if(($this.resolver.ExcludeResourceGroupNames| Measure-Object).Count -gt 0 -or ($this.resolver.ExcludeResourceNames| Measure-Object).Count -gt 0) + { + $this.PublishCustomMessage("One or more resources/resource groups will be excluded from the scan based on exclude flags.") + if(-not [string]::IsNullOrEmpty($this.resolver.ExcludeResourceGroupWarningMessage)) + { + $this.PublishCustomMessage("$($this.resolver.ExcludeResourceGroupWarningMessage)",[MessageType]::Warning) + + } + if(-not [string]::IsNullOrEmpty($this.resolver.ExcludeResourceWarningMessage)) + { + $this.PublishCustomMessage("$($this.resolver.ExcludeResourceWarningMessage)",[MessageType]::Warning) + } + $this.PublishCustomMessage("Summary of exclusions: "); + if(($this.resolver.ExcludeResourceGroupNames| Measure-Object).Count -gt 0) + { + $this.PublishCustomMessage(" Resource groups excluded: $(($ExcludedResourceGroups | Measure-Object).Count)", [MessageType]::Info); + } + $this.PublishCustomMessage(" Resources excluded: $(($ExcludedResources | Measure-Object).Count)(includes RGs,resourcetypenames and explicit exclusions).", [MessageType]::Info); + $this.PublishCustomMessage("For a detailed list of excluded resources, see 'ExcludedResources-$($this.RunIdentifier).txt' in the output log folder.") + $this.ReportExcludedResources($this.resolver); + } + } + if($runNonAutomated) + { + $this.ReportNonAutomatedResources(); + } + + $this.PublishCustomMessage("`nNumber of resources for which security controls will be evaluated: $($automatedResources.Count)",[MessageType]::Info); + $totalResources = $automatedResources.Count; + [int] $currentCount = 0; + $automatedResources | ForEach-Object { + $exceptionMessage = "Exception for resource: [ResourceType: $($_.ResourceTypeMapping.ResourceTypeName)] [ResourceGroupName: $($_.ResourceGroupName)] [ResourceName: $($_.ResourceName)]" + try + { + $currentCount += 1; + if($totalResources -gt 1) + { + $this.PublishCustomMessage(" `r`nChecking resource [$currentCount/$totalResources] "); + } + #Update resource scan retry count in scan snapshot in storage + #$this.UpdateRetryCountForPartialScan(); + $svtClassName = $_.ResourceTypeMapping.ClassName; + + $svtObject = $null; + + try + { + $extensionSVTClassName = $svtClassName + "Ext"; + $extensionSVTClassFilePath = [ConfigurationManager]::LoadExtensionFile($svtClassName); + if([string]::IsNullOrWhiteSpace($extensionSVTClassFilePath)) + { + $svtObject = New-Object -TypeName $svtClassName -ArgumentList $this.TenantContext.tenantId, $_ + } + else { + # file has to be loaded here due to scope contraint + . $extensionSVTClassFilePath + $svtObject = New-Object -TypeName $extensionSVTClassName -ArgumentList $this.TenantContext.tenantId, $_ + } + } + catch + { + $this.PublishCustomMessage($exceptionMessage); + # Unwrapping the first layer of exception which is added by New-Object function + $this.CommandError($_.Exception.InnerException.ErrorRecord); + } + + [SVTEventContext[]] $currentResourceResults = @(); + if($svtObject) + { + $svtObject.RunningLatestPSModule = $this.RunningLatestPSModule + $this.SetSVTBaseProperties($svtObject); + $currentResourceResults += $svtObject.$methodNameToCall(); + $svtObject.ChildSvtObjects | ForEach-Object { + $_.RunningLatestPSModule = $this.RunningLatestPSModule + $this.SetSVTBaseProperties($_) + $currentResourceResults += $_.$methodNameToCall(); + } + $result += $currentResourceResults; + + } + # if(($result | Measure-Object).Count -gt 0) + # { + # if($currentCount % 5 -eq 0 -or $currentCount -eq $totalResources) + # { + # $this.UpdatePartialCommitBlob() + # } + # } + + # Register/Deregister all listeners to cleanup the memory + #BUGBUG TODO [ListenerHelper]::RegisterListeners(); + } + catch + { + $this.PublishCustomMessage($exceptionMessage); + $this.CommandError($_); + } + } + + + return $result; + } + + hidden [SVTEventContext[]] RunAllControls() + { + return $this.RunForAllResources("EvaluateAllControls",$true,$this.Resolver.SVTResources) + } + # hidden [SVTEventContext[]] FetchAttestationInfo() + # { + # [ControlStateExtension] $ControlStateExt = [ControlStateExtension]::new($this.TenantContext, $this.InvocationContext); + # $ControlStateExt.UniqueRunId = $(Get-Date -format "yyyyMMdd_HHmmss"); + # $ControlStateExt.Initialize($false); + # $attestationFound = $ControlStateExt.ComputeControlStateIndexer(); + # $attestedResources = @() + # if(($null -ne $ControlStateExt.ControlStateIndexer) -and ([Helpers]::CheckMember($ControlStateExt.ControlStateIndexer, "ResourceId"))) + # { + # $attestedResources = $this.Resolver.SVTResources | Where-Object {$ControlStateExt.ControlStateIndexer.ResourceId -contains $_.ResourceId} + # } + # return $this.RunForAllResources("FetchStateOfAllControls",$false,$attestedResources) + # } + + hidden [void] ReportNonAutomatedResources() + { + $nonAutomatedResources = @(); + $nonAutomatedResources += ($this.Resolver.SVTResources | Where-Object { $null -eq $_.ResourceTypeMapping }); + + if(($nonAutomatedResources|Measure-Object).Count -gt 0) + { + $this.PublishCustomMessage("Number of resources for which security controls will NOT be evaluated: $($nonAutomatedResources.Count)", [MessageType]::Warning); + + $nonAutomatedResTypes = [array] ($nonAutomatedResources | Select-Object -Property ResourceType -Unique); + $this.PublishCustomMessage([MessageData]::new("Security controls are yet to be automated for the following service types: ", $nonAutomatedResTypes)); + + $this.PublishAzSKRootEvent([AzSKRootEvent]::UnsupportedResources, $nonAutomatedResources); + } + } + + + #BaseLineControlFilter Function + # [void] BaselineFilterCheck() + # { + # #Load ControlSetting Resource Types and Filter resources + # $scanSource = [AzSKSettings]::GetInstance().GetScanSource(); + # #Load ControlSetting Resource Types and Filter resources + # if($this.CentralStorageAccount){ + # [PartialScanManager] $partialScanMngr = [PartialScanManager]::GetInstance($this.CentralStorageAccount, $this.TenantContext.tenantId); + # } + # else{ + # [PartialScanManager] $partialScanMngr = [PartialScanManager]::GetInstance(); + # } + # $baselineControlsDetails = $partialScanMngr.GetBaselineControlDetails() + # #If Scan source is in supported sources or baselineControls switch is available + # if ($null -ne $baselineControlsDetails -and ($baselineControlsDetails.ResourceTypeControlIdMappingList | Measure-Object).Count -gt 0 -and ($baselineControlsDetails.SupportedSources -contains $scanSource -or $this.UseBaselineControls)) + # { + # #Get resource type and control ids mapping from controlsetting object + # #$this.PublishCustomMessage("Running cmdlet with baseline resource types and controls.", [MessageType]::Warning); + # $baselineResourceTypes = $baselineControlsDetails.ResourceTypeControlIdMappingList | Select-Object ResourceType | Foreach-Object {$_.ResourceType} + # #Filter SVT resources based on baseline resource types + # $ResourcesWithBaselineFilter =$this.Resolver.SVTResources | Where-Object {$null -ne $_.ResourceTypeMapping -and $_.ResourceTypeMapping.ResourceTypeName -in $baselineResourceTypes } + + # #Get the list of control ids + # $controlIds = $baselineControlsDetails.ResourceTypeControlIdMappingList | Select-Object ControlIds | ForEach-Object { $_.ControlIds } + # $BaselineControlIds = [system.String]::Join(",",$controlIds); + # if(-not [system.String]::IsNullOrEmpty($BaselineControlIds)) + # { + # $this.ControlIds = $controlIds; + + # } + # $this.Resolver.SVTResources = $ResourcesWithBaselineFilter + # } + + # } + + [void] ReportExcludedResources($SVTResolver) + { + $excludedObj=New-Object -TypeName PSObject; + $excludedObj | Add-Member -NotePropertyName ExcludedResourceGroupNames -NotePropertyValue $SVTResolver.ExcludedResourceGroupNames + $excludedObj | Add-Member -NotePropertyName ExcludedResources -NotePropertyValue $SVTResolver.ExcludedResources + $excludedObj | Add-Member -NotePropertyName ExcludedResourceType -NotePropertyValue $SVTResolver.ExcludeResourceTypeName + $excludedObj | Add-Member -NotePropertyName ExcludeResourceNames -NotePropertyValue $SVTResolver.ExcludeResourceNames + $this.PublishAzSKRootEvent([AzSKRootEvent]::WriteExcludedResources,$excludedObj); + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Framework.ps1 b/src/AzSK.AAD/0.9.0/Framework/Framework.ps1 new file mode 100644 index 000000000..2105c5803 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Framework.ps1 @@ -0,0 +1,119 @@ +Set-StrictMode -Version Latest + +$libraryPath = (Get-Item $PSScriptRoot).Parent.FullName+ "\Lib"; +Add-Type -Path "$libraryPath\Microsoft.IdentityModel.Clients.ActiveDirectory.dll" +. $PSScriptRoot\Models\Enums.ps1 + +#Constants +. $PSScriptRoot\Helpers\Constants.ps1 +. $PSScriptRoot\Helpers\OldConstants.ps1 + + +#Models +. $PSScriptRoot\Models\AzSKGenericEvent.ps1 +. $PSScriptRoot\Models\CommandDetails.ps1 +. $PSScriptRoot\Models\Exception\SuppressedException.ps1 +. $PSScriptRoot\Models\RemoteReports\CsvOutputModel.ps1 +. $PSScriptRoot\Models\FeatureFlight.ps1 +. $PSScriptRoot\Helpers\CommandHelper.ps1 +. $PSScriptRoot\Abstracts\EventBase.ps1 +. $PSScriptRoot\Helpers\Helpers.ps1 + +#Helpers (independent of models) +. $PSScriptRoot\Helpers\AccountHelper.ps1 +. $PSScriptRoot\Helpers\ConfigurationHelper.ps1 + +. $PSScriptRoot\Models\AzSKConfig.ps1 +. $PSScriptRoot\Models\AzSKEvent.ps1 +. $PSScriptRoot\Models\AzSKSettings.ps1 + +. $PSScriptRoot\Models\SVT\SVTConfig.ps1 +. $PSScriptRoot\Models\SVT\SVTEvent.ps1 +. $PSScriptRoot\Models\SVT\SVTResource.ps1 +. $PSScriptRoot\Models\SVT\AttestationOptions.ps1 +. $PSScriptRoot\Models\SVT\PartialScanResourceMap.ps1 +. $PSScriptRoot\Models\RemoteReports\LSRScanResultModel.ps1 +. $PSScriptRoot\Models\RemoteReports\ComplianceStateModel.ps1 +. $PSScriptRoot\Models\SubscriptionCore\AzureSecurityCenter.ps1 +. $PSScriptRoot\Models\SubscriptionCore\ManagementCertificate.ps1 +. $PSScriptRoot\Models\SubscriptionSecurity\SubscriptionRBAC.ps1 +. $PSScriptRoot\Models\ContinuousAssurance\AutomationAccount.ps1 +. $PSScriptRoot\Models\ControlState.ps1 +. $PSScriptRoot\Models\FixControl\FixControlModel.ps1 +. $PSScriptRoot\Models\RemoteReports\RecommendationReportModel.ps1 +. $PSScriptRoot\Models\RemoteReports\ScanResultModels.ps1 + +#Helpers +. $PSScriptRoot\Helpers\Helpers.ps1 + +. $PSScriptRoot\Helpers\WebRequestHelper.ps1 +. $PSScriptRoot\Helpers\ActiveDirectoryHelper.ps1 +. $PSScriptRoot\Helpers\SVTMapping.ps1 +. $PSScriptRoot\Helpers\IdentityHelpers.ps1 +. $PSScriptRoot\Helpers\ConfigOverride.ps1 + +. $PSScriptRoot\Models\Common\ResourceInventory.ps1 + + +#Managers +. $PSScriptRoot\Managers\ConfigurationManager.ps1 +. $PSScriptRoot\Managers\FeatureFlightingManager.ps1 +. $PSScriptRoot\Managers\AzSKPDFExtension.ps1 + +. $PSScriptRoot\Helpers\OMSHelper.ps1 +. $PSScriptRoot\Helpers\RemoteReportHelper.ps1 +. $PSScriptRoot\Helpers\RemoteApiHelper.ps1 +. $PSScriptRoot\Core\PrivacyNotice.ps1 + + +#Abstracts +. $PSScriptRoot\Abstracts\AzSKRoot.ps1 +. $PSScriptRoot\Abstracts\SVTBase.ps1 + +. $PSScriptRoot\Abstracts\FixControl\FixControlBase.ps1 +. $PSScriptRoot\Abstracts\FixControl\FixServicesBase.ps1 +. $PSScriptRoot\Abstracts\FixControl\FixSubscriptionBase.ps1 + +. $PSScriptRoot\Abstracts\ListenerBase.ps1 +. $PSScriptRoot\Abstracts\FileOutputBase.ps1 + +. $PSScriptRoot\Helpers\ResourceHelper.ps1 + +#Listeners +. $PSScriptRoot\Listeners\UserReports\WriteFolderPath.ps1 +(Get-ChildItem -Path "$PSScriptRoot\Listeners\UserReports" -Recurse -File -Include "*.ps1" -Exclude "WriteFolderPath.ps1") | + ForEach-Object { + . $_.FullName +} +. $PSScriptRoot\Listeners\GenericListener\GenericListenerBase.ps1 +. $PSScriptRoot\Listeners\RemoteReports\TelemetryStrings.ps1 +. $PSScriptRoot\Helpers\RemoteReportHelper.ps1 +. $PSScriptRoot\Helpers\AIOrgTelemetryHelper.ps1 +. $PSScriptRoot\Listeners\RemoteReports\RemoteReportsListener.ps1 +. $PSScriptRoot\Listeners\RemoteReports\AIOrgTelemetry.ps1 +. $PSScriptRoot\Listeners\RemoteReports\UsageTelemetry.ps1 +. $PSScriptRoot\Listeners\OMS\OMSOutput.ps1 +. $PSScriptRoot\Listeners\FixControl\WriteFixControlFiles.ps1 +. $PSScriptRoot\Listeners\EventHub\EventHubOutput.ps1 +. $PSScriptRoot\Listeners\Webhook\WebhookOutput.ps1 +. $PSScriptRoot\Listeners\GenericListener\GenericListener.ps1 +. $PSScriptRoot\Listeners\SecurityRecommendationReport.ps1 +. $PSScriptRoot\Listeners\ListenerHelper.ps1 + +#Remaining Abstracts +. $PSScriptRoot\Core\SVT\SVTControlAttestation.ps1 +. $PSScriptRoot\Abstracts\CommandBase.ps1 + +#Remaining Abstracts +. $PSScriptRoot\Abstracts\SVTCommandBase.ps1 + +#Core +(Get-ChildItem -Path "$PSScriptRoot\Core\SVT\AAD\" -Recurse -File) | + ForEach-Object { + . $_.FullName +} + +. $PSScriptRoot\Core\SVT\Resolver.ps1 +. $PSScriptRoot\Core\SVT\AADResourceResolver.ps1 +. $PSScriptRoot\Core\SVT\ServicesSecurityStatus.ps1 +#. $PSScriptRoot\Core\SVT\SVTStatusReport.ps1 diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/AIOrgTelemetryHelper.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/AIOrgTelemetryHelper.ps1 new file mode 100644 index 000000000..17317aaad --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/AIOrgTelemetryHelper.ps1 @@ -0,0 +1,516 @@ +Set-StrictMode -Version Latest + +class AIOrgTelemetryHelper { + static hidden [string[]] $ParamsToMask = @("OMSSharedKey"); + static hidden [Microsoft.ApplicationInsights.TelemetryClient] $OrgTelemetryClient; + + #This is a helper object for the actual OrgTelemetry listener. + #In some places it's methods also get called directly (outside of the event listener/handler scheme) + static [PSObject] $CommonProperties; + static AIOrgTelemetryHelper() { + [AIOrgTelemetryHelper]::OrgTelemetryClient = [Microsoft.ApplicationInsights.TelemetryClient]::new() + } + + static [void] TrackEvent([string] $Name) { + [AIOrgTelemetryHelper]::TrackEvent($Name, $null, $null); + } + + static [void] TrackEventWithOnlyProperties([string] $Name, [hashtable] $Properties) { + [AIOrgTelemetryHelper]::TrackEvent($Name, $Properties, $null); + } + + static [void] TrackEventWithOnlyMetrics([string] $Name, [hashtable] $Metrics) { + [AIOrgTelemetryHelper]::TrackEvent($Name, $null, $Metrics); + } + + static [void] TrackEvent([string] $Name, [hashtable] $Properties, [hashtable] $Metrics) { + if (![RemoteReportHelper]::IsAIOrgTelemetryEnabled()) { return; }; + [AIOrgTelemetryHelper]::TrackEventInternal($Name, $Properties, $Metrics); + [AIOrgTelemetryHelper]::OrgTelemetryClient.Flush(); + } + + static [void] TrackEvents([System.Collections.ArrayList] $events) { + #if (![RemoteReportHelper]::IsAIOrgTelemetryEnabled()) { return; }; + #foreach ($item in $events) { + # [AIOrgTelemetryHelper]::TrackEventInternal($item.Name, $item.Properties, $item.Metrics); + #} + [AIOrgTelemetryHelper]::PublishEvent($events,"AIOrg"); + #[AIOrgTelemetryHelper]::OrgTelemetryClient.Flush(); + } + + #TODO-Perf: It appears that Properties object is copied multiple times. See if it is possible to use just one (by-ref) copy per 'send-event-to-ai' operation. + static [void] TrackCommandExecution([string] $Name, [hashtable] $Properties, [hashtable] $Metrics, [System.Management.Automation.InvocationInfo] $invocationContext) { + if (![RemoteReportHelper]::IsAIOrgTelemetryEnabled()) + { + return; + } + $Properties = [AIOrgTelemetryHelper]::AttachInvocationInfo($Properties, $invocationContext); + [AIOrgTelemetryHelper]::TrackEventInternal($Name, $Properties, $Metrics); + [AIOrgTelemetryHelper]::OrgTelemetryClient.Flush(); + } + + static [void] TrackException([System.Management.Automation.ErrorRecord] $ErrorRecord, [System.Management.Automation.InvocationInfo] $InvocationContext) { + [AIOrgTelemetryHelper]::TrackException($ErrorRecord, $null, $null, $InvocationContext); + } + + static [void] TrackExceptionWithOnlyProperties([System.Management.Automation.ErrorRecord] $ErrorRecord, [hashtable] $Properties, [System.Management.Automation.InvocationInfo] $InvocationContext) { + [AIOrgTelemetryHelper]::TrackException($ErrorRecord, $Properties, $null, $InvocationContext); + } + + static [void] TrackExceptionWithOnlyMetrics([System.Management.Automation.ErrorRecord] $ErrorRecord, [hashtable] $Metrics, [System.Management.Automation.InvocationInfo] $InvocationContext) { + [AIOrgTelemetryHelper]::TrackException($ErrorRecord, $null, $Metrics, $InvocationContext); + } + + static [void] TrackException([System.Management.Automation.ErrorRecord] $ErrorRecord, [hashtable] $Properties, [hashtable] $Metrics, [System.Management.Automation.InvocationInfo] $InvocationContext) { + try { + if (![RemoteReportHelper]::IsAIOrgTelemetryEnabled()) { return; }; + $Properties = [AIOrgTelemetryHelper]::AttachInvocationInfo($Properties, $InvocationContext); + $Properties = [AIOrgTelemetryHelper]::AttachCommonProperties($Properties); + $Metrics = [AIOrgTelemetryHelper]::AttachCommonMetrics($Metrics); + $ex = [Microsoft.ApplicationInsights.DataContracts.ExceptionTelemetry]::new() + $ex.Exception = $ErrorRecord.Exception + try + { + $ex.Properties.Add("ScriptStackTrace", $ErrorRecord.ScriptStackTrace) + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + $Properties.Keys | ForEach-Object { + try + { + $event.Properties[$_] = $Properties[$_].ToString(); + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + $Metrics.Keys | ForEach-Object { + try + { + $event.Metrics[$_] = $Metrics[$_].ToString(); + } + catch{ + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + #BUGBUG: Why is this set/fetched for each invocation? + [AIOrgTelemetryHelper]::OrgTelemetryClient.InstrumentationKey = [RemoteReportHelper]::GetAIOrgTelemetryKey(); + [AIOrgTelemetryHelper]::OrgTelemetryClient.TrackException($ex); + [AIOrgTelemetryHelper]::OrgTelemetryClient.Flush(); + } + catch{ + # Eat the current exception which typically happens when network or other API issue while sending telemetry events + # No need to break execution + } + } + + + hidden static [void] TrackEventInternal([string] $Name, [hashtable] $Properties, [hashtable] $Metrics) { + $Properties = [AIOrgTelemetryHelper]::AttachCommonProperties($Properties); + $Metrics = [AIOrgTelemetryHelper]::AttachCommonMetrics($Metrics); + try { + $event = [Microsoft.ApplicationInsights.DataContracts.EventTelemetry]::new() + $event.Name = $Name + $Properties.Keys | ForEach-Object { + try + { + $event.Properties[$_] = $Properties[$_].ToString(); + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + $Metrics.Keys | ForEach-Object { + try + { + $event.Metrics[$_] = $Metrics[$_].ToString(); + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + #BUGBUG: Why is this set/fetched for each invocation? It ought to be set by now. :-( + [AIOrgTelemetryHelper]::OrgTelemetryClient.InstrumentationKey = [RemoteReportHelper]::GetAIOrgTelemetryKey(); + [AIOrgTelemetryHelper]::OrgTelemetryClient.TrackEvent($event); + } + catch{ + # Eat the current exception which typically happens when network or other API issue while sending telemetry events + # No need to break execution + } + } + + hidden static [hashtable] AttachCommonProperties([hashtable] $Properties) { + if ($null -eq $Properties) { + $Properties = @{} + } + else { + $Properties = $Properties.Clone() + } + try { + $NA = "NA"; + try { + $Properties.Add("ScanSource", [RemoteReportHelper]::GetScanSource()); + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try { + $module = Get-Module 'AzSK*' | Select-Object -First 1 + $Properties.Add("ScannerModuleName", $module.Name); + $Properties.Add("ScannerVersion", $module.Version.ToString()); + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try + { + $azureContext = [AccountHelper]::GetCurrentRmContext() + try + { + $Properties.Add([TelemetryKeys]::tenantId, $azureContext.Subscription.Id) + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try + { + $Properties.Add([TelemetryKeys]::TenantName, $azureContext.Subscription.Name) + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try + { + $Properties.Add("AzureEnv", $azureContext.Environment.Name) + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try + { + $Properties.Add("TenantId", $azureContext.Tenant.Id) + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try + { + $Properties.Add("AccountId", $azureContext.Account.Id) + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try + { + if ($Properties.ContainsKey("RunIdentifier")) { + $actualRunId = $Properties["RunIdentifier"] + $Properties["UniqueRunIdentifier"] = [RemoteReportHelper]::Mask($azureContext.Account.Id + '##' + $actualRunId.ToString()) + } + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try + { + $Properties.Add("AccountType", $azureContext.Account.Type); + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + return $Properties; + } + + hidden static [hashtable] AttachCommonMetrics([hashtable] $Metrics) { + if ($null -eq $Metrics) { + $Metrics = @{} + } + else { + $Metrics = $Metrics.Clone() + } + return $Metrics; + } + + hidden static [hashtable] AttachInvocationInfo([hashtable] $Properties, [System.Management.Automation.InvocationInfo] $invocationContext) { + if ($null -eq $Properties) { + $Properties = @{} + } + else { + $Properties = $Properties.Clone() + } + if ($null -eq $invocationContext) { return $Properties}; + $Properties.Add("Command", $invocationContext.MyCommand.Name) + $params = @{} + $invocationContext.BoundParameters.Keys | ForEach-Object { + $value = "MASKED" + if (![AIOrgTelemetryHelper]::ParamsToMask.Contains($_)) { + $value = $invocationContext.BoundParameters[$_].ToString() + } + $Properties.Add("Param" + $_, $value) + $params.Add("$_", $value) + } + $Properties.Add("Params", [Helpers]::ConvertToJsonCustomCompressed($params)) + $loadedModules = Get-Module | ForEach-Object { $_.Name + "=" + $_.Version.ToString()} + $Properties.Add("LoadedModules" , ($loadedModules -join ';')) + return $Properties; + } + + static [void] PublishEvent([string] $EventName, [hashtable] $Properties, [hashtable] $Metrics) { + try { + #return if telemetry key is empty + $telemetryKey= [RemoteReportHelper]::GetAIOrgTelemetryKey() + if ([string]::IsNullOrWhiteSpace($telemetryKey)) { return; }; + $eventObj = [AIOrgTelemetryHelper]::GetEventBaseObject($EventName) + $eventObj=[AIOrgTelemetryHelper]::SetCommonProperties($eventObj) + + if ($null -ne $Properties) { + $Properties.Keys | ForEach-Object { + try { + if (!$eventObj.data.baseData.properties.ContainsKey($_)) { + $eventObj.data.baseData.properties.Add($_ , $Properties[$_].ToString()) + } + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + } + if ($null -ne $Metrics) { + $Metrics.Keys | ForEach-Object { + try { + $metric = $Metrics[$_] -as [double] + if (!$eventObj.data.baseData.measurements.ContainsKey($_) -and $null -ne $metric) { + $eventObj.data.baseData.measurements.Add($_ , $Metrics[$_]) + } + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + } + + $eventJson = ConvertTo-Json $eventObj -Depth 100 -Compress + + Invoke-WebRequest -Uri "https://dc.services.visualstudio.com/v2/track" ` + -Method Post ` + -ContentType "application/x-json-stream" ` + -Body $eventJson ` + -UseBasicParsing | Out-Null + } + catch { + # Eat the current exception which typically happens when network or other API issue while sending telemetry events + # No need to break execution + } + } + + static [void] PublishARMCheckerEvent([string] $EventName, [hashtable] $Properties, [hashtable] $Metrics) { + try { + $armcheckerscantelemetryEvents = [System.Collections.ArrayList]::new() + $telemetryEvent = "" | Select-Object Name, Properties, Metrics + $telemetryEvent.Name = $EventName + $telemetryEvent.Properties = $Properties + $telemetryEvent.Metrics = $Metrics + $armcheckerscantelemetryEvents.Add($telemetryEvent) + [AIOrgTelemetryHelper]::PublishARMCheckerEvent($armcheckerscantelemetryEvents); + } + catch { + # Left blank intentionally + # Error while sending events to telemetry. No need to break the execution. + } + } + static [void] PublishARMCheckerEvent([System.Collections.ArrayList] $armcheckerscantelemetryEvents) { + try + { + #Attach Common Properties to each EventObject + $armcheckerscantelemetryEvents | ForEach-Object -Begin{ + $module = Get-Module 'AzSK*' | Select-Object -First 1 + } -Process { + $_.Properties.Add("ScannerModuleName", $module.Name); + $_.Properties.Add("ScannerVersion", $module.Version.ToString()); + $_.Properties.Add("Command","Get-AzSKARMChecker") + } -End {} + + [AIOrgTelemetryHelper]::PublishEvent($armcheckerscantelemetryEvents,"Usage"); + } + catch{ + # Left blank intentionally + # Error while sending events to telemetry. No need to break the execution. + } + + } + + static [PSObject] GetEventBaseObject([string] $EventName) { + $telemetryKey= [RemoteReportHelper]::GetAIOrgTelemetryKey() + $eventObj = "" | Select-Object data, iKey, name, tags, time + $eventObj.iKey = $telemetryKey + $eventObj.name = "Microsoft.ApplicationInsights." + $telemetryKey.Replace("-", "") + ".Event" + $eventObj.time = [datetime]::UtcNow.ToString("o") + + $eventObj.tags = "" | Select-Object ai.internal.sdkVersion + $eventObj.tags.'ai.internal.sdkVersion' = "dotnet: 2.1.0.26048" + + $eventObj.data = "" | Select-Object baseData, baseType + $eventObj.data.baseType = "EventData" + $eventObj.data.baseData = "" | Select-Object ver, name, measurements, properties + + $eventObj.data.baseData.ver = 2 + $eventObj.data.baseData.name = $EventName + + $eventObj.data.baseData.measurements = New-Object 'system.collections.generic.dictionary[string,double]' + $eventObj.data.baseData.properties = New-Object 'system.collections.generic.dictionary[string,string]' + + return $eventObj; + } + + #Telemetry functions -- start here + static [PSObject] SetCommonProperties([psobject] $EventObj) { + $notAvailable = "NA" + if([AIOrgTelemetryHelper]::CommonProperties) + { + try{ + $EventObj.data.baseData.properties.Add("tenantId",[AIOrgTelemetryHelper]::CommonProperties.tenantId) + $EventObj.data.baseData.properties.Add("TenantName",[AIOrgTelemetryHelper]::CommonProperties.TenantName) + $azureContext = [AccountHelper]::GetCurrentRmContext() + $EventObj.data.baseData.properties.Add("TenantId", $azureContext.Tenant.Id) + $EventObj.data.baseData.properties.Add("AccountId", $azureContext.Account.Id) + } + catch{ + # Eat the current exception which typically happens to avoid any break in event push + # No need to break execution + } + } + return $EventObj + } + + static [PSObject] GetUsageEventBaseObject([string] $EventName,[string] $type) { + $eventObj = "" | Select-Object data, iKey, name, tags, time + if($type -eq "Usage") + { + $eventObj.iKey = [Constants]::UsageTelemetryKey + } + else + { + $eventObj.iKey = [RemoteReportHelper]::GetAIOrgTelemetryKey() + } + $eventObj.name = $EventName + $eventObj.time = [datetime]::UtcNow.ToString("o") + + $eventObj.tags = "" | Select-Object ai.internal.sdkVersion + $eventObj.tags.'ai.internal.sdkVersion' = "dotnet: 2.1.0.26048" + + $eventObj.data = "" | Select-Object baseData, baseType + $eventObj.data.baseType = "EventData" + $eventObj.data.baseData = "" | Select-Object ver, name, measurements, properties + + $eventObj.data.baseData.ver = 2 + $eventObj.data.baseData.name = $EventName + + $eventObj.data.baseData.measurements = New-Object 'system.collections.generic.dictionary[string,double]' + $eventObj.data.baseData.properties = New-Object 'system.collections.generic.dictionary[string,string]' + + return $eventObj; + } + + + static [void] PublishEvent([System.Collections.ArrayList] $servicescantelemetryEvents,[string] $type) { + #TODO: Revisit AI telemetry post-preview + + try { + + $eventlist = [System.Collections.ArrayList]::new() + + $servicescantelemetryEvents | ForEach-Object { + + $eventObj = [AIOrgTelemetryHelper]::GetUsageEventBaseObject($_.Name,$type) + #SetCommonProperties -EventObj $eventObj + + $currenteventobj = $_ + if ($null -ne $currenteventobj.Properties) { + $currenteventobj.Properties.Keys | ForEach-Object { + try { + if (!$eventObj.data.baseData.properties.ContainsKey($_)) { + $eventObj.data.baseData.properties.Add($_ , $currenteventobj.Properties[$_].ToString()) + } + } + catch + { + # Left blank intentionally + # Error while sending CA events to telemetry. No need to break the execution. + } + } + } + if ($null -ne $currenteventobj.Metrics) { + $currenteventobj.Metrics.Keys | ForEach-Object { + try { + $metric = $currenteventobj.Metrics[$_] -as [double] + if (!$eventObj.data.baseData.measurements.ContainsKey($_) -and $null -ne $metric) { + $eventObj.data.baseData.measurements.Add($_ , $currenteventobj.Metrics[$_]) + } + } + catch { + # Left blank intentionally + # Error while sending CA events to telemetry. No need to break the execution. + } + } + } + $eventlist.Add($eventObj) + } + $eventJson = ConvertTo-Json $eventlist -Depth 100 -Compress + #Write-Warning("TODO: AI Org Telemetry IWR turned OFF.") + Invoke-WebRequest -Uri "https://dc.services.visualstudio.com/v2/track" ` + -Method Post ` + -ContentType "application/x-json-stream" ` + -Body $eventJson ` + -UseBasicParsing | Out-Null + + } + catch { + # Left blank intentionally + # Error while sending CA events to telemetry. No need to break the execution. + } + } + +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/AccountHelper.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/AccountHelper.ps1 new file mode 100644 index 000000000..57ea0f862 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/AccountHelper.ps1 @@ -0,0 +1,430 @@ +using namespace Newtonsoft.Json +using namespace Microsoft.Azure.Commands.Common.Authentication.Abstractions +using namespace Microsoft.Azure.Commands.Common.Authentication +using namespace Microsoft.Azure.Management.Storage.Models +using namespace Microsoft.IdentityModel.Clients.ActiveDirectory + +Set-StrictMode -Version Latest + + + +# Represents subset of directory roles that we check against for 'AAD admin-or-not' +[Flags()] +enum PrivilegedAADRoles +{ + None = 0 + SecurityReader = 1 + UserAccountAdmin = 2 + SecurityAdmin = 4 + CompanyAdmin = 8 +} + +#Creates an object for our (internal) representation of a privileged role +#The term 'privileged' or 'privRole' here refers to directory roles we consider in 'admin-or-not' check +#It does not refer to AAD-PIM (at least as yet) +function New-PrivRole() +{ + param ($DisplayName, $ObjectId, $AADPrivRole) + + $privRole = new-object PSObject + + $privRole | add-member -type NoteProperty -Name DisplayName -Value $DisplayName + $privRole | add-member -type NoteProperty -Name ObjectId -Value $ObjectId + $privRole | add-member -type NoteProperty -Name AADPrivRole -Value $AADPrivRole + + return $privRole +} + +class AccountHelper { + static hidden [PSObject] $currentAADContext; + static hidden [PSObject] $currentAzContext; + static hidden [PSObject] $currentRMContext; + static hidden [PSObject] $AADAPIAccessToken; + + #TODO: 'static' => most of these will get set for session! (Also statics in [Tenant] class) + #TODO: May need to consider situations where user runs for 2 diff tenants in same session... + static hidden [string] $tenantInfoMsg; + + static hidden [PSObject] $currentAADUserObject; + + static hidden [CommandType] $ScanType; + + static hidden [PrivilegedAADRoles] $UserAADPrivRoles = [PrivilegedAADRoles]::None; + static hidden [bool] $rolesLoaded = $false; + + hidden static [PSObject] GetCurrentRMContext() + { + if (-not [AccountHelper]::currentRMContext) + { + $rmContext = Get-AzContext -ErrorAction Stop + + if ((-not $rmContext) -or ($rmContext -and (-not $rmContext.Subscription -or -not $rmContext.Account))) { + [EventBase]::PublishGenericCustomMessage("No active Azure login session found. Initiating login flow...", [MessageType]::Warning); + [PSObject]$rmLogin = $null + $AzureEnvironment = [Constants]::DefaultAzureEnvironment + $AzskSettings = [Helpers]::LoadOfflineConfigFile("AzSK.AzureDevOps.Settings.json", $true) + if([Helpers]::CheckMember($AzskSettings,"AzureEnvironment")) + { + $AzureEnvironment = $AzskSettings.AzureEnvironment + } + if(-not [string]::IsNullOrWhiteSpace($AzureEnvironment) -and $AzureEnvironment -ne [Constants]::DefaultAzureEnvironment) + { + try{ + $rmLogin = Connect-AzAccount -EnvironmentName $AzureEnvironment + } + catch{ + [EventBase]::PublishGenericException($_); + } + } + else + { + $rmLogin = Connect-AzAccount + } + if ($rmLogin) { + $rmContext = $rmLogin.Context; + } + } + [AccountHelper]::currentRMContext = $rmContext + } + + return [AccountHelper]::currentRMContext + } + + hidden static [PSObject] GetCurrentAzContext() + { + if ([AccountHelper]::currentAzContext -eq $null) + { + throw ([SuppressedException]::new(("Cannot call this method before getting a sign-in context!"), [SuppressedExceptionType]::InvalidOperation)) + } + return [AccountHelper]::currentAzContext + } + + hidden static [void] ClearTenantContext() + { + [AccountHelper]::currentAADContext = $null; + [AccountHelper]::currentAzContext = $null; + [AccountHelper]::currentRMContext = $null; + [AccountHelper]::AADAPIAccessToken = $null; + [AccountHelper]::tenantInfoMsg = $null; + + [AccountHelper]::currentAADUserObject = $null; + + [AccountHelper]::UserAADPrivRoles = [PrivilegedAADRoles]::None; + [AccountHelper]::rolesLoaded = $false; + } + + # Can be called with $null (when tenantId is not specified by the user) + hidden static [PSObject] GetCurrentAzContext($desiredTenantId) + { + if(-not [AccountHelper]::currentAzContext) + { + $azContext = Get-AzContext + + #If there's no Az ctx, or it is indeterminate (user has no Azure subscription) or the tenantId in the azCtx does not match desired tenantId + if ($azContext -eq $null -or $azContext.Tenant -eq $null -or (-not [string]::IsNullOrEmpty($desiredTenantId) -and $azContext.Tenant.Id -ne $desiredTenantId)) + { + #TODO: Consider simplifying this...use AzCtx only if no tenantId or tenantId matches...for all else just do fresh ConnectAzureAD?? + #Better than clearing up existing AzCtx a user may want to keep using otherwise. + if ($azContext) #If we have a context for another tenant, disconnect. + { + Disconnect-AzAccount -ErrorAction Stop + } + #Now try to fetch a fresh context. + try { + $azureContext = Connect-AzAccount -ErrorAction Stop + #On a fresh login, the 'cached' context object we care about is inside the AzureContext + $azContext = $azureContext.Context + } + catch { + Write-Error "Could not login to Azure environment..." #TODO: PublishCustomMessage equivalent for 'static' classes? + throw ([SuppressedException]::new(("Could not login to Azure envmt. Will try direct Connect-AzureAD...."), [SuppressedExceptionType]::AccessDenied)) + } + } + [AccountHelper]::currentAzContext = $azContext + } + return [AccountHelper]::currentAzContext + } + + hidden static [PSObject] GetCurrentAADContext() + { + if ([AccountHelper]::currentAADContext -eq $null) + { + throw ([SuppressedException]::new(("Cannot call this method before getting a sign-in context!"), [SuppressedExceptionType]::InvalidOperation)) + } + return [AccountHelper]::currentAADContext + } + + hidden static [PSObject] GetCurrentAADContext($desiredTenantId) #Can be $null if user did not pass one. + { + $currAADCtx = [AccountHelper]::currentAADContext + + # If we don't have a context *or* the context does not match a non-null desired tenant + if(-not $currAADCtx -or (-not [String]::IsNullOrEmpty($desiredTenantId) -and $desiredTenantId -ne $currAADCtx.TenantID)) + { + [AccountHelper]::ClearTenantContext() + + $aadContext = $null + $aadUserObj = $null + #Try leveraging Azure context if available + try { + $tenantId = $null + $crossTenant = $false + $accountId = $null + + if (-not [string]::IsNullOrEmpty($desiredTenantId)) + { + $tenantId = $desiredTenantId + } + + $azContext = $null + try { + #Either throws or returns non-null + $azContext = [AccountHelper]::GetCurrentAzContext($desiredTenantId) + $accountId = $azContext.Account.Id + } + catch { + Write-Warning "Could not acquire Azure context. Falling back to Connect-AzureAD..." + } + + if ($azContext -ne $null -and $azContext.Tenant -ne $null) #Can be $null when a user has no Azure subscriptions. + { + $nativeTenantId = $azContext.Tenant.Id + if ($tenantId -eq $null) #No 'desired tenant' passed in by user + { + $tenantId = $nativeTenantId + } + else + { + #Check if desiredTenant and native tenant are diff => this user is guest in the desired tenant + if ($nativeTenantId -ne $desiredTenantId) + { + $crossTenant = $true + } + } + } + + $aadContext = $null + if (-not [string]::IsNullOrEmpty($tenantId) -and -not [string]::IsNullOrEmpty($accountId)) + { + $aadContext = Connect-AzureAD -TenantId $tenantId -AccountId $accountId -ErrorAction Stop + } + elseif (-not [string]::IsNullOrEmpty($accountId)) + { + $aadContext = Connect-AzureAd -AccountId $accountId -ErrorAction Stop + $tenantId = $aadContext.TenantId + } + else { + $aadContext = Connect-AzureAd -ErrorAction Stop + $tenantId = $aadContext.TenantId + } + + if (-not [String]::IsNullOrEmpty($desiredTenantId) -and $desiredTenantId -ne $aadContext.TenantID) + { + Write-Error "Mismatch between desired tenantId: $desiredTenantId and tenantId from login context: $($aadContext.TenantId).`r`nYou may have mistyped the value of 'tenantId' parameter. Please try again!" + throw ([SuppressedException]::new("Mismatch between desired tenantId: $desiredTenantId and tenantId from login context: $($aadContext.TenantId)", [SuppressedExceptionType]::Generic)) + } + + $upn = $aadContext.Account.Id + if (-not $crossTenant) + { + #in this case UPN is same as signin name use + $aadUserObj = Get-AzureADUser -Filter "UserPrincipalName eq '$upn'" + } + else + { + #Cross-tenant, UPN is the mangled version e.g., joe_contoso.com#desiredtenant.com + $upnx = (($upn -replace '@', '_')+'#') + $filter = "startswith(UserPrincipalName,'" + $upnx + "')" + $aadUserObj = Get-AzureAdUser -Filter $filter + } + } + catch { + throw ([SuppressedException]::new("Could not acquire an AAD tenant context!`r`n$_", [SuppressedExceptionType]::Generic)) + } + + [AccountHelper]::ScanType = [CommandType]::AAD + [AccountHelper]::currentAADContext = $aadContext + [AccountHelper]::currentAADUserObject = $aadUserObj + [AccountHelper]::tenantInfoMsg = "AAD Tenant Info: `n`tDomain: $($aadContext.TenantDomain)`n`tTenanId: $($aadContext.TenantId)" + } + + return [AccountHelper]::currentAADContext + } + + static [string] GetCurrentTenantInfo() + { + return [AccountHelper]::tenantInfoMsg + } + + static [string] GetCurrentSessionUser() + { + $context = [AccountHelper]::GetCurrentAADContext() + if ($null -ne $context) { + return $context.Account.Id + } + else { + return "NO_ACTIVE_SESSION" + } + } + + static [string] GetCurrentSessionUserObjectId() + { + return ([AccountHelper]::GetCurrentAADUserObject()).ObjectId; + } + + hidden static [PSObject] GetCurrentAADUserObject() + { + return [AccountHelper]::currentAADUserObject + } + + hidden static [PSObject] GetEnabledPrivRolesInTenant() + { + #Get subset of directory level roles that have been enabled in this tenant. (Not orgs enable all roles.) + $enabledDirRoles = [array] (Get-AzureADDirectoryRole) + + #$srRole = $activeRoles | ? { $_.DisplayName -eq "Security Reader"} + + $apr = @() + $enabledDirRoles | % { + $ar = $_ + + switch ($ar.DisplayName) + { + 'Security Reader' { + $apr += New-PrivRole -DisplayName 'Security Reader' -ObjectId $ar.ObjectId -AADPrivRole ([PrivilegedAADRoles]::SecurityReader) + } + + 'User Account Administrator' { + $apr += New-PrivRole -DisplayName 'User Account Administrator' -ObjectId $ar.ObjectId -AADPrivRole ([PrivilegedAADRoles]::UserAccountAdmin) + } + + 'Security Administrator' { + $apr += New-PrivRole -DisplayName 'Security Administrator' -ObjectId $ar.ObjectId -AADPrivRole ([PrivilegedAADRoles]::SecurityAdmin) + } + + 'Company Administrator' { + $apr += New-PrivRole -DisplayName 'Company Administrator' -ObjectId $ar.ObjectId -AADPrivRole ([PrivilegedAADRoles]::CompanyAdmin) + } + } + } + return $apr + } + + #Returns a bit flag representing all roles we consider 'admin-like' that the user is currently a member of. + #TODO: This only uses 'permanent' membership checks currently. Need to augment for PIM. + static [PrivilegedAADRoles] GetUserPrivTenantRoles([String] $uid) + { + if ([AccountHelper]::rolesLoaded -eq $false) + { + $upr = [PrivilegedAADRoles]::None + $apr = [AccountHelper]::GetEnabledPrivRolesInTenant() + $apr | % { + $pr = $_ + #Write-Host "$pr.AADPrivRole" + $roleMembers = [array] (Get-AzureADDirectoryRoleMember -ObjectId $pr.ObjectId) + #Write-Host "Count: $($roleMembers.Count)" + if($roleMembers) + { + $roleMembers | % { if ($_.ObjectId -eq $uid) {$upr = $upr -bor $pr.AADPrivRole}} + } + + } + + [AccountHelper]::UserAADPrivRoles = $upr + [AccountHelper]::rolesLoaded = $true + } + return [AccountHelper]::UserAADPrivRoles + } + + #Is user a member of any directory role we consider 'admin-equiv.'? + #Note: #TODO: This does not check for PIM-based role membership yet. + static [bool] IsUserInAPermanentAdminRole() + { + $uid = ([AccountHelper]::GetCurrentAADUserObject()).ObjectId + $upr = [AccountHelper]::GetUserPrivTenantRoles($uid) + return ($upr -ne [PrivilegedAADRoles]::None) + } + + + hidden static [PSObject] GetCurrentAADAPIToken() + { + if(-not [AccountHelper]::AADAPIAccessToken) + { + $apiToken = $null + + $AADAPIGuid = [Constants]::AADAPIGuid + + #Try leveraging Azure context if available + try { + #Either throws or returns non-null + $azContext = [AccountHelper]::GetCurrentAzContext() + $tenantId = $null + if ($azContext.Tenant -ne $null) #happens if user does not have any Azure subs. + { + $tenantId = $azContext.Tenant.Id + } + else { + $tenantId = ([AccountHelper]::GetCurrentAADContext()).TenantId + } + $apiToken = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate($azContext.Account, $azContext.Environment, $tenantId, $null, "Never", $null, $AADAPIGuid) + } + catch { + Write-Warning "Could not get AAD API token for: $AADAPIGuid." + throw ([SuppressedException]::new("Could not get AAD API token for: $AADAPIGuid.", [SuppressedExceptionType]::Generic)) + } + + [AccountHelper]::AADAPIAccessToken = $apiToken + #TODO move to detailed log: Write-Host("Successfully acquired API access token for $AADAPIGuid") + } + return [AccountHelper]::AADAPIAccessToken + } + + + hidden static [void] ResetCurrentRMContext() + { + [AccountHelper]::currentRMContext = $null + } + + #TODO: Review calls to this. Should we have an AAD-version for it? Or just remove... + static [string] GetAccessToken([string] $resourceAppIdUri, [string] $tenantId) + { + return [AccountHelper]::GetAzureDevOpsAccessToken(); + } + + static [string] GetAzureDevOpsAccessToken() + { + # TODO: Handlle login + if([AccountHelper]::currentAzureDevOpsContext) + { + return [AccountHelper]::currentAzureDevOpsContext.AccessToken + } + else + { + return $null + } + } + + static [string] GetAccessToken([string] $resourceAppIdUri) + { + if([AccountHelper]::ScanType -eq [CommandType]::AzureDevOps) + { + return [AccountHelper]::GetAzureDevOpsAccessToken() + } + else { + return [AccountHelper]::GetAccessToken($resourceAppIdUri, ""); + } + + } + + static [string] GetAccessToken() + { + if([AccountHelper]::ScanType -eq [CommandType]::AzureDevOps) + { + return [AccountHelper]::GetAzureDevOpsAccessToken() + } + else { + #TODO : Fix ResourceID + return [AccountHelper]::GetAccessToken("", ""); + } + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/ActiveDirectoryHelper.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/ActiveDirectoryHelper.ps1 new file mode 100644 index 000000000..080992694 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/ActiveDirectoryHelper.ps1 @@ -0,0 +1,244 @@ +Set-StrictMode -Version Latest +class ActiveDirectoryHelper { + + static [PSObject] GetADAppServicePrincipalByAppId($ApplicationId) + { + $TenantId = ([AccountHelper]::GetCurrentRmContext()).Tenant.Id + $ApiVersion = "1.6" + $GraphApiUrl = [WebRequestHelper]::GraphApiUri + $TenantId + "/servicePrincipals/{0}?api-version=$ApiVersion" + $uri = [string]::Format($GraphApiUrl + "&`$filter=(appId eq '{1}')", [string]::Empty , $ApplicationId); + $resultObject = [WebRequestHelper]::InvokeGetWebRequest($uri); + + #this returns array of objects. Actual object is present at 1st index + if($resultObject) + { + return $resultObject[0] + } + else + { + return $null + } + + } + + static [void] UpdateADAppServicePrincipalCredential( + $ApplicationID, + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $PublicCert, + [System.DateTime] + $NotBefore = (Get-Date).AddDays(-1), + [System.DateTime] + $NotAfter = $NotBefore.AddMonths(6), + [string] + $Delete = "False" + ) + { + #Initialization + $TenantId = ([AccountHelper]::GetCurrentRmContext()).Tenant.Id + $ApiVersion = "1.6" + $GraphApiUrl = [WebRequestHelper]::GraphApiUri + $TenantId + "/servicePrincipals/{0}?api-version=$ApiVersion" + $addMode = $False; + $startDateString = $NotBefore.ToString("O"); + $endDateString = $NotAfter.ToString("O"); + + if($Delete -eq "False") + { + if(-not $PublicCert) + { + throw "Public Certificate cannot be null" + } + } + + $servicePrincipal = [ActiveDirectoryHelper]::GetADAppServicePrincipalByAppId($ApplicationID) + + if($Delete -eq "False") + { + if($null -eq $servicePrincipal) + { + $addMode = $True; + $servicePrincipal = New-Object -TypeName PSObject; + $servicePrincipal | Add-Member -MemberType NoteProperty -Name appId -Value $ApplicationID -PassThru + } + + $publicCertString = [System.Convert]::ToBase64String($PublicCert.GetRawCertData()); + + $credentialObject = New-Object -TypeName PSObject + $credentialObject | Add-Member -MemberType NoteProperty -Name endDate -Value $endDateString -PassThru ` + | Add-Member -MemberType NoteProperty -Name startDate -Value $startDateString -PassThru + + $credentialObject | Add-Member -MemberType NoteProperty -Name type -Value "AsymmetricX509Cert" -PassThru ` + | Add-Member -MemberType NoteProperty -Name usage -Value "Verify" -PassThru ` + | Add-Member -MemberType NoteProperty -Name value -Value $publicCertString + + if ([bool](Get-Member -InputObject $servicePrincipal -Name "keyCredentials")) + { + [System.Collections.ArrayList]$keys = $servicePrincipal.keyCredentials + $credentialList = $keys.Add($credentialObject) + $servicePrincipal.keyCredentials = $keys + } + else + { + $servicePrincipal | Add-Member -MemberType NoteProperty -Name keyCredentials -Value @($credentialObject) + } + } + elseif($Delete -eq "True") + { + $servicePrincipal.keyCredentials = $servicePrincipal.keyCredentials | Where-Object { + [System.DateTime]::Parse($_.startDate).ToUniversalTime() -ne $NotBefore.ToUniversalTime() ` + -and [System.DateTime]::Parse($_.endDate).ToUniversalTime() -ne $NotAfter.ToUniversalTime() ` + } + } + elseif($Delete -eq "All") + { + $servicePrincipal.keyCredentials = @() + } + $servicePrincipal = $servicePrincipal | Select-Object * -ExcludeProperty "requiredResourceAccess" + + $body = ConvertTo-Json -InputObject $servicePrincipal + $operation = [string]::Empty; + $requestUri = [string]::Empty; + $GraphAPIAccessToken = Get-AzSKAccessToken -ResourceAppIdURI "https://graph.windows.net/"; + if($addMode) + { + $operation = "POST"; + $requestUri = [string]::Format($GraphApiUrl, [string]::Empty); + } + else + { + $operation = "PATCH"; + $requestUri = [string]::Format($GraphApiUrl, $servicePrincipal.objectId); + } + + $updateResult = Invoke-RestMethod ` + -Method $operation ` + -Uri $requestUri ` + -Headers @{ Authorization = "Bearer " + $GraphAPIAccessToken } ` + -ContentType "application/json" ` + -Body $body ` + -UseBasicParsing + + if($null -eq $updateResult) + { + Throw "There was a problem while updating the service principal with new certificate" + } + } + + static [PSObject] GetADAppByAppId($ApplicationId) + { + $TenantId = ([AccountHelper]::GetCurrentRmContext()).Tenant.Id + $ApiVersion = "1.6" + $GraphApiUrl = [WebRequestHelper]::GraphApiUri + $TenantId + "/applications/{0}?api-version=$ApiVersion" + $uri = [string]::Format($GraphApiUrl + "&`$filter=(appId eq '{1}')", [string]::Empty , $ApplicationId); + $resultObject = [WebRequestHelper]::InvokeGetWebRequest($uri); + + #this returns array of objects. Actual object is present at 1st index + if($resultObject) + { + return $resultObject[0] + } + else + { + return $null + } + + } + + static [void] UpdateADAppCredential( + $ApplicationID, + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $PublicCert, + [System.DateTime] + $NotBefore = (Get-Date).AddDays(-1), + [System.DateTime] + $NotAfter = $NotBefore.AddMonths(6), + [string] + $Delete = "False" + ) + { + #Initialization + $TenantId = ([AccountHelper]::GetCurrentRmContext()).Tenant.Id + $ApiVersion = "1.6" + $GraphApiUrl = [WebRequestHelper]::GraphApiUri + $TenantId + "/applications/{0}?api-version=$ApiVersion" + $startDateString = $NotBefore.ToString("O"); + $endDateString = $NotAfter.ToString("O"); + + if($Delete -eq "False") + { + if(-not $PublicCert) + { + throw "Public Certificate cannot be null" + } + } + + $ADApplication = [ActiveDirectoryHelper]::GetADAppByAppId($ApplicationID) + if($Delete -eq "False") + { + $publicCertString = [System.Convert]::ToBase64String($PublicCert.GetRawCertData()); + + $credentialObject = New-Object -TypeName PSObject + $credentialObject | Add-Member -MemberType NoteProperty -Name endDate -Value $endDateString -PassThru ` + | Add-Member -MemberType NoteProperty -Name startDate -Value $startDateString -PassThru + + $credentialObject | Add-Member -MemberType NoteProperty -Name type -Value "AsymmetricX509Cert" -PassThru ` + | Add-Member -MemberType NoteProperty -Name usage -Value "Verify" -PassThru ` + | Add-Member -MemberType NoteProperty -Name value -Value $publicCertString + if ([bool](Get-Member -InputObject $ADApplication -Name "keyCredentials")) + { + [System.Collections.ArrayList]$keys = $ADApplication.keyCredentials + $keys.Add($credentialObject) + $ADApplication.keyCredentials = $keys + } + else + { + $ADApplication | Add-Member -MemberType NoteProperty -Name keyCredentials -Value @($credentialObject) + } + } + elseif($Delete -eq "True") + { + $ADApplication.keyCredentials = $ADApplication.keyCredentials | Where-Object { + [System.DateTime]::Parse($_.startDate).ToUniversalTime() -ne $NotBefore.ToUniversalTime() ` + -and [System.DateTime]::Parse($_.endDate).ToUniversalTime() -ne $NotAfter.ToUniversalTime() ` + } + } + elseif($Delete -eq "All") + { + $ADApplication.keyCredentials = @() + } + $finalCredsObject = $ADApplication | Select-Object -Property keyCredentials + $body = ConvertTo-Json -InputObject $finalCredsObject + $operation = [string]::Empty; + $requestUri = [string]::Empty; + $GraphAPIAccessToken = Get-AzSKAccessToken -ResourceAppIdURI "https://graph.windows.net/"; + $operation = "PATCH"; + $requestUri = [string]::Format($GraphApiUrl, $ADApplication.objectId); + $updateResult = Invoke-RestMethod ` + -Method $operation ` + -Uri $requestUri ` + -Headers @{ Authorization = "Bearer " + $GraphAPIAccessToken } ` + -ContentType "application/json" ` + -Body $body ` + -UseBasicParsing + + if($null -eq $updateResult) + { + Throw "There was a problem while updating the service principal with new certificate" + } + } + + static [PSObject] NewSelfSignedCertificate($AppName,$CertStartDate,$CertEndDate,$Provider) + { + $newCertificate = New-SelfSignedCertificate -DnsName $AppName ` + -Subject "CN=$AppName" ` + -CertStoreLocation Cert:\CurrentUser\My ` + -KeyExportPolicy Exportable ` + -NotBefore $CertStartDate ` + -NotAfter $CertEndDate ` + -Type DocumentEncryptionCert ` + -KeyUsage DataEncipherment ` + -KeySpec KeyExchange ` + -KeyUsageProperty Decrypt ` + -Provider $Provider ` + -ErrorAction Stop + return $newCertificate + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/AliasHelper.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/AliasHelper.ps1 new file mode 100644 index 000000000..21e2b13e3 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/AliasHelper.ps1 @@ -0,0 +1,7 @@ +[CommandHelper]::Mapping | ForEach-Object { + $commandName = $_.Verb + '-' + $_.Noun + $alias = $_.ShortName + Set-Alias -Name $alias -Value $commandName -ErrorAction SilentlyContinue + Export-ModuleMember -Alias $alias -Function $commandName +} +Export-ModuleMember -Alias "*AzSK*" -Function "*" diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/CommandHelper.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/CommandHelper.ps1 new file mode 100644 index 000000000..8e290b0f1 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/CommandHelper.ps1 @@ -0,0 +1,366 @@ +using namespace System.Management.Automation +Set-StrictMode -Version Latest +class CommandHelper +{ + static [CommandDetails[]] $Mapping = @( + # Services Security Status + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKAzureServicesSecurityStatus"; + ShortName = "GRS"; + IsLatestRequired = $false; + }, + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKControlsStatus"; + ShortName = "GACS"; + IsLatestRequired = $false; + }, + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKExpressRouteNetworkSecurityStatus"; + ShortName = "GES"; + IsLatestRequired = $false; + }, + + #Subscription Security + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKSubscriptionSecurityStatus"; + ShortName = "GSS"; + IsLatestRequired = $false; + }, + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKSubscriptionSecurity"; + ShortName = "SSS"; + HasAzSKComponentWritePermission = $true; + }, + [CommandDetails]@{ + Verb = "Update"; + Noun = "AzSKSubscriptionSecurity"; + ShortName = "USS"; + IsLatestRequired = $false; + HasAzSKComponentWritePermission = $true; + }, + [CommandDetails]@{ + Verb = "Remove"; + Noun = "AzSKSubscriptionSecurity"; + ShortName = "RSS"; + }, + + # CA + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKContinuousAssurance"; + ShortName = "GCA"; + IsLatestRequired = $false; + }, + [CommandDetails]@{ + Verb = "Install"; + Noun = "AzSKContinuousAssurance"; + ShortName = "ICA"; + HasAzSKComponentWritePermission = $true; + }, + [CommandDetails]@{ + Verb = "Remove"; + Noun = "AzSKContinuousAssurance"; + ShortName = "RCA"; + }, + [CommandDetails]@{ + Verb = "Update"; + Noun = "AzSKContinuousAssurance"; + ShortName = "UCA"; + HasAzSKComponentWritePermission = $true; + }, + + #Alerts + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKAlerts"; + ShortName = "SAA"; + HasAzSKComponentWritePermission = $true; + }, + [CommandDetails]@{ + Verb = "Remove"; + Noun = "AzSKAlerts"; + ShortName = "RAL"; + }, + + #Alerts Monitoring + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKAlertMonitoring"; + ShortName = "SAM"; + }, + [CommandDetails]@{ + Verb = "Remove"; + Noun = "AzSKAlertMonitoring"; + ShortName = "RAM"; + }, + + #ARM Policies + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKARMPolicies"; + ShortName = "SAP"; + HasAzSKComponentWritePermission = $true; + }, + [CommandDetails]@{ + Verb = "Remove"; + Noun = "AzSKARMPolicies"; + ShortName = "RAP"; + }, + + #RBAC + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKSubscriptionRBAC"; + ShortName = "SRB"; + HasAzSKComponentWritePermission = $true; + }, + [CommandDetails]@{ + Verb = "Remove"; + Noun = "AzSKSubscriptionRBAC"; + ShortName = "RRB"; + }, + + # Security Centre + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKAzureSecurityCenterPolicies"; + ShortName = "SSC"; + HasAzSKComponentWritePermission = $true; + }, + + # OMS + [CommandDetails]@{ + Verb = "Install"; + Noun = "AzSKOMSSolution"; + ShortName = "IOM"; + }, + + # FixControl + [CommandDetails]@{ + Verb = "Repair"; + Noun = "AzSKAzureServicesSecurity"; + ShortName = "RRS"; + }, + [CommandDetails]@{ + Verb = "Repair"; + Noun = "AzSKSubscriptionSecurity"; + ShortName = "RASS"; + }, + + # Policy Store + [CommandDetails]@{ + Verb = "Install"; + Noun = "AzSKOrganizationPolicy"; + ShortName = "IOP"; + IsLatestRequired = $false; + }, + [CommandDetails]@{ + Verb = "Update"; + Noun = "AzSKOrganizationPolicy"; + ShortName = "UOP"; + IsLatestRequired = $false; + }, + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKOrganizationPolicyStats"; + ShortName = "GOP"; + IsLatestRequired = $false; + }, + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKInfo"; + ShortName = "GAI"; + IsLatestRequired = $false; + }, + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKSecurityRecommendationReport"; + ShortName = "GAR"; + IsLatestRequired = $false; + }, + [CommandDetails]@{ + Verb = "Clear"; + Noun = "AzSKSessionState"; + ShortName = "CSS"; + IsLatestRequired = $false; + }, + # Update-PersistedState + + [CommandDetails]@{ + Verb = "Update"; + Noun = "AzSKPersistedState"; + ShortName = "UPS"; + IsLatestRequired = $false; + }, + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKAccessToken"; + ShortName = "GAT"; + }, + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKSupportedResourceTypes"; + ShortName = "GSRT"; + }, + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKPolicySettings"; + ShortName = "SPS"; + }, + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKOMSSettings"; + ShortName = "SOS"; + }, + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKEventHubSettings"; + ShortName = "SEHS"; + }, + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKWebhookSettings"; + ShortName = "SWHS"; + }, + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKUsageTelemetryLevel"; + ShortName = "SUTL"; + }, + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKLocalAIOrgTelemetrySettings"; + ShortName = "SLOTS"; + }, + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKUserPreference"; + ShortName = "SUP"; + }, + [CommandDetails]@{ + Verb = "Set"; + Noun = "AzSKPrivacyNoticeResponse"; + ShortName = "SPNR"; + }, + [CommandDetails]@{ + Verb = "Send"; + Noun = "AzSKInternalData"; + ShortName = "SID"; + }, + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKARMTemplateSecurityStatus"; + ShortName = "GATS"; + }, + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKAzureDevOpsSecurityStatus"; + ShortName = "GADS"; + CommandType = [CommandType]::AzureDevOps; + }, + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKAADTenantSecurityStatus"; + ShortName = "GTS"; + }, + [CommandDetails]@{ + Verb = "Get"; + Noun = "AzSKAADUserSecurityStatus"; + ShortName = "GUS"; + } + ); + + static BeginCommand([InvocationInfo] $invocationContext) + { + # Validate Command Prerequisites like AzureRM multiple version load issue + [CommandHelper]::CheckCommandPrerequisites($invocationContext); + [CommandHelper]::SetAzSKModuleName($invocationContext); + [CommandHelper]::SetCurrentAzSKModuleVersion($invocationContext); + # display warning if alias + [CommandHelper]::CheckForAlias($invocationContext.InvocationName) + } + + static CheckCommandPrerequisites([InvocationInfo] $invocationContext) + { + # Validate required module version dependency + try + { + #Loop through all required modules list + $invocationContext.MyCommand.Module.RequiredModules | ForEach-Object { + $requiredModule = $_ + $moduleList = Get-Module $requiredModule.Name + #Get list of other than required version is loaded into session + $otherThanRequiredModule = @(); + $otherThanRequiredModule += $moduleList | Where-Object { $_.Version -ne $requiredModule.Version} + if($otherThanRequiredModule.Count -gt 0 ) + { + #Display warning + $loadedVersions = @(); + $moduleList | ForEach-Object { + $loadedVersions += $_.Version.ToString() + }; + Write-Host "WARNING: Found multiple versions of Azure PowerShell ($($requiredModule.Name)) modules loaded in the session. ($($requiredModule.Name) versions found: $([string]::Join(", ", $loadedVersions)))" -ForegroundColor Yellow + Write-Host "WARNING: This will lead to issues when running AzSK cmdlets." -ForegroundColor Yellow + Write-Host 'Recommendation: Please start a fresh PowerShell session and run "Import-Module AzSK" first to avoid getting into this situation.' -ForegroundColor Yellow + } + else + { + # Continue execution without any error or warning + Write-Debug ($requiredModule.Name + " module version dependency validation successful") + } + }; + } + catch + { + Write-Debug "Not able to validate version dependency $_" + } + #check if old and new both modules are loaded in same session + $newModule = Get-Module|Where-Object {$_.Name -like "$([Constants]::NewModuleName)*"} | Select-Object -First 1 + $oldModule = Get-Module|Where-Object {$_.Name -like "$([Constants]::OldModuleName)*"} | Select-Object -First 1 + if($newModule -and $oldModule) + { + $warningMsg = [String]::Format([Constants]::MultipleModulesWarning, $newModule.Name,$oldModule.Name,$newModule.Name) + Write-Host $warningMsg -ForegroundColor Yellow + #stop execution + throw ([SuppressedException]::new("",[SuppressedExceptionType]::Generic)) + } + else + { + # Continue execution without any error or warning + Write-Debug ("Multiple modules ($([Constants]::NewModuleName) and $([Constants]::OldModuleName)) load validation successful") + } + + } + + static [void] SetAzSKModuleName([InvocationInfo] $invocationContext) + { + if($invocationContext) + { + [Constants]::SetAzSKModuleName($invocationContext.MyCommand.Module.Name); + } + } + static [void] SetCurrentAzSKModuleVersion([InvocationInfo] $invocationContext) + { + if($invocationContext) + { + [Constants]::SetAzSKCurrentModuleVersion($invocationContext.MyCommand.Version); + } + } + + static [void] CheckForAlias([string] $methodName) + { + if($methodName -like "*-$([Constants]::OldModuleName)*") + { + #get correct casing + $methodName = get-command -Name $methodName + $newMethodName = $methodName -replace ("-"+[Constants]::OldModuleName),("-"+[Constants]::NewModuleName); + $CommandNameChangeWarning = [Constants]::CommandNameChangeWarning -f $methodName,$newMethodName + Write-Warning $CommandNameChangeWarning + } + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/ConfigOverride.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/ConfigOverride.ps1 new file mode 100644 index 000000000..6e6988051 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/ConfigOverride.ps1 @@ -0,0 +1,122 @@ +Set-StrictMode -Version Latest + +class ConfigOverride +{ + hidden [string] $ConfigFileName; + [PSObject] $ParsedFile; + hidden [string[]] $ChangedProperties = @(); + + ConfigOverride([string] $configFileName) + { + if([string]::IsNullOrWhiteSpace($configFileName)) + { + throw [System.ArgumentException] ("The argument 'configFileName' is null or empty") + } + + $this.ConfigFileName = $configFileName; + $this.ParsedFile = [ConfigurationHelper]::LoadModuleJsonFile($configFileName); + + if(-not $this.ParsedFile) + { + throw [System.ArgumentException] ("The file '$configFileName' is empty") + } + } + + ConfigOverride([string] $FolderPath, [string] $fileName) + { + if([string]::IsNullOrWhiteSpace($fileName)) + { + throw [System.ArgumentException] ("The argument 'configFileName' is null or empty") + } + + $this.ConfigFileName = $fileName; + #Load file from AzSK App folder + $rootConfigPath = $FolderPath ; + $extension = [System.IO.Path]::GetExtension($fileName); + + $filePath = $null + if(Test-Path -Path $rootConfigPath) + { + $filePath = (Get-ChildItem $rootConfigPath -Name -Recurse -Include $fileName) | Select-Object -First 1 + } + + if ($filePath) { + if($extension -eq ".json") + { + $this.ParsedFile = (Get-Content -Raw -Path ($rootConfigPath + $filePath)) | ConvertFrom-Json + } + else + { + $this.ParsedFile = (Get-Content -Raw -Path ($rootConfigPath + $filePath)) + } + } + else { + throw "Unable to find the specified file '$fileName'" + } + } + + [bool] UpdatePropertyValue([string] $propertyName, [PSObject] $propertyValue) + { + if([string]::IsNullOrWhiteSpace($propertyName)) + { + throw [System.ArgumentException] ("The argument 'propertyName' is null or empty") + } + + #if(-not $propertyValue) + #{ + # throw [System.ArgumentException] ("The argument 'propertyValue' is null or empty") + #} + + if([Helpers]::CheckMember($this.ParsedFile, $propertyName, $false)) + { + $this.ParsedFile.$propertyName = $propertyValue; + $this.ChangedProperties += $propertyName; + return $true; + } + else + { + $this.ParsedFile | Add-Member -Type NoteProperty -Name $propertyName -Value $propertyValue + $this.ChangedProperties += $propertyName; + return $true; + } + + return $false; + } + + [void] WriteToFolder() + { + $this.WriteToFolder([Constants]::AzSKAppFolderPath + "\Temp\PolicySetup"); + } + + [void] WriteToFolder([string] $folderName) + { + if([string]::IsNullOrWhiteSpace($folderName)) + { + throw [System.ArgumentException] ("The argument 'folderName' is null or empty") + } + + if (-not (Test-Path $folderName)) + { + mkdir -Path $folderName -ErrorAction Stop | Out-Null + } + + if (-not $folderName.EndsWith("\")) + { + $folderName += "\"; + } + + [Helpers]::ConvertToJsonCustom(($this.ParsedFile | Select-Object -Property $this.ChangedProperties)) | Out-File -Force -FilePath ($folderName + $this.ConfigFileName) -Encoding utf8 + } + + [void] static ClearConfigInstance() + { + [AzSKSettings]::Instance = $null + [AzSKConfig]::Instance = $null + [ConfigurationHelper]::ServerConfigMetadata = $null + [ConfigurationHelper]::OfflineMode = $false + [ConfigurationHelper]::ConfigVersion = $null + [ConfigurationHelper]::IsIssueLogged = $false + [ConfigurationHelper]::LocalPolicyEnabled = $false + [Helpers]::currentRMContext = $null + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/ConfigurationHelper.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/ConfigurationHelper.ps1 new file mode 100644 index 000000000..910934c92 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/ConfigurationHelper.ps1 @@ -0,0 +1,396 @@ +Set-StrictMode -Version Latest +# +# ConfigurationHelper.ps1 +# +class ConfigurationHelper { + hidden static [bool] $IsIssueLogged = $false + hidden static [PSObject] $ServerConfigMetadata = $null + hidden static [bool] $OfflineMode = $false; + hidden static [string] $ConfigVersion ="" + hidden static [bool] $LocalPolicyEnabled= $false + hidden static [string] $ConfigPath = [string]::Empty + hidden static [PSObject] LoadOfflineConfigFile([string] $fileName) + { + return [ConfigurationHelper]::LoadOfflineConfigFile($fileName, $true); + } + hidden static [PSObject] LoadOfflineConfigFile([string] $fileName, [bool] $parseJson) { + $rootConfigPath = [Constants]::AzSKAppFolderPath + "\" ; + return [ConfigurationHelper]::LoadOfflineConfigFile($fileName, $true,$rootConfigPath); + } + hidden static [PSObject] LoadOfflineConfigFile([string] $fileName, [bool] $parseJson, $path) { + #Load file from AzSK App folder + $rootConfigPath = $path + "\" ; + + $extension = [System.IO.Path]::GetExtension($fileName); + + $filePath = $null + if(Test-Path -Path $rootConfigPath) + { + $filePath = (Get-ChildItem $rootConfigPath -Name -Recurse -Include $fileName) | Select-Object -First 1 + } + #If file not present in App folder load settings from Configurations in Module folder + if (!$filePath) { + $rootConfigPath = (Get-Item $PSScriptRoot).Parent.FullName + "\Configurations\"; + $filePath = (Get-ChildItem $rootConfigPath -Name -Recurse -Include $fileName) | Select-Object -First 1 + } + + if ($filePath) + { + if($parseJson) + { + if($extension -eq ".json" -or $extension -eq ".omsview") + { + $fileContent = (Get-Content -Raw -Path ($rootConfigPath + $filePath)) | ConvertFrom-Json + } + else + { + $fileContent = (Get-Content -Raw -Path ($rootConfigPath + $filePath)) + } + } + else + { + $fileContent = (Get-Content -Raw -Path ($rootConfigPath + $filePath)) + } + } + else { + throw "Unable to find the specified file '$fileName'" + } + if (-not $fileContent) { + throw "The specified file '$fileName' is empty" + } + + return $fileContent; + } + + hidden static [PSObject] LoadServerConfigFile([string] $policyFileName, [bool] $useOnlinePolicyStore, [string] $onlineStoreUri, [bool] $enableAADAuthForOnlinePolicyStore) { + [PSObject] $fileContent = ""; + if ([string]::IsNullOrWhiteSpace($policyFileName)) { + throw [System.ArgumentException] ("The argument 'policyFileName' is null"); + } + + if ($useOnlinePolicyStore) { + + if ([string]::IsNullOrWhiteSpace($onlineStoreUri)) + { + throw [System.ArgumentException] ("The argument 'onlineStoreUri' is null"); + } + + if($policyFileName -eq [Constants]::ServerConfigMetadataFileName -and $null -ne [ConfigurationHelper]::ServerConfigMetadata) + { + return [ConfigurationHelper]::ServerConfigMetadata; + } + #First load offline OSS Content + #TODO-Perf: should we cache indiv. local files (and even server downloaded ones) for session-scope? + $fileContent = [ConfigurationHelper]::LoadOfflineConfigFile($policyFileName) + + #Check if policy present in server using metadata file + if(-not [ConfigurationHelper]::OfflineMode -and [ConfigurationHelper]::IsPolicyPresentOnServer($policyFileName,$useOnlinePolicyStore,$onlineStoreUri,$enableAADAuthForOnlinePolicyStore)) + { + try + { + if([String]::IsNullOrWhiteSpace([ConfigurationHelper]::ConfigVersion) -and -not [ConfigurationHelper]::LocalPolicyEnabled) + { + try + { + $Version = [System.Version] ($global:ExecutionContext.SessionState.Module.Version); + #Look for the policy-file under the current module-version subfolder at the policy-store-URL + $serverFileContent = [ConfigurationHelper]::InvokeControlsAPI($onlineStoreUri, $Version, $policyFileName, $enableAADAuthForOnlinePolicyStore); + [ConfigurationHelper]::ConfigVersion = $Version; + } + catch + { + try{ #BUGBUG/TODO: Need comments here!! + $Version = ([ConfigurationHelper]::LoadOfflineConfigFile("AzSK.json")).ConfigSchemaBaseVersion; + $serverFileContent = [ConfigurationHelper]::InvokeControlsAPI($onlineStoreUri, $Version, $policyFileName, $enableAADAuthForOnlinePolicyStore); + [ConfigurationHelper]::ConfigVersion = $Version; + } + catch{ + #BUGBUG: Does Test-Path support URLs? + #BUGBUG: Even if it did, this should this not be the *resolvedURL* as opposed to the raw one (which has placeholders) + if(Test-Path $onlineStoreUri) + { + [EventBase]::PublishGenericCustomMessage("Running Org-Policy from local policy store location: [$onlineStoreUri]", [MessageType]::Warning); + $serverFileContent = [ConfigurationHelper]::LoadOfflineConfigFile($policyFileName, $true, $onlineStoreUri) + [ConfigurationHelper]::LocalPolicyEnabled = $true + } + else { + throw $_ + } + } + } + } + elseif([ConfigurationHelper]::LocalPolicyEnabled) + { + $serverFileContent = [ConfigurationHelper]::LoadOfflineConfigFile($policyFileName, $true, $onlineStoreUri) + } + else + { + $Version = [ConfigurationHelper]::ConfigVersion ; + $serverFileContent = [ConfigurationHelper]::InvokeControlsAPI($onlineStoreUri, $Version, $policyFileName, $enableAADAuthForOnlinePolicyStore); + } + + #Completely override offline config if Server Override flag is enabled + if([ConfigurationHelper]::IsOverrideOfflineEnabled($policyFileName)) + { + $fileContent = $serverFileContent + } + else + { + $fileContent = [Helpers]::MergeObjects($fileContent,$serverFileContent) + } + } + catch + { + #Something is seriously wrong...switch to 'offlineMode' so we do not attempt further downloads from online-policy-url... + [ConfigurationHelper]::OfflineMode = $true; + + #Post this problem to log, only once. + if(-not [ConfigurationHelper]::IsIssueLogged) + { + if([Helpers]::CheckMember($_,"Exception.Response.StatusCode") -and $_.Exception.Response.StatusCode.ToString().ToLower() -eq "unauthorized") + { + #[EventBase]::PublishGenericCustomMessage(("Not able to fetch org-specific policy: $policyFileName. The server policy location may be incorrect or you may not have access to it."), [MessageType]::Warning); + [ConfigurationHelper]::IsIssueLogged = $true + } + elseif($policyFileName -eq [Constants]::ServerConfigMetadataFileName) + { + #[EventBase]::PublishGenericCustomMessage(("Not able to fetch org-specific policy. Validate if org policy URL is correct."), [MessageType]::Warning); + [ConfigurationHelper]::IsIssueLogged = $true + } + else + { + [EventBase]::PublishGenericCustomMessage(("Error while fetching the policy [$policyFileName] from online store. " + [Constants]::OfflineModeWarning), [MessageType]::Warning); + [EventBase]::PublishGenericException($_); + [ConfigurationHelper]::IsIssueLogged = $true + } + } + } + } + + if (-not $fileContent) { + #Fire special event to notify user about switching to offline policy + $fileContent = [ConfigurationHelper]::LoadOfflineConfigFile($policyFileName) + } + # return $updateResult + } + else { + [EventBase]::PublishGenericCustomMessage(([Constants]::OfflineModeWarning + " Policy: $policyFileName"), [MessageType]::Warning); + $fileContent = [ConfigurationHelper]::LoadOfflineConfigFile($policyFileName) + } + + if (-not $fileContent) { + throw "The specified file '$policyFileName' is empty" + } + + return $fileContent; + } + + hidden static [PSObject] LoadServerFileRaw([string] $fileName, [bool] $useOnlinePolicyStore, [string] $onlineStoreUri, [bool] $enableAADAuthForOnlinePolicyStore) + { + [PSObject] $fileContent = ""; + if ([string]::IsNullOrWhiteSpace($fileName)) { + throw [System.ArgumentException] ("The argument 'fileName' is null"); + } + + if ($useOnlinePolicyStore) { + + if ([string]::IsNullOrWhiteSpace($onlineStoreUri)) + { + throw [System.ArgumentException] ("The argument 'onlineStoreUri' is null"); + } + + #Check if policy present in server using metadata file + if(-not [ConfigurationHelper]::OfflineMode -and [ConfigurationHelper]::IsPolicyPresentOnServer($fileName,$useOnlinePolicyStore,$onlineStoreUri,$enableAADAuthForOnlinePolicyStore)) + { + try + { + if([String]::IsNullOrWhiteSpace([ConfigurationHelper]::ConfigVersion)) + { + try + { + $Version = [System.Version] ($global:ExecutionContext.SessionState.Module.Version); + $serverFileContent = [ConfigurationHelper]::InvokeControlsAPI($onlineStoreUri, $Version, $fileName, $enableAADAuthForOnlinePolicyStore); + [ConfigurationHelper]::ConfigVersion = $Version; + } + catch + { + $Version = ([ConfigurationHelper]::LoadOfflineConfigFile("AzSK.json")).ConfigSchemaBaseVersion; + $serverFileContent = [ConfigurationHelper]::InvokeControlsAPI($onlineStoreUri, $Version, $fileName, $enableAADAuthForOnlinePolicyStore); + [ConfigurationHelper]::ConfigVersion = $Version; + } + } + else + { + $Version = [ConfigurationHelper]::ConfigVersion ; + $serverFileContent = [ConfigurationHelper]::InvokeControlsAPI($onlineStoreUri, $Version, $fileName, $enableAADAuthForOnlinePolicyStore); + } + + $fileContent = $serverFileContent + } + catch + { + [ConfigurationHelper]::OfflineMode = $true; + + if(-not [ConfigurationHelper]::IsIssueLogged) + { + if([Helpers]::CheckMember($_,"Exception.Response.StatusCode") -and $_.Exception.Response.StatusCode.ToString().ToLower() -eq "unauthorized") + { + [EventBase]::PublishGenericCustomMessage(("Not able to fetch org-specific policy. The current Azure subscription is not linked to your org tenant."), [MessageType]::Warning); + [ConfigurationHelper]::IsIssueLogged = $true + } + elseif($fileName -eq [Constants]::ServerConfigMetadataFileName) + { + [EventBase]::PublishGenericCustomMessage(("Not able to fetch org-specific policy. Validate if org policy URL is correct."), [MessageType]::Warning); + [ConfigurationHelper]::IsIssueLogged = $true + } + else + { + [EventBase]::PublishGenericCustomMessage(("Error while fetching the policy [$fileName] from online store. " + [Constants]::OfflineModeWarning), [MessageType]::Warning); + [EventBase]::PublishGenericException($_); + [ConfigurationHelper]::IsIssueLogged = $true + } + } + } + + + } + + } + else { + [EventBase]::PublishGenericCustomMessage(([Constants]::OfflineModeWarning + " Policy: $fileName"), [MessageType]::Warning); + } + + return $fileContent; + } + + hidden static [PSObject] InvokeControlsAPI([string] $onlineStoreUri,[string] $configVersion, [string] $policyFileName, [bool] $enableAADAuthForOnlinePolicyStore) + { + #Evaluate all code block in onlineStoreUri. + #Can use '$FileName' in uri to fill dynamic file name. + #Revisit + $FileName = $policyFileName; + $Version = $configVersion; + $uri = $global:ExecutionContext.InvokeCommand.ExpandString($onlineStoreUri) + [System.Uri] $validatedUri = $null; + $ResourceAppIdURI = "https://management.core.windows.net/" + #TODO/BUGBUG: Need to tweak this for AAD scans. Would we need a serverconfig? + if([System.Uri]::TryCreate($uri, [System.UriKind]::Absolute, [ref] $validatedUri)) + { + if($enableAADAuthForOnlinePolicyStore) + { + $rmContext = [AccountHelper]::GetCurrentRmContext(); + if(-not [string]::IsNullOrWhiteSpace($rmContext.Environment.Name) -and $rmContext.Environment.Name -ne [Constants]::DefaultAzureEnvironment) + { + $ResourceAppIdURI = $rmContext.Environment.ServiceManagementUrl + } + else { + $ResourceAppIdURI = "https://management.core.windows.net/" + } + $accessToken = [AccountHelper]::GetAccessToken($ResourceAppIdURI) + $serverFileContent = Invoke-RestMethod ` + -Method GET ` + -Uri $validatedUri ` + -Headers @{"Authorization" = "Bearer $accessToken"} ` + -UseBasicParsing + return $serverFileContent + } + else + { + $serverFileContent = Invoke-RestMethod ` + -Method GET ` + -Uri $validatedUri ` + -UseBasicParsing + return $serverFileContent + } + } + else + { + [EventBase]::PublishGenericCustomMessage(("'UseOnlinePolicyStore' is enabled but the 'OnlinePolicyStoreUrl' is not valid Uri: [$uri]. `r`n" + [Constants]::OfflineModeWarning), [MessageType]::Warning); + [ConfigurationHelper]::OfflineMode = $true; + } + return $null; + } + + #Need to rethink on this function logic + hidden static [PSObject] LoadModuleJsonFile([string] $fileName) { + + $rootConfigPath = (Get-Item $PSScriptRoot).Parent.FullName + "\Configurations\"; + $filePath = (Get-ChildItem $rootConfigPath -Name -Recurse -Include $fileName) | Select-Object -First 1 + if ($filePath) { + $fileContent = (Get-Content -Raw -Path ($rootConfigPath + $filePath)) | ConvertFrom-Json + } + else { + throw "Unable to find the specified file '$fileName'" + } + return $fileContent; + } + + hidden static [PSObject] LoadModuleRawFile([string] $fileName) { + + $rootConfigPath = (Get-Item $PSScriptRoot).Parent.FullName + "\Configurations\"; + $filePath = (Get-ChildItem $rootConfigPath -Name -Recurse -Include $fileName) | Select-Object -First 1 + if ($filePath) { + $fileContent = (Get-Content -Raw -Path ($rootConfigPath + $filePath)) + } + else { + throw "Unable to find the specified file '$fileName'" + } + return $fileContent; + } + + hidden static [bool] IsPolicyPresentOnServer([string] $fileName, [bool] $useOnlinePolicyStore, [string] $onlineStoreUri, [bool] $enableAADAuthForOnlinePolicyStore) + { + #Check if Config meta data is null and load the meta data from server + if($null -eq [ConfigurationHelper]::ServerConfigMetadata) + { + #If file being sought is the server-config-metadata file then return $true, this is to break the chicken-and-egg for 'ServerConfigMetadata.json' + #This means that we will *always* attempt to download *at least* 'ServerConfigMetadata.json' from server if not anything else. + if($fileName -eq [Constants]::ServerConfigMetadataFileName) + { + return $true + } + else + { + $filecontent = [ConfigurationHelper]::LoadServerConfigFile([Constants]::ServerConfigMetadataFileName, $useOnlinePolicyStore, $onlineStoreUri, $enableAADAuthForOnlinePolicyStore); + [ConfigurationHelper]::ServerConfigMetadata = $filecontent; + } + } + + if($null -ne [ConfigurationHelper]::ServerConfigMetadata) + { + if([ConfigurationHelper]::ServerConfigMetadata.OnlinePolicyList | Where-Object { $_.Name -eq $fileName}) + { + return $true + } + else + { + return $false + } + } + else + { + #If Metadata file is not present on server then set offline default meta data.. + [ConfigurationHelper]::ServerConfigMetadata = [ConfigurationHelper]::LoadOfflineConfigFile([Constants]::ServerConfigMetadataFileName); + return $false + } + } + + #Function to check if Override Offline flag is enabled + hidden static [bool] IsOverrideOfflineEnabled([string] $fileName) + { + if($fileName -eq [Constants]::ServerConfigMetadataFileName) + { + return $true + } + + $PolicyMetadata = [ConfigurationHelper]::ServerConfigMetadata.OnlinePolicyList | Where-Object { $_.Name -eq $fileName} + if(($PolicyMetadata -and [Helpers]::CheckMember($PolicyMetadata,"OverrideOffline") -and $PolicyMetadata.OverrideOffline -eq $true) ) + { + return $true + } + else + { + return $false + } + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/Constants.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/Constants.ps1 new file mode 100644 index 000000000..3d8338362 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/Constants.ps1 @@ -0,0 +1,196 @@ +Set-StrictMode -Version Latest +class Constants +{ + #All constant used across all modules Defined Here. + static [string] $DoubleDashLine = "================================================================================" + static [string] $HashLine = "################################################################################" + static [string] $GTLine = ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" + static [string] $SingleDashLine = "--------------------------------------------------------------------------------" + static [string] $UnderScoreLineLine= "________________________________________________________________________________" + static [string] $RemediationMsg = "** Next steps **`r`n" + +"Look at the individual control evaluation status in the CSV file.`r`n" + +" a) If the control has passed, no action is necessary.`r`n" + +" b) If the control has failed, look at the control evaluation detail in the LOG file to understand why.`r`n" + +" c) If the control status says 'Verify', it means that human judgement is required to determine the final control status. Look at the control evaluation output in the LOG file to make a determination.`r`n" + +" d) If the control status says 'Manual', it means that AzSK (currently) does not cover the control via automation OR AzSK is not able to fetch the data. You need to manually implement/verify it.`r`n" + +"`r`nNote: The 'Recommendation' column in the CSV file provides basic (generic) guidance that can help you fix a failed control. You can also use standard Azure product documentation. You should carefully consider the implications of making the required change in the context of your application. `r`n" + + static [string] $RemediationMsgForARMChekcer = "** Next steps **`r`n" + +"Look at the individual control evaluation status in the CSV file.`r`n" + +" a) If the control has passed, no action is necessary.`r`n" + +" b) If the control has failed, look at the control evaluation detail in the CSV file (LineNumber, ExpectedValue, CurrentValue, etc.) and fix the issue.`r`n" + +" c) If the control status says 'Skipped', it means that you have chosen to skip certain controls using the '-SkipControlsFromFile' parameter.`r`n" + + + static [string] $DefaultInfoCmdMsg = "This command provides overall information about different components of the AzSK which includes subscription information, security controls information, attestation information, host information. 'Get-AzSKInfo' command can be used with 'InfoType' parameter to fetch information.`r`n" + + "`r`nFollowing InfoType parameter values are currently supported by Get-AzSKInfo cmdlet.`r`n" + + "`tSubscriptionInfo : To get version details about different component of AzSK configured in Subscription.`r`n" + + "`tControlInfo : To get baseline, severity, description, rationale etc information about security controls.`r`n" + + "`tAttestationInfo : To get statistics, attestation justification, expiry etc information about controls attestation.`r`n" + + "`tHostInfo : To get information about machine details.`r`n" + + "`r`n`r`nExamples:`r`n" + + "`tGet-AzSKInfo -InfoType SubscriptionInfo -tenantId `r`n" + + "`tGet-AzSKInfo -InfoType ControlInfo -ResourceTypeName All -UseBaselineControls `r`n" + + "`tGet-AzSKInfo -InfoType AttestationInfo -tenantId -ResourceTypeName All -UseBaselineControls `r`n" + + "`tGet-AzSKInfo -InfoType HostInfo `r`n"; + + static [string] $DefaultControlInfoCmdMsg = "Run 'Get-AzSKInfo' command with below combination of parameter to get information about Azure services security control(s).`r`n`r`n" + + " All controls : Get-AzSKInfo -InfoType ControlInfo `r`n" + + " Baseline controls information : Get-AzSKInfo -InfoType ControlInfo -UseBaselineControls `r`n" + + " Controls for specific resource type : Get-AzSKInfo -InfoType ControlInfo -ResourceTypeName AppService `r`n" + + " Controls with specific severity : Get-AzSKInfo -InfoType ControlInfo -ControlSeverity 'High' `r`n" + + " Controls with specific tag(s) : Get-AzSKInfo -InfoType ControlInfo -FilterTags 'Automated, FunctionApp' `r`n" + + " Controls with specific keyword : Get-AzSKInfo -InfoType ControlInfo -ControlIdContains 'AppService_AuthZ_' `r`n" + + " Control(s) with specific controlId(s) : Get-AzSKInfo -InfoType ControlInfo -ResourceTypeName AppService -ControlIds 'Azure_AppService_AuthZ_Grant_Min_RBAC_Access, Azure_AppService_DP_Use_CNAME_With_SSL' `r`n" + + " Get information on PS console : Use any of above command with additional -Verbose argument`r`n"; + + static [string] $OfflineModeWarning = "Running in offline policy mode. Commands will run against local JSON files!" + #Constants for AzSKConfig + static [string] $AutomationAccount = "AzSKContinuousAssurance" + static [string] $RunbookName = "Continuous_Assurance_Runbook" + static [string] $ScheduleName = "CA_Scan_Schedule" + static [string] $connectionAssetName = "AzureRunAsConnection" + #static [string] $AzSKRGName = "AzSKRG" + static [string] $SupportDL = "azsksupext@microsoft.com" + static [string] $CICDShortLink = "https://aka.ms/devopskit/cicd" + + #Constants for SVTs + static [string] $ModuleStartHeading = [Constants]::DoubleDashLine + + "`r`nStarting analysis: [FeatureName: {0}] [ResourceGroupName: {1}] [ResourceName: {2}] `r`n" + [Constants]::SingleDashLine + static [string] $ModuleStartHeadingSub = [Constants]::DoubleDashLine + + "`r`nStarting analysis: [FeatureName: {0}] [TenantName: {1}] [tenantId: {2}] `r`n" + [Constants]::SingleDashLine + static [string] $AnalysingControlHeading = "Checking: [{0}]-[{1}]" + static [string] $AnalysingControlHeadingSub = "Checking: [{0}]-[{1}]" + static [string] $CompletedAnalysis = [Constants]::SingleDashLine + "`r`nCompleted analysis: [FeatureName: {0}] [ResourceGroupName: {1}] [ResourceName: {2}] `r`n" + [Constants]::DoubleDashLine + static [string] $CompletedAnalysisSub = [Constants]::SingleDashLine + "`r`nCompleted analysis: [FeatureName: {0}] [TenantName: {1}] [tenantId: {2}] `r`n" + [Constants]::DoubleDashLine + static [string] $PIMAPIUri="https://api.azrbac.mspim.azure.com/api/v2/privilegedAccess/azureResources/resources"; + #Constants for Attestation + static [string] $ModuleAttestStartHeading = [Constants]::DoubleDashLine + + "`r`nInfo: Starting attestation [{3}/{4}]- [FeatureName: {0}] [ResourceGroupName: {1}] [ResourceName: {2}] `r`n" + [Constants]::SingleDashLine + static [string] $ModuleAttestStartHeadingSub = [Constants]::DoubleDashLine + + "`r`nInfo: Starting attestation - [FeatureName: {0}] [TenantName: {1}] [tenantId: {2}] `r`n" + [Constants]::SingleDashLine + static [string] $CompletedAttestAnalysis = [Constants]::SingleDashLine + "`r`nCompleted attestation: [FeatureName: {0}] [ResourceGroupName: {1}] [ResourceName: {2}] `r`n" + [Constants]::DoubleDashLine + static [string] $CompletedAttestAnalysisSub = [Constants]::SingleDashLine + "`r`nCompleted attestation: [FeatureName: {0}] [TenantName: {1}] [tenantId: {2}] `r`n" + [Constants]::DoubleDashLine + static [System.Version] $AzSKCurrentModuleVersion=[System.Version]::new() + static [string] $AzSKModuleName = "AzSK"; + static [string] $AttestationDataContainerName = "attestation-data" + static [string] $CAMultiSubScanConfigContainerName = "ca-multisubscan-config" + static [string] $CAScanProgressSnapshotsContainerName = "ca-scan-checkpoints" + static [string] $CAScanOutputLogsContainerName= "ca-scan-logs" + static [string] $ResourceScanTrackerBlobName = "ResourceScanTracker.json" + static [string] $ResourceScanTrackerCMBlobName = "ResourceScanTracker_CentralMode.json" + static [hashtable] $AttestationStatusHashMap = @{ + [AttestationStatus]::NotAnIssue ="1"; + [AttestationStatus]::WillNotFix ="2"; + [AttestationStatus]::WillFixLater ="3"; + [AttestationStatus]::NotApplicable ="4"; + [AttestationStatus]::StateConfirmed ="5"; + } + + static [string] $StorageAccountPreName= "azsk" + static [string] $AzSKAppFolderPath = $Env:LOCALAPPDATA + "\Microsoft\" + [Constants]::AzSKModuleName + static [string] $AzSKLogFolderPath = $Env:LOCALAPPDATA + "\Microsoft\" + static [string] $AzSKTempFolderPath = $env:TEMP + "\" + [Constants]::AzSKModuleName + "\" + static [string] $AzSKExtensionsFolderPath = $Env:LOCALAPPDATA + "\Microsoft\" + [Constants]::AzSKModuleName + "\Extensions" + static [string] $ARMManagementUri = "https://management.azure.com/"; + static [string] $VersionCheckMessage = "A newer version of AzSK is available: Version {0} `r`nTo update, run the command below in a fresh PS window:`r`n" ; + static [string] $VersionWarningMessage = ("Using the latest version ensures that AzSK security commands you run use the latest, most up-to-date controls. `r`nResults from the current version should not be considered towards compliance requirements.`r`n" + [Constants]::DoubleDashLine); + static [string] $UsageTelemetryKey = "cf4c5e1a-d68d-4ea1-9901-37b67f58a192"; + static [string] $AzSKRGLocation = "eastus2"; + static [string] $OMSRequestURI = "https://management.azure.com/{0}?api-version=2015-03-20"; + static [string] $NewStorageSku = "Standard_LRS"; + static [string] $NewStorageKind = "BlobStorage"; + static [string] $ARMControlsFileURI = "https://azsdkossep.azureedge.net/1.0.0/ARMControls.json"; + static [string] $RecommendationURI = "https://azsdkossep.azureedge.net/recmnds/r.json "; + static [string] $AttestationReadMsg = "`r`nControl results may not reflect attestation if you do not have permissions to read attestation data from " + #V1 alert RG name constant is temporary and added for backward compatibility + static [string] $AlertActionGroupName = "AzSKAlertActionGroup" + static [string] $CriticalAlertActionGroupName = "AzSKCriticalAlertActionGroup" + static [string] $ResourceDeploymentActionGroupName = "ResourceDeploymentActionGroup" + + # Append recommendation when control require elevated permission + static [string] $RequireOwnerPermMessage = "(The status for this control has been marked as 'Manual' because elevated (Co-Admin/Owner/Contributor) permission is required to check security configuration for this resource. You can re-run the control with the appropriate privilege.) " + static [string] $OwnerAccessTagName = "OwnerAccess" + + static [string] $BlanktenantId = "00000000-0000-0000-0000-000000000000" + static [string] $BlankSubscriptionName = "DevOpsKitForX" + static [string] $BlankScope = "/subscriptions/00000000-0000-0000-0000-000000000000"; + static [string] $DefaultAzureEnvironment = "AzureCloud"; + + static [string] $CentralRBACVersionTagName = "CentralRBACVersion" + static [string] $DeprecatedRBACVersionTagName = "DeprecatedRBACVersion" + static [string] $ARMPolicyConfigVersionTagName = "ARMPolicyConfigVersion" + static [string] $AzSKAlertsVersionTagName = "AzSKAlertsVersion" + static [string] $SecurityCenterConfigVersionTagName = "SecurityCenterConfigVersion" + static [string] $NoActionRequiredMessage ="No Action Required" + static [string] $PolicyMigrationTagName = "PolicyMigratedOn" + static [string] $AlertRunbookName= "Alert_Runbook" + static [string] $Alert_ResourceCreation_Runbook= "Continuous_Assurance_ScanOnTrigger_Runbook" + static [string] $AutomationWebhookName="WebhookForAlertRunbook" + static [string] $AutomationAccountName="AzSKContinuousAssurance" + static [int] $AlertWebhookUriExpiryInDays = 60 + + static [int] $DefaultControlExpiryInDays = 90 + static [int] $PartialScanMaxRetryCount = 3 + static [string] $NewModuleName = "AzSK" + static [string] $OldModuleName = "AzSDK" + + #CA variables names + static [string] $AppResourceGroupNames = "AppResourceGroupNames" + static [string] $ReportsStorageAccountName = "ReportsStorageAccountName" + static [string] $OMSWorkspaceId = "OMSWorkspaceId" + static [string] $OMSSharedKey = "OMSSharedKey" + static [string] $AltOMSWorkspaceId = "AltOMSWorkspaceId" + static [string] $AltOMSSharedKey = "AltOMSSharedKey" + static [string] $WebhookUrl = "WebhookUrl" + static [string] $WebhookAuthZHeaderName = "WebhookAuthZHeaderName" + static [string] $WebhookAuthZHeaderValue = "WebhookAuthZHeaderValue" + static [string] $DisableAlertRunbook = "DisableAlertRunbook" + static [string] $CATargetSubsBlobName= "TargetSubs.json" + static [string] $CoAdminElevatePermissionMsg = "(If you are 'Owner' then please elevate to 'Co-Admin' in the portal and re-run in a *fresh* PS console.)" + + static [string] $CommandNameChangeWarning = "The command {0} shall be renamed to {1} in a future release ('SDK' shall be replaced with 'SK')."; + static [string] $MultipleModulesWarning = "Found multiple modules ({0} and {1}) loaded in the PS session.`r`n"+ + "Stopping cmdlet execution.`r`n"+ + "Recommendation: Please start a fresh PS session and run 'Import-Module {2}' first to avoid getting into this situation.`r`n" + + #Constants for Org Policy + static [string] $OrgPolicyTagPrefix = "AzSKOrgName_" + static [int] $SASTokenExpiryReminderInDays = 30 + # Local Subscription Report Constants + #static [string] $ComplianceReportContainerName = "compliance-state" + static [string] $ComplianceReportTableName = "ComplianceState" + static [DateTime] $AzSKDefaultDateTime = '1900-01-01T00:00:00' + static [int] $ControlResultComplianceInDays = 3 + static [string] $ComplianceReportPath = [Constants]::AzSKAppFolderPath + "\TempState\ComplianceData" + + static [string] $ServerConfigMetadataFileName = "ServerConfigMetadata.json" + + #Constants for AzureDevOps + static [string] $DefaultClientId = "872cd9fa-d31f-45e0-9eab-6e460a02d1f1" + static [string] $DefaultReplyUri = "urn:ietf:wg:oauth:2.0:oob" + static [string] $DefaultAzureDevOpsResourceId = "499b84ac-1321-427f-aa17-267ca6975798" + + #Constants for AAD (#TODO) + static [string] $AADAPIGuid = "74658136-14ec-4630-ad9b-26e160ff0fc6" + static [string] $AADAPIUrl = "https://main.iam.ad.ext.azure.com" + static [string] $RegExForValidURL = "(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})" + + + static [void] SetAzSKModuleName($moduleName) + { + if(-not [string]::IsNullOrWhiteSpace($moduleName)) + { + [Constants]::AzSKModuleName = $moduleName.Replace("azsk","AzSK"); + [Constants]::AzSKAppFolderPath = $Env:LOCALAPPDATA + "\Microsoft\" + [Constants]::AzSKModuleName + [Constants]::AzSKTempFolderPath = $env:TEMP + "\" + [Constants]::AzSKModuleName + "\" + } + } + static [void] SetAzSKCurrentModuleVersion($moduleVersion) + { + if(-not [string]::IsNullOrWhiteSpace($moduleVersion)) + { + [Constants]::AzSKCurrentModuleVersion = $moduleVersion; + } + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/Helpers.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/Helpers.ps1 new file mode 100644 index 000000000..bd77e796a --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/Helpers.ps1 @@ -0,0 +1,1203 @@ +using namespace Newtonsoft.Json +using namespace Microsoft.Azure.Commands.Common.Authentication.Abstractions +using namespace Microsoft.Azure.Commands.Common.Authentication +using namespace Microsoft.Azure.Management.Storage.Models +using namespace Microsoft.IdentityModel.Clients.ActiveDirectory + +Set-StrictMode -Version Latest + +class Helpers { + + hidden static [PSObject] LoadOfflineConfigFile([string] $fileName, [bool] $parseJson) { + $rootConfigPath = [Constants]::AzSKAppFolderPath + "\" ; + return [Helpers]::LoadOfflineConfigFile($fileName, $true,$rootConfigPath); + } + + hidden static [PSObject] LoadOfflineConfigFile([string] $fileName, [bool] $parseJson, $path) { + #Load file from AzSK App folder + $rootConfigPath = $path + "\" ; + + $extension = [System.IO.Path]::GetExtension($fileName); + + $filePath = $null + if(Test-Path -Path $rootConfigPath) + { + $filePath = (Get-ChildItem $rootConfigPath -Name -Recurse -Include $fileName) | Select-Object -First 1 + } + #If file not present in App folder load settings from Configurations in Module folder + if (!$filePath) { + $rootConfigPath = (Get-Item $PSScriptRoot).Parent.FullName + "\Configurations\"; + $filePath = (Get-ChildItem $rootConfigPath -Name -Recurse -Include $fileName) | Select-Object -First 1 + } + + if ($filePath) + { + if($parseJson) + { + if($extension -eq ".json" -or $extension -eq ".omsview") + { + $fileContent = (Get-Content -Raw -Path ($rootConfigPath + $filePath)) | ConvertFrom-Json + } + else + { + $fileContent = (Get-Content -Raw -Path ($rootConfigPath + $filePath)) + } + } + else + { + $fileContent = (Get-Content -Raw -Path ($rootConfigPath + $filePath)) + } + } + else { + throw "Unable to find the specified file '$fileName'" + } + if (-not $fileContent) { + throw "The specified file '$fileName' is empty" + } + + return $fileContent; + } + + static AbstractClass($obj, $classType) { + $type = $obj.GetType() + if ($type -eq $classType) { + throw("Class '$type' must be inherited") + } + } + + static [string] SanitizeFolderName($folderPath) { + return ($folderPath -replace '[<>:"/\\\[\]|?*]', ''); + } + + static [string] ConvertObjectToString([PSObject] $dataObject, [bool] $defaultPsOutput) { + [string] $msg = ""; + if ($dataObject) { + if ($dataObject.GetType().FullName -eq "System.Management.Automation.ErrorRecord") { + if($dataObject.Exception -is [SuppressedException]) + { + $msg = $dataObject.Exception.ConvertToString(); + } + else + { + if ($defaultPsOutput) + { + $msg = $dataObject.ToString(); + } + else + { + $msg = ($dataObject | Out-String) + "`r`nStackTrace: " + $dataObject. ScriptStackTrace + } + } + } + else { + if ($defaultPsOutput -or $dataObject.GetType() -eq [string]) { + $msg = $dataObject | Out-String; + } + else { + try { + #$msg = $dataObject | ConvertTo-Json -Depth 5 | Out-String; + #$msg = [Helpers]::ConvertToJsonCustom($dataObject); + $msg = [Helpers]::ConvertToPson($dataObject); + } + catch { + $e = $_ + $msg = $dataObject | Format-List | Out-String; + } + + $msg = $msg.Trim(); + #$msg = $msg.TrimStart("`r`n"); + } + } + } + + return $msg.Trim("`r`n"); + } + + static [JsonSerializerSettings] $SerializerSettings = $null; + hidden static [JsonSerializerSettings] GetSerializerSettings() { + if (-not [Helpers]::SerializerSettings) { + $settings = [JsonSerializerSettings]::new(); + $settings.Converters.Add([Converters.StringEnumConverter]::new()); + $settings.Formatting = [Formatting]::Indented; + $settings.NullValueHandling = [NullValueHandling]::Ignore; + $settings.ReferenceLoopHandling = [ReferenceLoopHandling]::Ignore; + [Helpers]::SerializerSettings = $settings; + } + return [Helpers]::SerializerSettings; + } + + static [string] ConvertToJson([PSObject] $dataObject) { + if ($dataObject) { + if ($dataObject.GetType() -eq [System.Object[]] -and $dataObject.Count -ne 0) { + $list = New-Object -TypeName "System.Collections.Generic.List[$($dataObject[0].GetType().fullname)]"; + $dataObject | ForEach-Object { + if ($_) { + $list.Add($_); + } + } + return [JsonConvert]::SerializeObject($list, [Helpers]::GetSerializerSettings()); + } + + return [JsonConvert]::SerializeObject($dataObject, [Helpers]::GetSerializerSettings()); + } + return ""; + } + + static [string] ConvertToJsonCustom([PSObject] $Object, [Int]$Depth, [Int]$Layers) { + Set-StrictMode -Off + $res = [Helpers]::ConvertToJsonCustomNotStrict($Object, $Depth, $Layers, $false) + Set-StrictMode -Version Latest + return $res + } + + static [string] ConvertToJsonCustom([PSObject] $Object) { + return [Helpers]::ConvertToJsonCustom($Object, 10, 10); + } + + static [string] ConvertToJsonCustomCompressed([PSObject] $Object) { + Set-StrictMode -Off + $res = [Helpers]::ConvertToJsonCustomNotStrict($Object, 10, 0, $false) + Set-StrictMode -Version Latest + return $res + } + + static [string] ConvertToPson([PSObject] $Object) { + Set-StrictMode -Off + $res = [Helpers]::ConvertToPsonNotStrict($Object, 10, 10, $false, $false, (Get-Variable -Name PSVersionTable).Value.PSVersion) + Set-StrictMode -Version Latest + return $res + } + + static [string] ConvertToJsonCustomNotStrict([PSObject] $Object, [Int]$Depth, [Int]$Layers, [bool]$IsWind) { + $Format = $Null + $Quote = If ($Depth -le 0) {""} + Else {""""} + $Space = If ($Layers -le 0) {""} + Else {" "} + If ($null-eq $Object) { return "null"} + Else { + $JSON = If ($Object -is "Array") { + $Format = "[", ",$Space", "]" + If ($Depth -gt 1) { + For ($i = 0; $i -lt $Object.Count; $i++) { + [Helpers]::ConvertToJsonCustomNotStrict($Object[$i], $Depth - 1, $Layers - 1, $IsWind) + } + } + } + ElseIf ($Object -is "Xml") { + $String = New-Object System.IO.StringWriter + $Object.Save($String) + $Xml = "'" + ([String]$String).Replace("`'", "'") + "'" + If ($Layers -le 0) { + ($Xml -Replace "\r\n\s*", "") -Replace "\s+", " " + } + ElseIf ($Layers -eq 1) { + $Xml + } + Else { + $Xml.Replace("`r`n", "`r`n ") + } + $String.Dispose() + } + ElseIf ($Object -is "Enum") { + "$Quote$($Object.ToString())$Quote" + } + ElseIf ($Object -is "DateTime") { + "$Quote$($Object.ToString("o"))$Quote" + } + ElseIf ($Object -is "TimeSpan") { + "$Quote$($Object.ToString())$Quote" + } + ElseIf ($Object -is "String") { + $Object = ConvertTo-Json $Object -Depth 1 + "$Object" + } + ElseIf ($Object -is "Boolean") { + If ($Object) {"true"} + Else {"false"} + } + ElseIf ($Object -is "Char") { + "$Quote$Object$Quote" + } + ElseIf ($Object -is "guid") { + "$Quote$Object$Quote" + } + ElseIf ($Object -is "ValueType") { + $Object + } + ElseIf ($Object -is [System.Collections.IDictionary]) { + If ($null -eq $Object.Keys) { + return "null" + } + $Format = "{", ",$Space", "}" + If ($Depth -gt 1) { + $Object.GetEnumerator() | ForEach-Object { + $Quote + $_.Key + $Quote + "$Space`:$Space" + ([Helpers]::ConvertToJsonCustomNotStrict($_.Value, $Depth - 1, $Layers - 1, $IsWind)) + } + } + } + ElseIf ($Object -is 'System.Collections.IList') { + $Format = "[", ",$Space", "]" + If ($Depth -gt 1) { + $Object | ForEach-Object { + [Helpers]::ConvertToJsonCustomNotStrict($_, $Depth - 1, $Layers - 1, $IsWind) + } + } + } + ElseIf ($Object -is "Object") { + If ($Object -is "System.Management.Automation.ErrorRecord" -and !$IsWind) { + $Depth = 3 + $Layers = 3 + $IsWind = $true + } + $Format = "{", ",$Space", "}" + If ($Depth -gt 1) { + Get-Member -InputObject $Object -MemberType Properties | ForEach-Object { + $Quote + $_.Name + $Quote + "$Space`:$Space" + ([Helpers]::ConvertToJsonCustomNotStrict($Object.$($_.Name), $Depth - 1, $Layers - 1, $IsWind)) + } + } + } + Else {$Object} + If ($Format) { + $JSON = $Format[0] + (& { + If (($Layers -le 1) -or ($JSON.Count -le 0)) { + $JSON -Join $Format[1] + } + Else { + ("`r`n" + ($JSON -Join "$($Format[1])`r`n")).Replace("`r`n", "`r`n ") + "`r`n" + } + }) + $Format[2] + } + return "$JSON" + } + } + + + # Adapted from https://stackoverflow.com/questions/15139552/save-hash-table-in-powershell-object-notation-pson + # PSON - PowerShell Object Notation + static [string] ConvertToPsonNotStrict([PSObject] $Object, [Int]$Depth, [Int]$Layers, [bool]$IsWind, [bool]$Strict, [Version]$Version) { + $Format = $Null + $Quote = If ($Depth -le 0) {""} + Else {""""} + $Space = If ($Layers -le 0) {""} + Else {" "} + If ($null -eq $Object) { + return "`$Null" + } + Else { + $Type = "[" + $Object.GetType().Name + "]" + $PSON = If ($Object -is "Array") { + $Format = "@(", ",$Space", ")" + If ($Depth -gt 1) { + For ($i = 0; $i -lt $Object.Count; $i++) { + [Helpers]::ConvertToPsonNotStrict($Object[$i], $Depth - 1, $Layers - 1, $IsWind, $Strict, $Version) + } + } + } + ElseIf ($Object -is "Xml") { + $Type = "[Xml]" + $String = New-Object System.IO.StringWriter + $Object.Save($String) + $Xml = "'" + ([String]$String).Replace("`'", "'") + "'" + If ($Layers -le 0) { + ($Xml -Replace "\r\n\s*", "") -Replace "\s+", " " + } + ElseIf ($Layers -eq 1) { + $Xml + } + Else { + $Xml.Replace("`r`n", "`r`n ") + } + $String.Dispose() + } + ElseIf ($Object -is "Enum") { + "$Quote$($Object.ToString())$Quote" + } + ElseIf ($Object -is "DateTime") { + "$Quote$($Object.ToString('s'))$Quote" + } + ElseIf ($Object -is "TimeSpan") { + "$Quote$($Object.ToString())$Quote" + } + ElseIf ($Object -is "String") { + 0..11 | ForEach-Object { + $Object = $Object.Replace([String]"```'""`0`a`b`f`n`r`t`v`$"[$_], ('`' + '`''"0abfnrtv$'[$_]))}; "$Quote$Object$Quote" + } + ElseIf ($Object -is "Boolean") { + If ($Object) {"`$True"} + Else {"`$False"} + } + ElseIf ($Object -is "Char") { + If ($Strict) {[Int]$Object} + Else {"$Quote$Object$Quote"} + } + ElseIf ($Object -is "guid") { + "$Quote$Object$Quote" + } + ElseIf ($Object -is "ValueType") { + $Object + } + ElseIf ($Object -is [System.Collections.IDictionary]) { + If ($null -eq $Object.Keys) { + return "`$Null" + } + If ($Type -eq "[OrderedDictionary]") {$Type = "[Ordered]"} + $Format = "@{", ";$Space", "}" + If ($Depth -gt 1) { + $Object.GetEnumerator() | ForEach-Object { + $Quote + $_.Key + $Quote + "$Space=$Space" + ([Helpers]::ConvertToPsonNotStrict($_.Value, $Depth - 1, $Layers - 1, $IsWind, $Strict, $Version)) + } + } + } + ElseIf ($Object -is 'System.Collections.IList') { + $Format = "@(", ",$Space", ")" + If ($Depth -gt 1) { + $Object | ForEach-Object { + [Helpers]::ConvertToPsonNotStrict($_, $Depth - 1, $Layers - 1, $IsWind, $Strict, $Version) + } + } + } + ElseIf ($Object -is "Object") { + If ($Object -is "System.Management.Automation.ErrorRecord" -and !$IsWind) { + $Depth = 3 + $Layers = 3 + $IsWind = $true + } + If ($Version -le [Version]"2.0") {$Type = "New-Object PSObject -Property "} + $Format = "@{", ";$Space", "}" + If ($Depth -gt 1) { + Get-Member -InputObject $Object -MemberType Properties | ForEach-Object { + $Quote + $_.Name + $Quote + "$Space=$Space" + ([Helpers]::ConvertToPsonNotStrict($Object.$($_.Name), $Depth - 1, $Layers - 1, $IsWind, $Strict, $Version)) + } + } + } + Else {$Object} + If ($Format) { + $PSON = $Format[0] + (& { + If (($Layers -le 1) -or ($PSON.Count -le 0)) { + $PSON -Join $Format[1] + } + Else { + ("`r`n" + ($PSON -Join "$($Format[1])`r`n")).Replace("`r`n", "`r`n ") + "`r`n" + } + }) + $Format[2] + } + If ($Strict) { + return "$Type$PSON" + } + Else { + return "$PSON" + } + } + } + + static [bool] CompareObject($referenceObject, $differenceObject) { + return [Helpers]::CompareObject($referenceObject, $differenceObject, $false) + } + + static [bool] CompareObject($referenceObject, $differenceObject, [bool] $strictComparison) { + $result = $true; + + if ($null -ne $referenceObject) { + if ($null -ne $differenceObject) { + if ($referenceObject -is "Array") { + if ($differenceObject -is "Array") { + if ((-not $strictComparison) -or ($referenceObject.Count -eq $differenceObject.Count)) { + foreach ($refObject in $referenceObject) { + $arrayResult = $false; + foreach ($diffObject in $differenceObject) { + $arrayResult = [Helpers]::CompareObject($refObject, $diffObject, $strictComparison); + if ($arrayResult) { + break; + } + } + + $result = $result -and $arrayResult + if (-not $arrayResult) { + break; + } + } + } + else { + $result = $false; + } + } + else { + $result = $false; + } + } + # Condition for all primitive types + elseif ($referenceObject -is "string" -or $referenceObject -is "ValueType") { + # For primitive types, use default comparer + $result = $result -and (((Compare-Object $referenceObject $differenceObject) | Where-Object { $_.SideIndicator -eq "<=" } | Measure-Object).Count -eq 0) + + } + else { + $result = $result -and [Helpers]::CompareObjectProperties($referenceObject, $differenceObject, $strictComparison) + } + } + else { + $result = $false; + } + } + elseif ($null -eq $differenceObject) { + $result = $true; + } + else { + $result = $false; + } + + return $result; + } + + hidden static [bool] CompareObjectProperties($referenceObject, $differenceObject, [bool] $strictComparison) { + $result = $true; + $refProps = @(); + $diffProps = @(); + $refProps += [Helpers]::GetProperties($referenceObject); + $diffProps += [Helpers]::GetProperties($differenceObject); + + if ((-not $strictComparison) -or ($refProps.Count -eq $diffProps.Count)) { + foreach ($propName in $refProps) { + $refProp = $referenceObject.$propName; + + if (-not [string]::IsNullOrWhiteSpace(($diffProps | Where-Object { $_ -eq $propName } | Select-Object -First 1))) { + $compareProp = $differenceObject.$propName; + + if ($null -ne $refProp) { + if ($null -ne $compareProp) { + $result = $result -and [Helpers]::CompareObject($refProp, $compareProp, $strictComparison); + } + else { + $result = $result -and $false; + } + } + elseif ($null -eq $compareProp) { + $result = $result -and $true; + } + else { + $result = $result -and $false; + } + } + else { + $result = $false; + } + + if (-not $result) { + break; + } + } + } + else { + $result = $false; + } + + + return $result; + } + + static [bool] CompareObject($referenceObject, $differenceObject, [bool] $strictComparison,$AttestComparisionType) { + $result = $true; + + if ($null -ne $referenceObject) { + if ($null -ne $differenceObject) { + if ($referenceObject -is "Array") { + if ($differenceObject -is "Array") { + if ((-not $strictComparison) -or ($referenceObject.Count -eq $differenceObject.Count)) { + foreach ($refObject in $referenceObject) { + $arrayResult = $false; + foreach ($diffObject in $differenceObject) { + if($AttestComparisionType -eq [ComparisionType]::NumLesserOrEqual) + { + $arrayResult = [Helpers]::CompareObject($refObject, $diffObject, $strictComparison,$AttestComparisionType); + } + else + { + $arrayResult = [Helpers]::CompareObject($refObject, $diffObject, $strictComparison); + } + if ($arrayResult) { + break; + } + } + + $result = $result -and $arrayResult + if (-not $arrayResult) { + break; + } + } + } + else { + $result = $false; + } + } + else { + $result = $false; + } + } + # Condition for all primitive types + elseif ($referenceObject -is "string" -or $referenceObject -is "ValueType") { + # For primitive types, use default comparer + if($AttestComparisionType -eq [ComparisionType]::NumLesserOrEqual) + { + $result = $result -and ($referenceObject -ge $differenceObject) + } + else + { + $result = $result -and (((Compare-Object $referenceObject $differenceObject) | Where-Object { $_.SideIndicator -eq "<=" } | Measure-Object).Count -eq 0) + } + + } + else { + if($AttestComparisionType -eq [ComparisionType]::NumLesserOrEqual) + { + $result = $result -and [Helpers]::CompareObjectProperties($referenceObject, $differenceObject, $strictComparison,$AttestComparisionType) + } + else + { + $result = $result -and [Helpers]::CompareObjectProperties($referenceObject, $differenceObject, $strictComparison) + } + + } + } + else { + $result = $false; + } + } + elseif ($null -eq $differenceObject) { + $result = $true; + } + else { + $result = $false; + } + + return $result; + } + + hidden static [bool] CompareObjectProperties($referenceObject, $differenceObject, [bool] $strictComparison,$AttestComparisionType) { + $result = $true; + $refProps = @(); + $diffProps = @(); + $refProps += [Helpers]::GetProperties($referenceObject); + $diffProps += [Helpers]::GetProperties($differenceObject); + + if ((-not $strictComparison) -or ($refProps.Count -eq $diffProps.Count)) { + foreach ($propName in $refProps) { + $refProp = $referenceObject.$propName; + + if (-not [string]::IsNullOrWhiteSpace(($diffProps | Where-Object { $_ -eq $propName } | Select-Object -First 1))) { + $compareProp = $differenceObject.$propName; + + if ($null -ne $refProp) { + if ($null -ne $compareProp) { + if($AttestComparisionType -eq [ComparisionType]::NumLesserOrEqual) + { + $result = $result -and [Helpers]::CompareObject($refProp, $compareProp, $strictComparison,$AttestComparisionType); + } + else + { + $result = $result -and [Helpers]::CompareObject($refProp, $compareProp, $strictComparison); + } + + } + else { + $result = $result -and $false; + } + } + elseif ($null -eq $compareProp) { + $result = $result -and $true; + } + else { + $result = $result -and $false; + } + } + else { + $result = $false; + } + + if (-not $result) { + break; + } + } + } + else { + $result = $false; + } + + + return $result; + } + + static [string[]] GetProperties($object) { + $props = @(); + if($object) + { + if ($object -is "Hashtable") { + $object.Keys | ForEach-Object { + $props += $_; + }; + } + else { + ($object | Get-Member -MemberType Properties) | + ForEach-Object { + $props += $_.Name; + }; + } + } + return $props; + } + + static [bool] CompareObjectOld($referenceObject, $differenceObject) { + $result = $true; + + if ($null -ne $referenceObject) { + if ($null -ne $differenceObject) { + ($referenceObject | Get-Member -MemberType Properties) | + ForEach-Object { + $refProp = $referenceObject."$($_.Name)"; + + if ($differenceObject | Get-Member -Name $_.Name) { + $compareProp = $differenceObject."$($_.Name)"; + + if ($null -ne $refProp) { + if ($null -ne $compareProp) { + if ($refProp.GetType().Name -eq "PSCustomObject") { + $result = $result -and [Helpers]::CompareObjectOld($refProp, $compareProp); + } + else { + $result = $result -and (((Compare-Object $refProp $compareProp) | Where-Object { $_.SideIndicator -eq "<=" } | Measure-Object).Count -eq 0) + } + } + else { + $result = $result -and $false; + } + } + elseif ($null -eq $compareProp) { + $result = $result -and $true; + } + else { + $result = $result -and $false; + } + } + else { + $result = $false; + } + } + } + else { + $result = $false; + } + } + elseif ($null -eq $differenceObject) { + $result = $true; + } + else { + $result = $false; + } + + return $result; + } + + static [bool] CheckMember([PSObject] $refObject, [string] $memberPath) + { + return [Helpers]::CheckMember($refObject, $memberPath, $true); + } + + static [bool] CheckMember([PSObject] $refObject, [string] $memberPath, [bool] $checkNull) + { + [bool]$result = $false; + if ($refObject) { + $properties = @(); + $properties += $memberPath.Split("."); + + if ($properties.Count -gt 0) { + $currentItem = $properties.Get(0); + if (-not [string]::IsNullOrWhiteSpace($currentItem)) { + if ($refObject | Get-Member -Name $currentItem) + { + if ($properties.Count -gt 1) + { + if($refObject.$currentItem) + { + $result = $true; + $result = $result -and [Helpers]::CheckMember($refObject.$currentItem, [string]::Join(".", $properties[1..($properties.length - 1)])); + } + } + else + { + if($checkNull) + { + if($refObject.$currentItem) + { + $result = $true; + } + } + else + { + $result = $true; + } + } + } + } + } + } + return $result; + } + + static [PSObject] SelectMembers([PSObject] $refObject, [string[]] $memberPaths) { + $result = $null; + if ($null -ne $refObject) { + if ($refObject -is "Array") { + $result = @(); + $refObject | ForEach-Object { + $memberValue = [Helpers]::SelectMembers($_, $memberPaths); + if ($null -ne $memberValue) { + $result += $memberValue; + } + }; + } + else { + $processedMemberPaths = @(); + $objectProps = [Helpers]::GetProperties($refObject); + if ($objectProps.Count -ne 0 -and $null -ne $memberPaths -and $memberPaths.Count -ne 0) { + $memberPaths | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + ForEach-Object { + $splitPaths = @(); + $splitPaths += $_.Split("."); + $firstMemberPath = $splitPaths.Get(0); + if (-not [string]::IsNullOrWhiteSpace($firstMemberPath) -and $objectProps.Contains($firstMemberPath)) { + $pathObject = $processedMemberPaths | Where-Object { $_.MemberPath -eq $firstMemberPath } | Select-Object -First 1; + + if (-not $pathObject) { + $pathObject = @{ + MemberPath = $firstMemberPath; + ChildPaths = @(); + }; + $processedMemberPaths += $pathObject; + } + + # Count > 1 indicates that it has child path + if ($splitPaths.Count -gt 1) { + $pathObject.ChildPaths += [string]::Join(".", $splitPaths[1..($splitPaths.length - 1)]); + } + } + }; + } + + if ($processedMemberPaths.Count -ne 0) { + $processedMemberPaths | ForEach-Object { + $memberValue = $null; + + if ($_.ChildPaths.Count -eq 0) { + $memberValue = $refObject."$($_.MemberPath)"; + } + else { + $memberValue = [Helpers]::SelectMembers($refObject."$($_.MemberPath)", $_.ChildPaths); + } + + if ($null -ne $memberValue) { + if ($null -eq $result) { + $result = New-Object PSObject; + } + + $result | Add-Member -MemberType NoteProperty -Name ($_.MemberPath) -Value $memberValue; + } + }; + } + else { + $result = $refObject; + } + } + } + + return $result; + } + + static [string] ComputeHash([String] $data) { + $HashValue = [System.Text.StringBuilder]::new() + [System.Security.Cryptography.HashAlgorithm]::Create("SHA256").ComputeHash([System.Text.Encoding]::UTF8.GetBytes($data))| ForEach-Object { + [void]$HashValue.Append($_.ToString("x")) + } + return $HashValue.ToString() + } + + static [VerificationResult] EvaluateVerificationResult([VerificationResult] $verificationResult, [AttestationStatus] $attestationStatus) { + [VerificationResult] $result = $verificationResult; + # No action required if Attestation status is None OR verification result is Passed + if ($attestationStatus -ne [AttestationStatus]::None -or $verificationResult -ne [VerificationResult]::Passed) { + # Changing State Machine logic + #if($verificationResult -eq [VerificationResult]::Verify -or $verificationResult -eq [VerificationResult]::Manual) + #{ + switch ($attestationStatus) { + ([AttestationStatus]::NotAnIssue) { + $result = [VerificationResult]::Passed; + break; + } + ([AttestationStatus]::WillNotFix) { + $result = [VerificationResult]::Exception; + break; + } + ([AttestationStatus]::WillFixLater) { + $result = [VerificationResult]::Remediate; + break; + } + ([AttestationStatus]::NotApplicable) { + $result = [VerificationResult]::Passed; + break; + } + ([AttestationStatus]::StateConfirmed) { + $result = [VerificationResult]::Passed; + break; + } + } + #} + #elseif($verificationResult -eq [VerificationResult]::Failed -or $verificationResult -eq [VerificationResult]::Error) + #{ + # $result = [VerificationResult]::RiskAck; + #} + } + return $result; + } + + static [void] RegisterResourceProviderIfNotRegistered([string] $provideNamespace) + { + if([string]::IsNullOrWhiteSpace($provideNamespace)) + { + throw [System.ArgumentException] "The argument '$provideNamespace' is null or empty"; + } + + # Check if provider is registered or not + if(-not [Helpers]::IsProviderRegistered($provideNamespace)) + { + [EventBase]::PublishGenericCustomMessage(" `r`nThe resource provider: [$provideNamespace] is not registered on the subscription. `r`nRegistering resource provider, this can take up to a minute...", [MessageType]::Warning); + + Register-AzResourceProvider -ProviderNamespace $provideNamespace + + $retryCount = 10; + while($retryCount -ne 0 -and (-not [Helpers]::IsProviderRegistered($provideNamespace))) + { + $timeout = 10 + Start-Sleep -Seconds $timeout + $retryCount--; + #[EventBase]::PublishGenericCustomMessage("Checking resource provider status every $timeout seconds..."); + } + + if(-not [Helpers]::IsProviderRegistered($provideNamespace)) + { + throw ([SuppressedException]::new(("Resource provider: [$provideNamespace] registration failed. `r`nTry registering the resource provider from Azure Portal --> your Subscription --> Resource Providers --> $provideNamespace --> Register"), [SuppressedExceptionType]::Generic)) + } + else + { + [EventBase]::PublishGenericCustomMessage("Resource provider: [$provideNamespace] registration successful.`r`n ", [MessageType]::Update); + } + } + } + + hidden static [bool] IsProviderRegistered([string] $provideNamespace) + { + return ((Get-AzResourceProvider -ProviderNamespace $provideNamespace | Where-Object { $_.RegistrationState -ne "Registered" } | Measure-Object).Count -eq 0); + } + + static [PSObject] DeepCopy([PSObject] $inputObject) + { + $memoryStream = New-Object System.IO.MemoryStream + $binaryFormatter = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + $binaryFormatter.Serialize($memoryStream, $inputObject) + $memoryStream.Position = 0 + $dataDeep = $binaryFormatter.Deserialize($memoryStream) + $memoryStream.Close() + return $dataDeep + } + + static [bool] IsvNetExpressRouteConnected($resourceName, $resourceGroupName) + { + $result = $false; + $gateways = @(); + $gateways += Get-AzVirtualNetworkGateway -ResourceGroupName $resourceGroupName | Where-Object { $_.GatewayType -eq "ExpressRoute" } + if($gateways.Count -ne 0) + { + $vNet = Get-AzVirtualNetwork -Name $resourceName -ResourceGroupName $resourceGroupName + if($vnet) + { + $subnetIds = @(); + $vnet | ForEach-Object { + if($_.Subnets) + { + $subnetIds += $_.Subnets | Select-Object -Property Id | Select-Object -ExpandProperty Id + } + }; + + if($subnetIds.Count -ne 0) + { + $gateways | ForEach-Object { + $result = $result -or (($_.IpConfigurations | Where-Object { $subnetIds -contains $_.Subnet.Id } | Measure-Object).Count -ne 0); + }; + } + } + } + return $result; + } + + static [Object] MergeObjects([Object] $source,[Object] $extend, [string] $idName) + { + $idPropName = "Id"; + if(-not [string]::IsNullOrWhiteSpace($idName)) + { + $idPropName = $idName; + } + if($source.GetType().Name -eq "PSCustomObject" -and $extend.GetType().Name -eq "PSCustomObject"){ + foreach($Property in $extend | Get-Member -type NoteProperty, Property){ + if(-not [Helpers]::CheckMember($source,$Property.Name,$false)){ + $source | Add-Member -MemberType NoteProperty -Value $extend.$($Property.Name) -Name $Property.Name ` + } + $source.$($Property.Name) = [Helpers]::MergeObjects($source.$($Property.Name), $extend.$($Property.Name), $idName) + } + } + elseif($source.GetType().Name -eq "Object[]" -and $extend.GetType().Name -eq "Object[]"){ + if([Helpers]::IsPSObjectArray($source) -or [Helpers]::IsPSObjectArray($extend)) + { + foreach($extendArrElement in $extend) { + $PropertyId = $extendArrElement | Get-Member -type NoteProperty, Property | Where-Object { $_.Name -eq $idPropName} | Select-Object -First 1 + if(($PropertyId | Measure-Object).Count -gt 0) + { + $PropertyId = $PropertyId | Select-Object -First 1 + } + else { + $PropertyId = $extendArrElement | Get-Member -type NoteProperty, Property | Select-Object -First 1 + } + $sourceElement = $source | Where-Object { $_.$($PropertyId.Name) -eq $extendArrElement.$($PropertyId.Name) } + if($sourceElement) + { + $sourceElement = [Helpers]::MergeObjects($sourceElement, $extendArrElement, $idName) + } + else + { + $source +=$extendArrElement + } + } + } + else + { + $source = ($source + $extend) | Select-Object -Unique + } + } + else{ + $source = $extend; + } + return $source + } + + + static [Object] MergeObjects([Object] $source,[Object] $extend) + { + return [Helpers]::MergeObjects($source,$extend,""); + } + + static [Bool] IsPSObjectArray($arrayObj) + { + if(($arrayObj | Measure-Object).Count -gt 0) + { + $firstElement = $arrayObj | Select-Object -First 1 + if($firstElement.GetType().Name -eq "PSCustomObject") + { + return $true + } + else + { + return $false + } + } + else + { + return $false + } + } + + #BOM replace function + static [void] RemoveUtf8BOM([System.IO.FileInfo] $file) + { + [Helpers]::SetUtf8Encoding($file); + if($file) + { + $byteBuffer = New-Object System.Byte[] 3 + $reader = $file.OpenRead() + $bytesRead = $reader.Read($byteBuffer, 0, 3); + if ($bytesRead -eq 3 -and + $byteBuffer[0] -eq 239 -and + $byteBuffer[1] -eq 187 -and + $byteBuffer[2] -eq 191) + { + $tempFile = [System.IO.Path]::GetTempFileName() + $writer = [System.IO.File]::OpenWrite($tempFile) + $reader.CopyTo($writer) + $writer.Dispose() + $reader.Dispose() + Move-Item -Path $tempFile -Destination $file.FullName -Force + } + else + { + $reader.Dispose() + } + } + } + + static [void] SetUtf8Encoding([System.IO.FileInfo] $file) + { + if($file) + { + $fileContent = Get-Content -Path $file.FullName; + if($fileContent) + { + Out-File -InputObject $fileContent -Force -FilePath $file.FullName -Encoding utf8 + } + } + } + + static [void] CleanupLocalFolder($folderPath) + { + try + { + if(Test-Path $folderPath) + { + Remove-Item -Path $folderPath -Recurse -Force -ErrorAction Stop | Out-Null + } + } + catch{ + #this call happens from finally block. Try to clean the files, if it don't happen it would get cleaned in the next attempt + } + } + + static [void] CreateFolderIfNotExist($FolderPath,$MakeFolderEmpty) + { + if(-not (Test-Path $FolderPath)) + { + mkdir -Path $FolderPath -ErrorAction Stop | Out-Null + } + elseif($MakeFolderEmpty) + { + Remove-Item -Path "$FolderPath*" -Force -Recurse + } + } + + Static [string] GetSubString($CotentString, $Pattern) + { + return [regex]::match($CotentString, $pattern).Groups[1].Value + } + + #TODO: Currently this function is specific to Org PolicyHealth Check. Need to make generic + Static [string] IsStringEmpty($String) + { + if([string]::IsNullOrEmpty($String)) + { + return "Not Available" + } + else + { + $String= $String.Split("?")[0] + return $String + } + } + + + static [PSObject] NewSecurePassword() { + #create password + $randomBytes = New-Object Byte[] 32 + $provider = [System.Security.Cryptography.RNGCryptoServiceProvider]::Create() + $provider.GetBytes($randomBytes) + $provider.Dispose() + $pwstring = [System.Convert]::ToBase64String($randomBytes) + $newPassword = new-object securestring + $pwstring.ToCharArray() | ForEach-Object { + $newPassword.AppendChar($_) + } + $encryptedPassword = ConvertFrom-SecureString -SecureString $newPassword -Key (1..16) + $securePassword = ConvertTo-SecureString -String $encryptedPassword -Key (1..16) + return $securePassword + } + + + #TODO: Move to ActiveDirectoryHelper? + static [void] SetSPNPermission($Scope,$ApplicationId,$Role) + { + $assignedRole = $null + $retryCount = 0; + While($null -eq $assignedRole -and $retryCount -le 6) + { + #Assign RBAC to SPN - contributor at RG + New-AzRoleAssignment -Scope $Scope -RoleDefinitionName $Role -ServicePrincipalName $ApplicationId -ErrorAction SilentlyContinue | Out-Null + Start-Sleep -Seconds 10 + $assignedRole = Get-AzRoleAssignment -ServicePrincipalName $ApplicationId -Scope $Scope -RoleDefinitionName $Role -ErrorAction SilentlyContinue + $retryCount++; + } + if($null -eq $assignedRole -and $retryCount -gt 6) + { + throw ([SuppressedException]::new(("SPN permission could not be set"), [SuppressedExceptionType]::InvalidOperation)) + } + } + + #TODO: StorageHelper? + static [string] CreateStorageAccountSharedKey([string] $StringToSign,[string] $AccountName,[string] $AccessKey) + { + $KeyBytes = [System.Convert]::FromBase64String($AccessKey) + $HMAC = New-Object System.Security.Cryptography.HMACSHA256 + $HMAC.Key = $KeyBytes + $UnsignedBytes = [System.Text.Encoding]::UTF8.GetBytes($StringToSign) + $KeyHash = $HMAC.ComputeHash($UnsignedBytes) + $SignedString = [System.Convert]::ToBase64String($KeyHash) + $sharedKey = $AccountName+":"+$SignedString + return $sharedKey + } + + #TODO: StorageHelper? + Static [bool] IsSASTokenUpdateRequired($policyUrl) + { + [System.Uri] $validatedUri = $null; + $IsSASTokenUpdateRequired = $false + + if([System.Uri]::TryCreate($policyUrl, [System.UriKind]::Absolute, [ref] $validatedUri) -and $validatedUri.Query.Contains("&se=")) + { + $pattern = '&se=(.*?)T' + [DateTime] $expiryDate = Get-Date + if([DateTime]::TryParse([Helpers]::GetSubString($($validatedUri.Query),$pattern),[ref] $expiryDate)) + { + if($expiryDate.AddDays(-[Constants]::SASTokenExpiryReminderInDays) -lt [DateTime]::UtcNow) + { + $IsSASTokenUpdateRequired = $true + } + } + } + return $IsSASTokenUpdateRequired + } + + #TODO: StorageHelper? + Static [string] GetUriWithUpdatedSASToken($policyUrl, $updateUrl) + { + [System.Uri] $validatedUri = $null; + $UpdatedUrl = $policyUrl + + if([System.Uri]::TryCreate($policyUrl, [System.UriKind]::Absolute, [ref] $validatedUri) -and $validatedUri.Query.Contains("&se=") -and [System.Uri]::TryCreate($policyUrl, [System.UriKind]::Absolute, [ref] $validatedUri)) + { + + $UpdatedUrl = $policyUrl.Split("?")[0] + "?" + $updateUrl.Split("?")[1] + + } + return $UpdatedUrl + } + + static [bool] ValidateEmail([string]$address){ + $validAddress = ($address -as [System.Net.Mail.MailAddress]) + return ($null -ne $validAddress -and $validAddress.Address -eq $address ) + } + + #Returns invalid email list + static [string[]] ValidateEmailList([string[]]$emailList ) + { + $invalidEmails = @(); + $emailList | ForEach-Object { + if(-not [Helpers]::ValidateEmail($_)) + { + $invalidEmails += $_ + } + } + return $invalidEmails + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/IdentityHelpers.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/IdentityHelpers.ps1 new file mode 100644 index 000000000..c8ec5db18 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/IdentityHelpers.ps1 @@ -0,0 +1,85 @@ +Set-StrictMode -Version Latest + +class IdentityHelpers +{ + + hidden static [bool] IsServiceAccount($ObjectId, $SignInName, $ObjectType, $GraphAccessToken) + { + $return = $null + $header = "Bearer " + $GraphAccessToken + $RMContext = [AccountHelper]::GetCurrentRmContext() + $headers = @{"Authorization"=$header;"Content-Type"="application/json"} + $uri="" + $output = $null + if($ObjectType -eq "User") + { + if($null -ne $ObjectId -and [System.Guid]::Empty -ne $ObjectId) + { + $uri = [string]::Format("https://graph.windows.net/{0}/users/{1}?api-version=1.6",$RMContext.Tenant.Id, $ObjectId) + } + elseif ($null -ne $SignInName) { + $uri = [string]::Format("https://graph.windows.net/{0}/users/{1}?api-version=1.6",$RMContext.Tenant.Id, $SignInName) + } + else { + return $false + } + } + elseif($ObjectType -eq "ServicePrincipal"){ + return $false + } + else + { + #in the case of coadmins + return $false + } + + $err = $null + $result = "" + try { + $result = Invoke-WebRequest -Method GET -Uri $uri -Headers $headers -UseBasicParsing + if($result.StatusCode -ge 200 -and $result.StatusCode -le 399){ + if($null -ne $result.Content){ + $json = (ConvertFrom-Json $result.Content) + if($null -ne $json){ + $output = $json + if($null -ne ($json | Get-Member value) ) + { + $output = $json.value + } + } + } + $isGuid = [IdentityHelpers]::IsADObjectGUID($output.immutableId) + return $isGuid + } + } + catch{ + $err = $_ + if($null -ne $err) + { + if($null -ne $err.ErrorDetails.Message){ + $json = (ConvertFrom-Json $err.ErrorDetails.Message) + if($null -ne $json){ + $return = $json + if($json.'odata.error'.code -eq "Request_ResourceNotFound") + { + return $false; + } + } + } + } + } + return $null + } + + + hidden static [bool] IsADObjectGUID($immutableId){ + try { + $decodedII = [system.convert]::frombase64string($immutableId) + $guid = [GUID]$decodedII + } + catch { + return $false + } + return $true + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/OMSHelper.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/OMSHelper.ps1 new file mode 100644 index 000000000..b2d064368 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/OMSHelper.ps1 @@ -0,0 +1,375 @@ +Set-StrictMode -Version Latest +Class OMSHelper{ + static [string] $DefaultOMSType = "AzSK_AAD" + static [string] $CommandEventType = "AzSK_AAD_CommandEvent" + hidden static [int] $isOMSSettingValid = 0 #-1:Fail (OMS Empty, OMS Return Error) | 0:Local + hidden static [int] $isAltOMSSettingValid = 0 + # Create the function to create and post the request + # BUGBUG: Need to rename OMSType here...it is used for 'LogType' in almost all other places. Perhaps OMSInstance (=OMS, AltOMS) or something? + static PostOMSData([string] $OMSWorkspaceID, [string] $SharedKey, $Body, $LogType, $OMSType) + { + try + { + if(($OMSType | Measure-Object).Count -gt 0 -and [OMSHelper]::$("is"+$OMSType+"SettingValid") -ne -1) + { + if([string]::IsNullOrWhiteSpace($LogType)) + { + $LogType = [OMSHelper]::DefaultOMSType + } + [string] $method = "POST" + [string] $contentType = "application/json" + [string] $resource = "/api/logs" + $rfc1123date = [System.DateTime]::UtcNow.ToString("r") + [int] $contentLength = $Body.Length + [string] $signature = [OMSHelper]::GetOMSSignature($OMSWorkspaceID , $SharedKey , $rfc1123date ,$contentLength ,$method ,$contentType ,$resource) + [string] $uri = "https://" + $OMSWorkspaceID + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01" + [DateTime] $TimeStampField = [System.DateTime]::UtcNow + $headers = @{ + "Authorization" = $signature; + "Log-Type" = $LogType; + "x-ms-date" = $rfc1123date; + "time-generated-field" = $TimeStampField; + } + $response = Invoke-WebRequest -Uri $uri -Method $method -ContentType $contentType -Headers $headers -Body $Body -UseBasicParsing + } + } + catch + { + $warningMsg="" + if($OMSType -eq 'OMS' -or $OMSType -eq 'AltOMS') + { + switch([OMSHelper]::$("is"+$OMSType+"SettingValid")) + { + 0 { $warningMsg += "The $($OMSType) workspace id or key is invalid in the local settings file. You can use Set-AzSKOMSSettings with correct values to update it.";} + 1 { $warningMsg += "The $($OMSType) workspace id or key is invalid in the ContinuousAssurance configuration. You can use Update-AzSKContinuousAssurance with the correct OMS values to correct it."; } + } + [EventBase]::PublishGenericCustomMessage(" `r`nWARNING: $($warningMsg)", [MessageType]::Warning); + + #Flag to disable OMS scan + [OMSHelper]::$("is"+$OMSType+"SettingValid") = -1 + } + } + } + + static [string] GetOMSSignature ($OMSWorkspaceID, $SharedKey, $Date, $ContentLength, $Method, $ContentType, $Resource) + { + [string] $xHeaders = "x-ms-date:" + $Date + [string] $stringToHash = $Method + "`n" + $ContentLength + "`n" + $ContentType + "`n" + $xHeaders + "`n" + $Resource + + [byte[]]$bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash) + + [byte[]]$keyBytes = [Convert]::FromBase64String($SharedKey) + + [System.Security.Cryptography.HMACSHA256] $sha256 = New-Object System.Security.Cryptography.HMACSHA256 + $sha256.Key = $keyBytes + [byte[]]$calculatedHash = $sha256.ComputeHash($bytesToHash) + $encodedHash = [Convert]::ToBase64String($calculatedHash) + $authorization = 'SharedKey {0}:{1}' -f $OMSWorkspaceID,$encodedHash + return $authorization + } + + static [PSObject[]] GetOMSBodyObjects([SVTEventContext] $eventContext,[AzSKContextDetails] $AzSKContext) + { + [PSObject[]] $output = @(); + [array] $eventContext.ControlResults | ForEach-Object{ + Set-Variable -Name ControlResult -Value $_ -Scope Local + $out = [OMSModel]::new() + if($eventContext.IsResource()) + { + $out.ResourceType=$eventContext.ResourceContext.ResourceType + $out.ResourceGroup=$eventContext.ResourceContext.ResourceGroupName + $out.ResourceName=$eventContext.ResourceContext.ResourceName + $out.ResourceId = $eventContext.ResourceContext.ResourceId + $out.ChildResourceName=$ControlResult.ChildResourceName + $out.PartialScanIdentifier=$eventContext.PartialScanIdentifier + } + + $out.Reference=$eventContext.Metadata.Reference + $out.ControlStatus=$ControlResult.VerificationResult.ToString() + $out.ActualVerificationResult=$ControlResult.ActualVerificationResult.ToString() + $out.ControlId=$eventContext.ControlItem.ControlID + $out.TenantName=$eventContext.TenantContext.TenantName + $out.tenantId=$eventContext.TenantContext.tenantId + $out.FeatureName=$eventContext.FeatureName + $out.Recommendation=$eventContext.ControlItem.Recommendation + $out.ControlSeverity=$eventContext.ControlItem.ControlSeverity.ToString() + $out.Source=$AzSKContext.Source + $out.Tags=$eventContext.ControlItem.Tags + $out.RunIdentifier = $AzSKContext.RunIdentifier + $out.HasRequiredAccess = $ControlResult.CurrentSessionContext.Permissions.HasRequiredAccess + $out.ScannerVersion = $AzSKContext.Version + $out.IsBaselineControl = $eventContext.ControlItem.IsBaselineControl + $out.HasAttestationWritePermissions = $ControlResult.CurrentSessionContext.Permissions.HasAttestationWritePermissions + $out.HasAttestationReadPermissions = $ControlResult.CurrentSessionContext.Permissions.HasAttestationReadPermissions + $out.IsLatestPSModule = $ControlResult.CurrentSessionContext.IsLatestPSModule + $out.PolicyOrgName = $AzSKContext.PolicyOrgName + $out.IsControlInGrace = $ControlResult.IsControlInGrace + #mapping the attestation properties + if($null -ne $ControlResult -and $null -ne $ControlResult.StateManagement -and $null -ne $ControlResult.StateManagement.AttestedStateData) + { + $attestedData = $ControlResult.StateManagement.AttestedStateData; + $out.AttestationStatus = $ControlResult.AttestationStatus.ToString(); + $out.AttestedBy = $attestedData.AttestedBy; + $out.Justification = $attestedData.Justification; + $out.AttestedDate = $attestedData.AttestedDate + $out.ExpiryDate = $attestedData.ExpiryDate + } + $output += $out + } + return $output + } + + static [void] PostApplicableControlSet([SVTEventContext[]] $contexts,[AzSKContextDetails] $AzSKContext) { + if (($contexts | Measure-Object).Count -lt 1) { return; } + $set = [OMSHelper]::ConvertToSimpleSet($contexts,$AzSKContext); + [OMSHelper]::WriteControlResult($set,"AzSK_Inventory") + #$omsMetadata = [ConfigurationManager]::LoadServerConfigFile("OMSSettings.json") + #[OMSHelper]::WriteControlResult($omsMetadata,"AzSK_MetaData") + } + + static [void] WriteControlResult([PSObject[]] $omsDataObject, [string] $OMSEventType) + { + try + { + $settings = [ConfigurationManager]::GetAzSKSettings() + if([string]::IsNullOrWhiteSpace($OMSEventType)) + { + $OMSEventType = $settings.OMSType + } + #TODO: This check may not be needed, given the IsSendingOMSEvents() check in the handler! + if((-not [string]::IsNullOrWhiteSpace($settings.OMSWorkspaceId)) -or (-not [string]::IsNullOrWhiteSpace($settings.AltOMSWorkspaceId))) + { + $omsDataObject | ForEach-Object{ + Set-Variable -Name tempBody -Value $_ -Scope Local + $body = $tempBody | ConvertTo-Json + $omsBodyByteArray = ([System.Text.Encoding]::UTF8.GetBytes($body)) + #publish to primary workspace + if(-not [string]::IsNullOrWhiteSpace($settings.OMSWorkspaceId) -and [OMSHelper]::isOMSSettingValid -ne -1) + { + [OMSHelper]::PostOMSData($settings.OMSWorkspaceId, $settings.OMSSharedKey, $omsBodyByteArray, $OMSEventType, 'OMS') + } + #publish to secondary workspace + if(-not [string]::IsNullOrWhiteSpace($settings.AltOMSWorkspaceId) -and [OMSHelper]::isAltOMSSettingValid -ne -1) + { + [OMSHelper]::PostOMSData($settings.AltOMSWorkspaceId, $settings.AltOMSSharedKey, $omsBodyByteArray, $OMSEventType, 'AltOMS') + } + } + } + } + catch + { + throw ([SuppressedException]::new("Error sending events to OMS. The following exception occurred: `r`n$($_.Exception.Message) `r`nFor more on AzSK OMS setup, refer: https://aka.ms/devopskit/ca")); + } + } + + static [PSObject[]] ConvertToSimpleSet($contexts,[AzSKContextDetails] $AzSKContext) + { + $ControlSet = [System.Collections.ArrayList]::new() + foreach ($item in $contexts) { + $set = [OMSResourceInvModel]::new() + $set.RunIdentifier = $AzSKContext.RunIdentifier + $set.tenantId = $item.TenantContext.tenantId + $set.TenantName = $item.TenantContext.TenantName + $set.Source = $AzSKContext.Source + $set.ScannerVersion = $AzSKContext.Version + $set.FeatureName = $item.FeatureName + if([Helpers]::CheckMember($item,"ResourceContext")) + { + $set.ResourceGroupName = $item.ResourceContext.ResourceGroupName + $set.ResourceName = $item.ResourceContext.ResourceName + $set.ResourceId = $item.ResourceContext.ResourceId + } + $set.ControlIntId = $item.ControlItem.Id + $set.ControlId = $item.ControlItem.ControlID + $set.ControlSeverity = $item.ControlItem.ControlSeverity + $set.Tags = $item.ControlItem.Tags + $set.IsBaselineControl = $item.ControlItem.IsBaselineControl + $ControlSet.Add($set) + } + return $ControlSet; + } + + static [void] SetOMSDetails($currentInstance) + { + #Check if Settings already contain details of OMS + $settings = [ConfigurationManager]::GetAzSKSettings() + + if([string]::IsNullOrWhiteSpace($settings.OMSWorkspaceId) -or [string]::IsNullOrWhiteSpace($settings.OMSSharedKey)) + { + [OMSHelper]::isOMSSettingValid = -1 + } + + if([string]::IsNullOrWhiteSpace($settings.AltOMSWorkspaceId) -or [string]::IsNullOrWhiteSpace($settings.AltOMSSharedKey)) + { + [OMSHelper]::isAltOMSSettingValid = -1 + } + + #If either of the settings are valid, remember so in OMSOutput object to help with efficiency + if ([OMSHelper]::isOMSSettingValid -ne -1 -or [OMSHelper]::isAltOMSSettingValid -ne -1) + { + $currentInstance.SetSendingOMSEvents() + } + } + + static PostResourceInventory([AzSKContextDetails] $AzSKContext) + { + if($AzSKContext.Source.Equals("CC", [System.StringComparison]::OrdinalIgnoreCase) -or + $AzSKContext.Source.Equals("CA", [System.StringComparison]::OrdinalIgnoreCase)){ + $resourceSet = [System.Collections.ArrayList]::new() + [ResourceInventory]::FetchResources(); + foreach($resource in [ResourceInventory]::FilteredResources){ + $set = [OMSResourceModel]::new() + $set.RunIdentifier = $AzSKContext.RunIdentifier + $set.tenantId = $resource.tenantId + #$set.TenantName = $item.TenantContext.TenantName + $set.Source = $AzSKContext.Source + $set.ScannerVersion = $AzSKContext.Version + $set.ResourceType = $resource.ResourceType + $set.ResourceGroupName = $resource.ResourceGroupName + $set.ResourceName = $resource.Name + $set.ResourceId = $resource.ResourceId + + $resourceSet.Add($set) + } + [OMSHelper]::WriteControlResult($resourceSet,"AzSK_Inventory") + $omsMetadata = [ConfigurationManager]::LoadServerConfigFile("OMSSettings.json") + [OMSHelper]::WriteControlResult($omsMetadata,"AzSK_MetaData") + } + } + + hidden static [PSObject] QueryStatusfromWorkspace([string] $workspaceId,[string] $query) + { + $result=$null; + try + { + $body = @{query=$query}; + $url="https://api.loganalytics.io/beta/workspaces/" +$workspaceId+"/api/query?api-version=2017-01-01-preview" + $response=[WebRequestHelper]::InvokePostWebRequest($url , $body); + + # Formating the response obtained from querying workspace. + if(($response | Measure-Object).Count -gt 0) + { + $data = $response; + #Out of four tables obtained, the first table contains result of query + if(($data | Measure-Object).Count -gt 0) + { + $table= $data.Tables[0]; + $Columns=$table.Columns; + $objectView = @{}; + $j = 0; + if($null -ne $table) + { + foreach ($valuetable in $table) { + foreach ($row in $table.Rows) { + $i = 0; + $count=$valuetable.Columns.Count; + $properties = @{} + foreach($col in $Columns) + { + if($i -lt $count) + { + $properties[$col.ColumnName] = $row[$i]; + } + $i++; + } + $objectView[$j] = (New-Object PSObject -Property $properties) + $j++; + } + } + $result=$objectView; + } + } + } + } + catch + { + [EventBase]::PublishGenericCustomMessage($_) + } + return $result; + } + +} + + + +Class OMSModel { + [string] $RunIdentifier + [string] $ResourceType + [string] $ResourceGroup + [string] $Reference + [string] $ResourceName + [string] $ChildResourceName + [string] $ResourceId + [string] $ControlStatus + [string] $ActualVerificationResult + [string] $ControlId + [string] $TenantName + [string] $tenantId + [string] $FeatureName + [string] $Source + [string] $Recommendation + [string] $ControlSeverity + [string] $TimeTakenInMs + [string] $AttestationStatus + [string] $AttestedBy + [string] $Justification + [string] $AttestedDate + [bool] $HasRequiredAccess + [bool] $HasAttestationWritePermissions + [bool] $HasAttestationReadPermissions + [bool] $IsLatestPSModule + [bool] $IsControlInGrace + [string[]] $Tags + [string] $ScannerVersion + [bool] $IsBaselineControl + [string] $ExpiryDate + [string] $PartialScanIdentifier + [string] $PolicyOrgName +} + +Class OMSResourceInvModel{ + [string] $RunIdentifier + [string] $tenantId + [string] $TenantName + [string] $Source + [string] $ScannerVersion + [string] $FeatureName + [string] $ResourceGroupName + [string] $ResourceName + [string] $ResourceId + [string] $ControlId + [string] $ControlIntId + [string] $ControlSeverity + [string[]] $Tags + [bool] $IsBaselineControl +} + +Class OMSResourceModel{ + [string] $RunIdentifier + [string] $tenantId + [string] $Source + [string] $ScannerVersion + [string] $ResourceType + [string] $ResourceGroupName + [string] $ResourceName + [string] $ResourceId +} + +Class AzSKContextDetails { + [string] $RunIdentifier + [string] $Version + [string] $Source + [string] $PolicyOrgName + } + +Class CommandModel{ + [string] $EventName + [string] $RunIdentifier + [string] $PartialScanIdentifier + [string] $ModuleVersion + [string] $MethodName + [string] $ModuleName + [string] $Parameters + [string] $tenantId + [string] $TenantName +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/OldConstants.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/OldConstants.ps1 new file mode 100644 index 000000000..9325d6887 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/OldConstants.ps1 @@ -0,0 +1,19 @@ +Set-StrictMode -Version Latest + +class OldConstants +{ + static [string] $AttestationDataContainerName = "azsdk-controls-state" + static [string] $CAMultiSubScanConfigContainerName = "azsdk-scan-objects" + static [string] $CAScanProgressSnapshotsContainerName = "azsdk-controls-baseline" + static [string] $CAScanOutputLogsContainerName= "azsdkexecutionlogs" + static [string] $V1AlertRGName = "AzSDKAlertsRG"; + static [string] $AzSDKRGName = "AzSDKRG"; + static [string] $StorageAccountPreName = "azsdk"; + static [string] $SettingsFileName = "AzSdkSettings.json" + static [string] $AutomationAccountName = "AzSDKContinuousAssurance"; + static [string] $AlertActionGroupName = "AzSDKAlertActionGroup" + static [string] $CriticalAlertActionGroupName = "AzSDKCriticalAlertActionGroup" + static [string] $AppFolderPath = [Constants]::AzSKAppFolderPath -replace [Constants]::NewModuleName,[Constants]::OldModuleName + static [string] $AzSDKAlertsVersionTagName = "AzSDKAlertsVersion" + static [string] $RunbookVersionTagName = "AzSDKCARunbookVersion" +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/RemoteApiHelper.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/RemoteApiHelper.ps1 new file mode 100644 index 000000000..cd62d3c67 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/RemoteApiHelper.ps1 @@ -0,0 +1,119 @@ +Set-StrictMode -Version Latest + +#Helper functions used by RemoteReportListner (for sending events to controls API) +class RemoteApiHelper { + hidden static [string] $ApiBaseEndpoint = [ConfigurationManager]::GetAzSKConfigData().AzSKApiBaseURL; #"https://localhost:44348/api" + + #TODO: Reconcile this with AccountHelper::GetAccessToken() + hidden static [string] GetAccessToken() { + $rmContext = [AccountHelper]::GetCurrentRmContext(); + $ResourceAppIdURI = [WebRequestHelper]::GetServiceManagementUrl() + return [AccountHelper]::GetAccessToken($ResourceAppIdURI); + } + + hidden static [psobject] PostContent($uri, $content, $type) { + try { + $accessToken = [RemoteApiHelper]::GetAccessToken() + $result = Invoke-WebRequest -Uri $([RemoteApiHelper]::ApiBaseEndpoint + $uri) ` + -Method Post ` + -Body $content ` + -ContentType $type ` + -Headers @{"Authorization" = "Bearer $accessToken"} ` + -UseBasicParsing + return $result + } + catch { + return "ERROR" + } + } + + hidden static [psobject] PostJsonContent($uri, $obj) { + $postContent = [Helpers]::ConvertToJsonCustomCompressed($obj) + return [RemoteApiHelper]::PostContent($uri, $postContent, "application/json") + } + + static [void] PostSubscriptionScanResult($scanResult) { + [RemoteApiHelper]::PostJsonContent("/scanresults/subscription", $scanResult) | Out-Null + } + + static [void] PostServiceScanResult($scanResult) { + [RemoteApiHelper]::PostJsonContent("/scanresults/service", $scanResult) | Out-Null + } + + static [void] PostResourceInventory($resources) { + [RemoteApiHelper]::PostJsonContent("/inventory/resources", $resources) | Out-Null + } + + static [void] PostResourceControlsInventory($resourceControlData) { + [RemoteApiHelper]::PostJsonContent("/inventory/resourceControls", $resourceControlData) | Out-Null + } + + static [void] PostResourceFlatInventory($resourcesFlat) { + [RemoteApiHelper]::PostJsonContent("/inventory/resourcesflat", $resourcesFlat) | Out-Null + } + + static [void] PostApplicableControlSet([SVTEventContext[]] $contexts) { + if (($contexts | Measure-Object).Count -lt 1) { return; } + $set = [RemoteApiHelper]::ConvertToSimpleSet($contexts); + [RemoteApiHelper]::PostJsonContent("/scanresults/service/applicable", $set) | Out-Null + } + + static [void] PostRBACTelemetry([TelemetryRBAC[]] $RBACAccess){ + [RemoteApiHelper]::PostJsonContent("/inventory/RBACTelemetry", $RBACAccess) | Out-Null + } + + static [void] PostPolicyComplianceTelemetry($PolicyComplianceData){ + [RemoteApiHelper]::PostJsonContent("/policycompliancedata", $PolicyComplianceData) | Out-Null + } + + hidden static [psobject] ConvertToSimpleSet([SVTEventContext[]] $contexts) { + $firstContext = $contexts[0] + $set = "" | Select-Object "tenantId", "TenantName", "Source", "ScannerVersion", "ControlVersion", "ControlSet" + $set.tenantId = $firstContext.TenantContext.tenantId + $set.TenantName = $firstContext.TenantContext.TenantName + $set.Source = [RemoteReportHelper]::GetScanSource() + #RENAME + $module = Get-Module 'AzSK*' | Select-Object -First 1 + $set.ScannerVersion = $module.Version.ToString() + $set.ControlVersion = $module.Version.ToString() + $set.ControlSet = [System.Collections.ArrayList]::new() + foreach ($item in $contexts) { + $controlItem = "" | Select-Object "FeatureName", "ResourceGroupName", "ResourceName", "ResourceId", "ControlIntId", "ControlId", "ControlSeverity" + $controlItem.FeatureName = $item.FeatureName + if([Helpers]::CheckMember($item,"ResourceContext")) + { + $controlItem.ResourceGroupName = $item.ResourceContext.ResourceGroupName + $controlItem.ResourceName = $item.ResourceContext.ResourceName + $controlItem.ResourceId = $item.ResourceContext.ResourceId + } + + $controlItem.ControlIntId = $item.ControlItem.Id + $controlItem.ControlId = $item.ControlItem.ControlID + $controlItem.ControlSeverity = $item.ControlItem.ControlSeverity + $set.ControlSet.Add($controlItem) | Out-Null + } + return $set; + } + + static [void] PushFeatureControlsTelemetry($ResourceControlsData) + { + if($null -ne $ResourceControlsData.ResourceContext -and ($ResourceControlsData.Controls | Measure-Object).Count -gt 0) + { + $ResourceControlsDataMini = "" | Select-Object ResourceName, ResourceGroupName, ResourceId, Controls, ChildResourceNames + $ResourceControlsDataMini.ResourceName = $ResourceControlsData.ResourceContext.ResourceName; + $ResourceControlsDataMini.ResourceGroupName = $ResourceControlsData.ResourceContext.ResourceGroupName; + $ResourceControlsDataMini.ResourceId = $ResourceControlsData.ResourceContext.ResourceId; + $controls = @(); + $ResourceControlsData.Controls | ForEach-Object { + $control = "" | Select-Object ControlStringId, ControlId; + $control.ControlStringId = $_.ControlId; + $control.ControlId = $_.Id; + $controls += $control; + } + $ResourceControlsDataMini.Controls = $controls; + $ResourceControlsDataMini.ChildResourceNames = $ResourceControlsData.ChildResourceNames; + + [RemoteApiHelper]::PostResourceControlsInventory($ResourceControlsDataMini); + } + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/RemoteReportHelper.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/RemoteReportHelper.ps1 new file mode 100644 index 000000000..fac9988d0 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/RemoteReportHelper.ps1 @@ -0,0 +1,233 @@ +Set-StrictMode -Version Latest + +#Helper functions used by various listeners that send events remotely (e.g., OMS, AIOrg/Control-Telemetry, User/Anon-Telemetry, RemoteReportsListener, etc.) +class RemoteReportHelper +{ + hidden static [string[]] $IgnoreScanParamList = "DoNotOpenOutputFolder"; + hidden static [string[]] $AllowedServiceScanParamList = "tenantId", "ResourceGroupNames"; + hidden static [string[]] $AllowedSubscriptionScanParamList = "tenantId"; + hidden static [int] $MaxServiceParamCount = [RemoteReportHelper]::IgnoreScanParamList.Count + [RemoteReportHelper]::AllowedServiceScanParamList.Count; + hidden static [int] $MaxSubscriptionParamCount = [RemoteReportHelper]::IgnoreScanParamList.Count + [RemoteReportHelper]::AllowedSubscriptionScanParamList.Count; + + static [FeatureGroup] GetFeatureGroup([SVTEventContext[]] $SVTEventContexts) + { + if(($SVTEventContexts | Measure-Object).Count -eq 0 -or $null -eq $SVTEventContexts[0].FeatureName) { + return [FeatureGroup]::Unknown + } + $feature = $SVTEventContexts[0].FeatureName.ToLower() + if($feature.Contains("subscription")){ + return [FeatureGroup]::Subscription + } else{ + return [FeatureGroup]::Service + } + } + + static [ServiceScanKind] GetServiceScanKind([string] $command, [hashtable] $parameters) + { + $parameterNames = [array] $parameters.Keys + if($parameterNames.Count -gt [RemoteReportHelper]::MaxServiceParamCount) + { + return [ServiceScanKind]::Partial; + } + $validParamCounter = 0; + foreach($parameterName in $parameterNames) + { + if ([RemoteReportHelper]::AllowedServiceScanParamList.Contains($parameterName)) + { + $validParamCounter += 1 + } + elseif ([RemoteReportHelper]::IgnoreScanParamList.Contains($parameterName)) + { + # Ignoring + } + else + { + return [ServiceScanKind]::Partial; + } + } + + if ($validParamCounter -eq 1) + { + return [ServiceScanKind]::Subscription; + } + elseif ($validParamCounter -eq 2) + { + return [ServiceScanKind]::ResourceGroup; + } + else + { + return [ServiceScanKind]::Partial; + } + } + + static [SubscriptionScanKind] GetSubscriptionScanKind([string] $command, [hashtable] $parameters) + { + $parameterNames = [array] $parameters.Keys + if($parameterNames.Count -gt [RemoteReportHelper]::MaxSubscriptionParamCount) + { + return [SubscriptionScanKind]::Partial; + } + $validParamCounter = 0; + foreach($parameterName in $parameterNames) + { + if ([RemoteReportHelper]::AllowedSubscriptionScanParamList.Contains($parameterName)) + { + $validParamCounter += 1 + } + elseif ([RemoteReportHelper]::IgnoreScanParamList.Contains($parameterName)) + { + # Ignoring + } + else + { + return [SubscriptionScanKind]::Partial; + } + } + + if ($validParamCounter -eq 1) + { + return [SubscriptionScanKind]::Complete; + } + else + { + return [SubscriptionScanKind]::Partial; + } + } + + static [SubscriptionControlResult] BuildSubscriptionControlResult([ControlResult] $controlResult, [ControlItem] $control) + { + $result = [SubscriptionControlResult]::new() + $result.ControlId = $control.ControlId + $result.ControlIntId = $control.Id + $result.ControlSeverity = $control.ControlSeverity + $result.ActualVerificationResult = $controlResult.ActualVerificationResult + $result.AttestationStatus = $controlResult.AttestationStatus + $result.VerificationResult = $controlResult.VerificationResult + $result.HasRequiredAccess = $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess + $result.IsBaselineControl = $control.IsBaselineControl + $result.MaximumAllowedGraceDays = $controlResult.MaximumAllowedGraceDays + if($control.Tags.Contains("OwnerAccess") -or $control.Tags.Contains("GraphRead")) + { + $result.HasOwnerAccessTag = $true + } + + $result.UserComments = $controlResult.UserComments + + if($null -ne $controlResult.StateManagement -and $null -ne $controlResult.StateManagement.AttestedStateData) { + $result.AttestedBy = $controlResult.StateManagement.AttestedStateData.AttestedBy + $result.Justification = $controlResult.StateManagement.AttestedStateData.Justification + $result.AttestedState = [Helpers]::ConvertToJsonCustomCompressed($controlResult.StateManagement.AttestedStateData.DataObject) + $result.AttestedDate = $controlResult.StateManagement.AttestedStateData.AttestedDate + + } + if($null -ne $controlResult.StateManagement -and $null -ne $controlResult.StateManagement.CurrentStateData) { + $result.CurrentState = [Helpers]::ConvertToJsonCustomCompressed($controlResult.StateManagement.CurrentStateData.DataObject) + } + return $result; + } + + static [ServiceControlResult] BuildServiceControlResult([ControlResult] $controlResult, [bool] $isNestedResource, [ControlItem] $control) + { + $result = [ServiceControlResult]::new() + $result.IsNestedResource = $isNestedResource + if ($isNestedResource) + { + $result.NestedResourceName = $controlResult.ChildResourceName + } + else + { + $result.NestedResourceName = $null + } + $result.ControlId = $control.ControlID + $result.ControlIntId = $control.Id + $result.ControlSeverity = $control.ControlSeverity + $result.ActualVerificationResult = $controlResult.ActualVerificationResult + $result.AttestationStatus = $controlResult.AttestationStatus + $result.VerificationResult = $controlResult.VerificationResult + $result.HasRequiredAccess = $controlResult.CurrentSessionContext.Permissions.HasRequiredAccess + $result.IsBaselineControl = $control.IsBaselineControl + $result.UserComments = $controlResult.UserComments + $result.MaximumAllowedGraceDays = $controlResult.MaximumAllowedGraceDays + if($control.Tags.Contains("OwnerAccess") -or $control.Tags.Contains("GraphRead")) + { + $result.HasOwnerAccessTag = $true + } + + if($null -ne $controlResult.StateManagement -and $null -ne $controlResult.StateManagement.AttestedStateData) { + $result.AttestedBy = $controlResult.StateManagement.AttestedStateData.AttestedBy + $result.Justification = $controlResult.StateManagement.AttestedStateData.Justification + $result.AttestedState = [Helpers]::ConvertToJsonCustomCompressed($controlResult.StateManagement.AttestedStateData.DataObject) + $result.AttestedDate = $controlResult.StateManagement.AttestedStateData.AttestedDate + } + if($null -ne $controlResult.StateManagement -and $null -ne $controlResult.StateManagement.CurrentStateData) { + $result.CurrentState = [Helpers]::ConvertToJsonCustomCompressed($controlResult.StateManagement.CurrentStateData.DataObject) + } + return $result; + } + + static [ScanSource] GetScanSource() + { + $settings = [ConfigurationManager]::GetAzSKSettings(); + [string] $omsSource = $settings.OMSSource; + if([string]::IsNullOrWhiteSpace($omsSource)){ + return [ScanSource]::SpotCheck + } + if($omsSource.Equals("CICD", [System.StringComparison]::OrdinalIgnoreCase)){ + return [ScanSource]::VSO + } + if($omsSource.Equals("CC", [System.StringComparison]::OrdinalIgnoreCase) -or + $omsSource.Equals("CA", [System.StringComparison]::OrdinalIgnoreCase)){ + return [ScanSource]::Runbook + } + return [ScanSource]::SpotCheck + } + + static [string] GetAIOrgTelemetryKey() + { + $settings = [ConfigurationManager]::GetAzSKConfigData(); + $telemetryKey = $settings.ControlTelemetryKey + + #BUGBUG: Need to address same 'perf' concern as AIOrgTMKey below! + [guid]$key = [guid]::NewGuid() + + if([guid]::TryParse($telemetryKey, [ref] $key) -and ![guid]::Empty.Equals($key)) + { + return $telemetryKey; + } + #BUGBUG: What is the intent here? + #BUGBUG: It appears that if telemetryKey in config is 00000- (and no server setting) this will return 0000--... + #TODO: This should work smoothly if we support locally forwarded OrgTelemetry in OSS mode... + return [ConfigurationManager]::GetAzSKSettings().LocalControlTelemetryKey; + } + + static [bool] IsAIOrgTelemetryEnabled() + { + $settings = [ConfigurationManager]::GetAzSKConfigData(); + $telemetryKey = $settings.ControlTelemetryKey + #BUGBUG: We should not burn a Guid each time like this. Just check non-null and perhaps length... + #If we need a mock guid, make one up 01234567-89ab-cdef-0123456789abcdef + #Also, cache the result and the fact that it has been set/checked (upon first call) + #TODO: Even otherwise, checking bEnabled first is much more optimal. Most people will have it as $false. + [guid]$key = [guid]::NewGuid() + if([guid]::TryParse($telemetryKey, [ref] $key) -and ![guid]::Empty.Equals($key)) + { + return $settings.EnableControlTelemetry; + } + #BUGBUG: Unclear why this would return LocalEnable... + return [ConfigurationManager]::GetAzSKSettings().LocalEnableControlTelemetry; + } + + static [string] Mask([psobject] $toMask) + { + $sha384 = [System.Security.Cryptography.SHA384Managed]::new() + $maskBytes = [System.Text.Encoding]::UTF8.GetBytes($toMask.ToString()) + $maskBytes = $sha384.ComputeHash($maskBytes) + $sha384.Dispose() + $take = 16 + $sb = [System.Text.StringBuilder]::new($take) + for($i = 0; $i -lt ($take/2); $i++){ + $x = $sb.Append($maskBytes[$i].ToString("x2")) + } + return $sb.ToString(); + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/ResourceHelper.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/ResourceHelper.ps1 new file mode 100644 index 000000000..b08a14181 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/ResourceHelper.ps1 @@ -0,0 +1 @@ +#TBD - role? \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/SVTMapping.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/SVTMapping.ps1 new file mode 100644 index 000000000..c76467fff --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/SVTMapping.ps1 @@ -0,0 +1,369 @@ +Set-StrictMode -Version Latest +class SVTMapping +{ + static [string] $VirtualNetworkTypeName = "VirtualNetwork"; + static [string] $ERvNetTypeName = "ERvNet"; + static [string] $LogicAppsTypeName = "LogicApps"; + static [string] $APIConnectionTypeName = "APIConnection"; + + hidden static [hashtable] $SupportedResourceMap = $null; + + static [string] GetResourceTypeEnumItems() + { + return ([SVTMapping]::Mapping | + Where-Object { -not [string]::IsNullOrEmpty($_.ResourceTypeName) } | + ForEach-Object { "$($_.ResourceTypeName.Replace(' ', '')) `r`n" } | + Sort-Object); + } + + static [hashtable] GetSupportedResourceMap() + { + if($null -eq [SVTMapping]::SupportedResourceMap){ + $supportedMap = @{} + foreach($map in [SVTMapping]::Mapping){ + if([string]::IsNullOrWhiteSpace($map.ResourceType) -or [string]::IsNullOrWhiteSpace($map.ResourceTypeName)){ + continue; + } + if($supportedMap.ContainsKey($map.ResourceType)) {continue;} + $supportedMap.Add($map.ResourceType.ToLower(), $map.ResourceTypeName) + } + [SVTMapping]::SupportedResourceMap = $supportedMap + } + return [SVTMapping]::SupportedResourceMap + } + + static [ResourceTypeMapping[]] $Mapping = ( + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Logic/Workflows"; + JsonFileName = "LogicApps.json"; + ClassName = "LogicApps"; + ResourceTypeName = [SVTMapping]::LogicAppsTypeName; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Compute/virtualMachines"; + JsonFileName = "VirtualMachine.json"; + ClassName = "VirtualMachine"; + ResourceTypeName = "VirtualMachine"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.DataLakeStore/accounts"; + JsonFileName = "DataLakeStore.json"; + ClassName = "DataLakeStore"; + ResourceTypeName = "DataLakeStore"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.DataLakeAnalytics/accounts"; + JsonFileName = "DataLakeAnalytics.json"; + ClassName = "DataLakeAnalytics"; + ResourceTypeName = "DataLakeAnalytics"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.KeyVault/vaults"; + JsonFileName = "KeyVault.json"; + ClassName = "KeyVault"; + ResourceTypeName = "KeyVault"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Sql/servers"; + JsonFileName = "SQLDatabase.json"; + ClassName = "SQLDatabase"; + FixClassName = "SQLDatabaseFix"; + FixFileName = "SQLDatabaseFix.ps1"; + ResourceTypeName = "SQLDatabase"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Web/sites"; + JsonFileName = "AppService.json"; + ClassName = "AppService"; + FixClassName = "AppServiceFix"; + FixFileName = "AppServiceFix.ps1"; + ResourceTypeName = "AppService"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.DataFactory/dataFactories"; + JsonFileName = "DataFactory.json"; + ClassName = "DataFactory"; + ResourceTypeName = "DataFactory"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.DataFactory/factories"; + JsonFileName = "DataFactoryV2.json"; + ClassName = "DataFactoryV2"; + ResourceTypeName = "DataFactoryV2"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Storage/storageAccounts"; + JsonFileName = "Storage.json"; + ClassName = "Storage"; + ResourceTypeName = "Storage"; + FixClassName = "StorageFix"; + FixFileName = "StorageFix.ps1"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.NotificationHubs/namespaces/notificationHubs"; + JsonFileName = "NotificationHub.json"; + ClassName = "NotificationHub"; + ResourceTypeName = "NotificationHub"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Cdn/profiles"; + JsonFileName = "CDN.json"; + ClassName = "CDN"; + ResourceTypeName = "CDN"; + FixClassName = "CDNFix"; + FixFileName = "CDNFix.ps1"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Network/virtualNetworks"; + JsonFileName = "VirtualNetwork.json"; + ClassName = "VirtualNetwork"; + ResourceTypeName = [SVTMapping]::VirtualNetworkTypeName; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Network/virtualNetworks"; + JsonFileName = "ERvNet.json"; + ClassName = "ERvNet"; + ResourceTypeName = [SVTMapping]::ERvNetTypeName; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.AnalysisServices/servers"; + JsonFileName = "AnalysisServices.json"; + ClassName = "AnalysisServices"; + ResourceTypeName = "AnalysisServices"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Search/searchServices"; + JsonFileName = "Search.json"; + ClassName = "Search"; + ResourceTypeName = "Search"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Batch/batchAccounts"; + JsonFileName = "Batch.json"; + ClassName = "Batch"; + ResourceTypeName = "Batch"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.ClassicCompute/domainNames"; + JsonFileName = "CloudService.json"; + ClassName = "CloudService"; + ResourceTypeName = "CloudService"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.ServiceBus/namespaces"; + JsonFileName = "ServiceBus.json"; + ClassName = "ServiceBus"; + ResourceTypeName = "ServiceBus"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Eventhub/namespaces"; + JsonFileName = "EventHub.json"; + ClassName = "EventHub"; + ResourceTypeName = "EventHub"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Cache/Redis"; + JsonFileName = "RedisCache.json"; + ClassName = "RedisCache"; + ResourceTypeName = "RedisCache"; + FixClassName = "RedisCacheFix"; + FixFileName = "RedisCacheFix.ps1"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.ServiceFabric/clusters"; + JsonFileName = "ServiceFabric.json"; + ClassName = "ServiceFabric"; + ResourceTypeName = "ServiceFabric"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Web/connectionGateways"; + JsonFileName = "ODG.json"; + ClassName = "ODG"; + ResourceTypeName = "ODG"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Network/trafficmanagerprofiles"; + JsonFileName = "TrafficManager.json"; + ClassName = "TrafficManager"; + ResourceTypeName = "TrafficManager"; + FixClassName = "TrafficManagerFix"; + FixFileName = "TrafficManagerFix.ps1"; + + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.StreamAnalytics/streamingjobs"; + JsonFileName = "StreamAnalytics.json"; + ClassName = "StreamAnalytics"; + ResourceTypeName = "StreamAnalytics"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.DocumentDb/databaseAccounts"; + JsonFileName = "CosmosDB.json"; + ClassName = "CosmosDb"; + ResourceTypeName = "CosmosDB"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Automation/automationAccounts"; + JsonFileName = "Automation.json"; + ClassName = "Automation"; + ResourceTypeName = "Automation"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Network/loadBalancers"; + JsonFileName = "LoadBalancer.json"; + ClassName = "LoadBalancer"; + ResourceTypeName = "LoadBalancer"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Web/connections"; + JsonFileName = "APIConnection.json"; + ClassName = "APIConnection"; + ResourceTypeName = [SVTMapping]::APIConnectionTypeName; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.BotService/botServices"; + JsonFileName = "BotService.json"; + ClassName = "BotService"; + ResourceTypeName = "BotService"; + }, + [ResourceTypeMapping]@{ + ResourceType = "AzSKCfg"; + ClassName = "AzSKCfg"; + JsonFileName = "AzSKCfg.json"; + ResourceTypeName = "AzSKCfg"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.ContainerInstance/containerGroups"; + ClassName = "ContainerInstances"; + JsonFileName = "ContainerInstances.json"; + ResourceTypeName = "ContainerInstances"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.ContainerRegistry/registries"; + ClassName = "ContainerRegistry"; + JsonFileName = "ContainerRegistry.json"; + ResourceTypeName = "ContainerRegistry"; + FixClassName = "ContainerRegistryFix"; + FixFileName = "ContainerRegistryFix.ps1"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.Databricks/workspaces"; + ClassName = "Databricks"; + JsonFileName = "Databricks.json"; + ResourceTypeName = "Databricks"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.HDInsight/clusters"; + ClassName = "HDInsight"; + JsonFileName = "HDInsight.json"; + ResourceTypeName = "HDInsight"; + }, + [ResourceTypeMapping]@{ + ResourceType = ""; + ClassName = ""; + JsonFileName = "ApplicationProxy.json"; + ResourceTypeName = ""; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.ApiManagement/service"; + ClassName = "APIManagement"; + JsonFileName = "APIManagement.json"; + ResourceTypeName = "APIManagement"; + }, + [ResourceTypeMapping]@{ + ResourceType = "Microsoft.ContainerService/ManagedClusters"; + ClassName = "KubernetesService"; + JsonFileName = "KubernetesService.json"; + ResourceTypeName = "KubernetesService"; + } + ); + + static [ResourceTypeMapping[]] $AzSKDevOpsResourceMapping = ( + [ResourceTypeMapping]@{ + ResourceType = "AzureDevOps.Organization"; + JsonFileName = "AzureDevOps.Organization.json"; + ClassName = "Organization"; + ResourceTypeName = "Organization"; + }, + [ResourceTypeMapping]@{ + ResourceType = "AzureDevOps.Project"; + ClassName = "Project"; + JsonFileName = "AzureDevOps.Project.json"; + ResourceTypeName = "Project"; + }, + [ResourceTypeMapping]@{ + ResourceType = "AzureDevOps.User"; + ClassName = "User"; + JsonFileName = "AzureDevOps.User.json"; + ResourceTypeName = "User"; + }, + [ResourceTypeMapping]@{ + ResourceType = "AzureDevOps.Build"; + ClassName = "Build"; + JsonFileName = "AzureDevOps.Build.json"; + ResourceTypeName = "Build"; + }, + [ResourceTypeMapping]@{ + ResourceType = "AzureDevOps.Release"; + ClassName = "Release"; + JsonFileName = "AzureDevOps.Release.json"; + ResourceTypeName = "Release"; + }, + [ResourceTypeMapping]@{ + ResourceType = "AzureDevOps.ServiceConnection"; + ClassName = "ServiceConnection"; + JsonFileName = "AzureDevOps.ServiceConnection.json"; + ResourceTypeName = "ServiceConnection"; + } + ) + + static [ResourceTypeMapping[]] $AADResourceMapping = ( + [ResourceTypeMapping]@{ + ResourceType = "AAD.Tenant"; + ClassName = "Tenant"; + JsonFileName = "AAD.Tenant.json"; + ResourceTypeName = "Tenant"; + }, + [ResourceTypeMapping]@{ + ResourceType = "AAD.Application"; + ClassName = "Application"; + JsonFileName = "AAD.Application.json"; + ResourceTypeName = "App"; + }, + [ResourceTypeMapping]@{ + ResourceType = "AAD.ServicePrincipal"; + ClassName = "ServicePrincipal"; + JsonFileName = "AAD.ServicePrincipal.json"; + ResourceTypeName = "SPN"; + }, + [ResourceTypeMapping]@{ + ResourceType = "AAD.Device"; + ClassName = "Device"; + JsonFileName = "AAD.Device.json"; + ResourceTypeName = "Device"; + }, + [ResourceTypeMapping]@{ + ResourceType = "AAD.User"; + ClassName = "User"; + JsonFileName = "AAD.User.json"; + ResourceTypeName = "User"; + }, + [ResourceTypeMapping]@{ + ResourceType = "AAD.Group"; + ClassName = "Group"; + JsonFileName = "AAD.Group.json"; + ResourceTypeName = "Group"; + } + ) + + + static [SubscriptionMapping] $SubscriptionMapping = @{ + ClassName = "SubscriptionCore"; + JsonFileName = "SubscriptionCore.json"; + FixClassName = "SubscriptionCoreFix"; + FixFileName = "SubscriptionCoreFix.ps1"; + }; + +} + +Invoke-Expression "enum ResourceTypeName { `r`n All `r`n $([SVTMapping]::GetResourceTypeEnumItems()) }"; diff --git a/src/AzSK.AAD/0.9.0/Framework/Helpers/WebRequestHelper.ps1 b/src/AzSK.AAD/0.9.0/Framework/Helpers/WebRequestHelper.ps1 new file mode 100644 index 000000000..6f2853842 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Helpers/WebRequestHelper.ps1 @@ -0,0 +1,374 @@ +Set-StrictMode -Version Latest +class WebRequestHelper { + #TODO: shouldn't these be in 'Constants' as well? + hidden static [string] $AzureManagementUri = "https://management.azure.com/"; + hidden static [string] $GraphApiUri = "https://graph.windows.net/"; + hidden static [string] $ClassicManagementUri = "https://management.core.windows.net/"; + + static [System.Object[]] InvokeGetWebRequest([string] $uri, [Hashtable] $headers) + { + return [WebRequestHelper]::InvokeWebRequest([Microsoft.PowerShell.Commands.WebRequestMethod]::Get, $uri, $headers, $null); + } + + static [System.Object[]] InvokeGetWebRequest([string] $uri) + { + return [WebRequestHelper]::InvokeGetWebRequest($uri, [WebRequestHelper]::GetAuthHeaderFromUri($uri)); + } + + hidden static [string] GetResourceManagerUrl() + { + $azureEnv= [AzSKSettings]::GetInstance().AzureEnvironment + if(-not [string]::IsNullOrWhiteSpace($azureEnv) -and ($azureEnv -ne [Constants]::DefaultAzureEnvironment)) + { + return [AccountHelper]::GetCurrentRmContext().Environment.ResourceManagerUrl + } + return "https://management.azure.com/" + } + + hidden static [string] GetServiceManagementUrl() + { + $azureEnv= [AzSKSettings]::GetInstance().AzureEnvironment + if(-not [string]::IsNullOrWhiteSpace($azureEnv) -and ($azureEnv -ne [Constants]::DefaultAzureEnvironment)) + { + return [AccountHelper]::GetCurrentRmContext().Environment.ServiceManagementUrl + } + return "https://management.core.windows.net/" + } + + hidden static [Hashtable] GetAuthHeaderFromUri([string] $uri) + { + [System.Uri] $validatedUri = $null; + if([System.Uri]::TryCreate($uri, [System.UriKind]::Absolute, [ref] $validatedUri)) + { + return @{ + "Authorization"= ("Bearer " + [AccountHelper]::GetAccessToken($validatedUri.GetLeftPart([System.UriPartial]::Authority))); + "Content-Type"="application/json" + }; + + } + + return @{ "Content-Type"="application/json" }; + } + + static [System.Object[]] InvokePostWebRequest([string] $uri, [Hashtable] $headers, [System.Object] $body) + { + return [WebRequestHelper]::InvokeWebRequest([Microsoft.PowerShell.Commands.WebRequestMethod]::Post, $uri, $headers, $body); + } + + static [System.Object[]] InvokePostWebRequest([string] $uri, [System.Object] $body) + { + return [WebRequestHelper]::InvokePostWebRequest($uri, [WebRequestHelper]::GetAuthHeaderFromUri($uri), $body); + } + + static [System.Object[]] InvokeWebRequest([Microsoft.PowerShell.Commands.WebRequestMethod] $method, [string] $uri, [System.Object] $body) + { + return [WebRequestHelper]::InvokeWebRequest($method, $uri, [WebRequestHelper]::GetAuthHeaderFromUri($uri), $body); + } + static [System.Object[]] InvokeWebRequest([Microsoft.PowerShell.Commands.WebRequestMethod] $method, [string] $uri, [Hashtable] $headers, [System.Object] $body) + { + return [WebRequestHelper]::InvokeWebRequest($method, $uri, $headers, $body, $Null); + } + static [System.Object[]] InvokeWebRequest([Microsoft.PowerShell.Commands.WebRequestMethod] $method, [string] $uri, [Hashtable] $headers, [System.Object] $body, [string] $contentType) + { + $outputValues = @(); + [System.Uri] $validatedUri = $null; + $orginalUri = ""; + while ([System.Uri]::TryCreate($uri, [System.UriKind]::Absolute, [ref] $validatedUri)) + { + if([string]::IsNullOrWhiteSpace($orginalUri)) + { + $orginalUri = $validatedUri.AbsoluteUri; + } + [int] $retryCount = 3 + $success = $false; + while($retryCount -gt 0 -and -not $success) + { + $retryCount = $retryCount -1; + try + { + $requestResult = $null; + + if ($method -eq [Microsoft.PowerShell.Commands.WebRequestMethod]::Get) + { + $requestResult = Invoke-WebRequest -Method $method -Uri $validatedUri -Headers $headers -UseBasicParsing + } + elseif ($method -eq [Microsoft.PowerShell.Commands.WebRequestMethod]::Post -or $method -eq [Microsoft.PowerShell.Commands.WebRequestMethod]::Put) + { + if($uri.EndsWith("`$batch")) + { + $requestResult = Invoke-WebRequest -Method $method -Uri $validatedUri -Headers $headers -Body $body -ContentType $contentType -UseBasicParsing + $success = $true + $uri = [string]::Empty + } + else + { + $requestResult = Invoke-WebRequest -Method $method -Uri $validatedUri -Headers $headers -Body ($body | ConvertTo-Json -Depth 10 -Compress) -UseBasicParsing + } + } + else + { + throw [System.ArgumentException] ("The web request method type '$method' is not supported.") + } + + if ($null -ne $requestResult -and $requestResult.StatusCode -ge 200 -and $requestResult.StatusCode -le 399) { + if (!$success -and $null -ne $requestResult.Content) { + $json = ConvertFrom-Json $requestResult.Content + if ($null -ne $json) { + if (($json | Get-Member -Name "value") -and $json.value) { + $outputValues += $json.value; + } + else { + $outputValues += $json; + } + + if (($json | Get-Member -Name "nextLink") -and $json.nextLink) { + $uri = $json.nextLink + } + elseif($requestResult.Headers.ContainsKey('x-ms-continuation-NextPartitionKey')) + { + $nPKey = $requestResult.Headers["x-ms-continuation-NextPartitionKey"] + $uri= $orginalUri + "&NextPartitionKey=$nPKey" + } + else { + $uri = [string]::Empty; + } + } + } + } + $success = $true; + } + catch + { + #eat the exception until it is in retry mode and throw once the retry is done + if($retryCount -eq 0) + { + if([Helpers]::CheckMember($_,"Exception.Response.StatusCode") -and $_.Exception.Response.StatusCode -eq "Forbidden"){ + throw ([SuppressedException]::new(("You do not have permission to view the requested resource."), [SuppressedExceptionType]::InvalidOperation)) + } + elseif ([Helpers]::CheckMember($_,"Exception.Message")){ + throw ([SuppressedException]::new(($_.Exception.Message.ToString()), [SuppressedExceptionType]::InvalidOperation)) + } + else { + throw; + } + } + } + } + } + + return $outputValues; + } + static [System.Object[]] InvokeTableStorageBatchWebRequest([string] $RGName, [string] $StorageAccountName, [string] $TableName,[PSObject[]]$Data,[bool]$IsMergeOperation, [string] $AccessKey) + { + $uri="https://$StorageAccountName.table.core.windows.net/`$batch" + $boundary = "batch_$([guid]::NewGuid())" + $Verb = "POST" + $ContentMD5 = "" + $ContentType = "multipart/mixed; boundary=$boundary" + $Date = [DateTime]::UtcNow.ToString('r') + $CanonicalizedResource = "/$StorageAccountName/`$batch" + $SigningParts=@($Verb,$ContentMD5,$ContentType,$Date,$CanonicalizedResource) + $StringToSign = [String]::Join("`n",$SigningParts) + $sharedKey = [Helpers]::CreateStorageAccountSharedKey($StringToSign,$StorageAccountName,$AccessKey) + + $xmsdate = $Date + $changeset = "changeset_$([guid]::NewGuid().ToString())" + $contentBody = "" + $miniDataTemplateForPost = @' +--{0} +Content-Type: application/http +Content-Transfer-Encoding: binary + +POST https://{1}.table.core.windows.net/{2}() HTTP/1.1 +Accept: application/json;odata=minimalmetadata +Content-Type: application/json +Prefer: return-no-content +DataServiceVersion: 3.0 + +{3} + +'@ + $miniDataTemplateForMerge = @' +--{0} +Content-Type: application/http +Content-Transfer-Encoding: binary + +MERGE https://{1}.table.core.windows.net/{2}(PartitionKey='{3}', RowKey='{4}') HTTP/1.1 +Accept: application/json;odata=minimalmetadata +Content-Type: application/json +Prefer: return-no-content +DataServiceVersion: 3.0 + +{5} + +'@ + $template = @' +--{0} +Content-Type: multipart/mixed; boundary={1} + +{2} +--{1}-- +--{0}-- +'@ + if($IsMergeOperation) + { + $data | ForEach-Object{ + $row = $_; + $contentBody = $contentBody + ($miniDataTemplateForMerge -f $changeset, $StorageAccountName, $TableName, $row.PartitionKey, $row.RowKey, ($row | ConvertTo-Json -Depth 10)) + } + } + else + { + $data | ForEach-Object{ + $row = $_; + $contentBody = $contentBody + ($miniDataTemplateForPost -f $changeset, $StorageAccountName, $TableName, ($row | ConvertTo-Json -Depth 10)) + } + } + + $requestBody = $template -f $Boundary, $changeset, $contentBody + $headers = @{"x-ms-date"=$xmsdate;"Authorization"="SharedKey $sharedKey";"x-ms-version"="2018-03-28"} + + return ([WebRequestHelper]::InvokeWebRequest([Microsoft.PowerShell.Commands.WebRequestMethod]::Post, [string] $uri, [Hashtable] $headers, [System.Object] $requestBody, [string] $contentType)) + } + + static [System.Object[]] InvokeWebRequest([Microsoft.PowerShell.Commands.WebRequestMethod] $method, [string] $uri, [Hashtable] $headers, [System.Object] $body, [string] $contentType, [Hashtable] $propertiesToReplace) + { + $outputValues = @(); + [System.Uri] $validatedUri = $null; + $orginalUri = ""; + while ([System.Uri]::TryCreate($uri, [System.UriKind]::Absolute, [ref] $validatedUri)) + { + if([string]::IsNullOrWhiteSpace($orginalUri)) + { + $orginalUri = $validatedUri.AbsoluteUri; + } + [int] $retryCount = 3 + $success = $false; + while($retryCount -gt 0 -and -not $success) + { + $retryCount = $retryCount -1; + try + { + $requestResult = $null; + + if ($method -eq [Microsoft.PowerShell.Commands.WebRequestMethod]::Get) + { + $requestResult = Invoke-WebRequest -Method $method -Uri $validatedUri -Headers $headers -UseBasicParsing + } + elseif ($method -eq [Microsoft.PowerShell.Commands.WebRequestMethod]::Post -or $method -eq [Microsoft.PowerShell.Commands.WebRequestMethod]::Put) + { + if($uri.EndsWith("`$batch")) + { + $requestResult = Invoke-WebRequest -Method $method -Uri $validatedUri -Headers $headers -Body $body -ContentType $contentType -UseBasicParsing + $success = $true + $uri = [string]::Empty + } + else + { + $requestResult = Invoke-WebRequest -Method $method -Uri $validatedUri -Headers $headers -Body ($body | ConvertTo-Json -Depth 10 -Compress) -UseBasicParsing + } + } + else + { + throw [System.ArgumentException] ("The web request method type '$method' is not supported.") + } + + if ($null -ne $requestResult -and $requestResult.StatusCode -ge 200 -and $requestResult.StatusCode -le 399) { + if (!$success -and $null -ne $requestResult.Content) { + $resultContent = $requestResult.Content + if($propertiesToReplace.Keys.Count -gt 0) + { + $propertiesToReplace.Keys | Foreach-Object { + $resultContent = $resultContent.ToString().Replace($_, $propertiesToReplace[$_]) + } + } + $json = ConvertFrom-Json $resultContent + if ($null -ne $json) { + if (($json | Get-Member -Name "value") -and $json.value) { + $outputValues += $json.value; + } + else { + $outputValues += $json; + } + + if (($json | Get-Member -Name "nextLink") -and $json.nextLink) { + $uri = $json.nextLink + } + elseif($requestResult.Headers.ContainsKey('x-ms-continuation-NextPartitionKey')) + { + $nPKey = $requestResult.Headers["x-ms-continuation-NextPartitionKey"] + $uri= $orginalUri + "&NextPartitionKey=$nPKey" + } + else { + $uri = [string]::Empty; + } + } + } + } + $success = $true; + } + catch + { + #eat the exception until it is in retry mode and throw once the retry is done + if($retryCount -eq 0) + { + if([Helpers]::CheckMember($_,"Exception.Response.StatusCode") -and $_.Exception.Response.StatusCode -eq "Forbidden"){ + throw ([SuppressedException]::new(("You do not have permission to view the requested resource."), [SuppressedExceptionType]::InvalidOperation)) + } + elseif ([Helpers]::CheckMember($_,"Exception.Message")){ + throw ([SuppressedException]::new(($_.Exception.Message.ToString()), [SuppressedExceptionType]::InvalidOperation)) + } + else { + throw; + } + } + } + } + } + + return $outputValues; + } + + hidden static [PSObject] InvokeAADAPI($methodUrl) + { + $apiToken = [AccountHelper]::GetCurrentAADAPIToken() + + $apiRoot = [WebRequestHelper]::GetAADAPIUrl(); + + $apiMethod = $methodUrl + $targetUrl = $apiRoot+$apiMethod + + $headers = @{ + "Authorization" = "Bearer $($apiToken.AccessToken)" + 'X-Requested-With'= 'XMLHttpRequest' + 'x-ms-client-request-id'= [guid]::NewGuid() + 'x-ms-correlation-id' = [guid]::NewGuid()} + + + $response = $null + + try { + $response = Invoke-RestMethod $targetUrl -Headers $headers -Method GET + } + catch { + #TODO: (1) Correct exception treatment? + #TODO: How to write exception details just to detailed log and not on-screen? + Write-Host -ForegroundColor Yellow "Error calling AAD API endpoint: $apiMethod." + #TODO: Absorbing exception and returning $response = $null below. + $response = $null + } + return $response + } + + hidden static [string] GetAADAPIUrl() + { + #BUGBUG: Add handling for Azure Gov. + return [Constants]::AADAPIUrl; + } + + hidden static [string] GetAADAPIGuid() + { + #BUGBUG: Will this also change for Azure Gov? + return Constants::AADAPIGuid; + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/EventHub/EventHubOutput.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/EventHub/EventHubOutput.ps1 new file mode 100644 index 000000000..efb44e059 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/EventHub/EventHubOutput.ps1 @@ -0,0 +1,170 @@ +Set-StrictMode -Version Latest + +class EventHubOutput: ListenerBase +{ + hidden static [EventHubOutput] $Instance = $null; + #Default source is kept as SDL / PowerShell. + #This value must be set in respective environment i.e. CICD,CC + [string] $EventHubSource; + + EventHubOutput() + { + + } + + + static [EventHubOutput] GetInstance() + { + if($null -eq [EventHubOutput]::Instance) + { + [EventHubOutput]::Instance = [EventHubOutput]::new(); + } + return [EventHubOutput]::Instance; + } + + [void] RegisterEvents() + { + $this.UnregisterEvents(); + + $this.RegisterEvent([SVTEvent]::EvaluationCompleted, { + $currentInstance = [EventHubOutput]::GetInstance(); + #Write-Host -ForegroundColor White "Hello World!" + try + { + $currentInstance.WriteControlResult([SVTEventContext[]] ($Event.SourceArgs)); + } + catch + { + $currentInstance.PublishException($_); + } + }); + } + + hidden [void] WriteControlResult([SVTEventContext[]] $eventContextAll) + { + try + { + $settings = [ConfigurationManager]::GetAzSKSettings() + $tempBodyObjectsAll = [System.Collections.ArrayList]::new() + + if(-not [string]::IsNullOrWhiteSpace($settings.EventHubSource)) + { + $this.EventHubSource = $settings.EventHubSource + } + + if(-not [string]::IsNullOrWhiteSpace($settings.EventHubNamespace)) + { + $eventContextAll | ForEach-Object{ + $eventContext = $_ + + $tempBodyObjects = $this.GetEventHubBodyObjects($this.EventHubSource,$eventContext) + $tempBodyObjects | ForEach-Object{ + Set-Variable -Name tempBody -Value $_ -Scope Local + $tempBodyObjectsAll.Add($tempBody) + } + } + + $body = $tempBodyObjectsAll | ConvertTo-Json + [EventHubOutput]::PostEventHubData(` + $settings.EventHubNamespace, ` + $settings.EventHubName, ` + $settings.EventHubSendKeyName, ` + $settings.EventHubSendKey,` + $body, ` + $settings.EventHubType) + } + } + catch + { + [Exception] $ex = [Exception]::new(("Invalid EventHub Settings: " + $_.Exception.ToString()), $_.Exception) + throw [SuppressedException] $ex + } + } + + hidden [PSObject[]] GetEventHubBodyObjects([string] $Source,[SVTEventContext] $eventContext) + { + [PSObject[]] $output = @(); + [array] $eventContext.ControlResults | ForEach-Object{ + Set-Variable -Name ControlResult -Value $_ -Scope Local + $out = "" | Select-Object ResourceType, ResourceGroup, Reference, ResourceName, ChildResourceName, ControlStatus, ActualVerificationResult, ControlId, TenantName, tenantId, FeatureName, Source, Recommendation, ControlSeverity, TimeTakenInMs, AttestationStatus, AttestedBy, Justification + if($eventContext.IsResource()) + { + $out.ResourceType=$eventContext.ResourceContext.ResourceType + $out.ResourceGroup=$eventContext.ResourceContext.ResourceGroupName + $out.ResourceName=$eventContext.ResourceContext.ResourceName + $out.ChildResourceName=$ControlResult.ChildResourceName + } + + $out.Reference=$eventContext.Metadata.Reference + $out.ControlStatus=$ControlResult.VerificationResult.ToString() + $out.ActualVerificationResult=$ControlResult.ActualVerificationResult.ToString() + $out.ControlId=$eventContext.ControlItem.ControlID + $out.TenantName=$eventContext.TenantContext.TenantName + $out.tenantId=$eventContext.TenantContext.tenantId + $out.FeatureName=$eventContext.FeatureName + $out.Recommendation=$eventContext.ControlItem.Recommendation + $out.ControlSeverity=$eventContext.ControlItem.ControlSeverity.ToString() + $out.Source=$Source + + #mapping the attestation properties + if($null -ne $ControlResult -and $null -ne $ControlResult.StateManagement -and $null -ne $ControlResult.StateManagement.AttestedStateData) + { + $attestedData = $ControlResult.StateManagement.AttestedStateData; + $out.AttestationStatus = $ControlResult.AttestationStatus.ToString(); + $out.AttestedBy = $attestedData.AttestedBy; + $out.Justification = $attestedData.Justification; + } + + $output += $out + } + return $output + } + + static [string] PostEventHubData([string] $ehNamespace, [string] $ehName, [string] $ehSendKeyName, [string] $ehSendKey, $body, $logType) + { + $ehUrl = "$ehNamespace.servicebus.windows.net/$ehName" + $ControlSettingsJson = [ConfigurationManager]::LoadServerConfigFile("ControlSettings.json") + $sasToken=GetEventHubToken -URI $ehUrl -AccessPolicyName $ehSendKeyName -AccessPolicyKey $ehSendKey -TokenTimeOut $ControlSettingsJson.EventHubOutput.TokenTimeOut + $response = SendEventHubMessage -URI $ehUrl -SASToken $sasToken -Message $body -TimeOut $ControlSettingsJson.EventHubOutput.TimeOut -APIVersion $ControlSettingsJson.EventHubOutput.APIVersion + return $response.StatusCode + } +} + +function GetEventHubToken([string]$URI, [string]$AccessPolicyName, [string]$AccessPolicyKey, [int]$TokenTimeOut) +{ + [Reflection.Assembly]::LoadWithPartialName("System.Web")| out-null + + $now = [DateTimeOffset]::Now + $Expires=($now.ToUnixTimeSeconds())+$TokenTimeOut + + $SignatureString=[System.Web.HttpUtility]::UrlEncode($URI)+ "`n" + [string]$Expires + $HMAC = New-Object System.Security.Cryptography.HMACSHA256 + $HMAC.key = [Text.Encoding]::ASCII.GetBytes($AccessPolicyKey) + $Signature = $HMAC.ComputeHash([Text.Encoding]::ASCII.GetBytes($SignatureString)) + $Signature = [Convert]::ToBase64String($Signature) + $SASToken = "SharedAccessSignature sr=" + ` + [System.Web.HttpUtility]::UrlEncode($URI) +` + "&sig=" + [System.Web.HttpUtility]::UrlEncode($Signature) + ` + "&se=" + $Expires + ` + "&skn=" + $AccessPolicyName + return $SASToken +} + +function SendEventHubMessage([string]$URI, [string]$SASToken, [string]$Message, [int]$TimeOut, [string]$APIVersion) +{ + try { + $webRequest=Invoke-WebRequest ` + -Method POST ` + -Uri ("https://"+$URI+"/messages?timeout="+$TimeOut+"&api-version="+$APIVersion) ` + -Header @{ Authorization = $SASToken} ` + -ContentType "application/atom+xml;type=entry;charset=utf-8" ` + -Body $Message ` + -ErrorAction SilentlyContinue + } + catch + { + Write-Error("Invoke-WebRequest returned: `n`tStatusCode: "+$_.Exception.Response.StatusCode+"`n`tStausDescription: "+$_.Exception.Response.StatusDescription) + break + } + return $webRequest +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/FixControl/FixControlScripts/README.txt b/src/AzSK.AAD/0.9.0/Framework/Listeners/FixControl/FixControlScripts/README.txt new file mode 100644 index 000000000..04e1a3854 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/FixControl/FixControlScripts/README.txt @@ -0,0 +1,17 @@ +*** This file describes how to interpret the different files created when AzSK cmdlets are executed with 'GenerateFixScript' parameter *** +To implement the recommendations for controls, + 1. The user can review the PowerShell files under 'Services' folder. + 2. The user can update a parameters file (FixControlConfig.json) to provide input values for the fix script. This is required for controls where the fix/remediation requires input params to be supplied by the user (e.g., IP addresses, user alias, etc.). + 3. The user runs the script (RunFixScript.ps1) to remediate the relevant controls. + 4. (Optionally) The user can rerun the scan to confirm that the target controls were indeed remediated. + +The contents of the 'FixControlScripts' folder are organized as under: + + \RunFixScript.ps1 <-- The file which starts implementing the recommendations. The file typically contains repair command which uses the files from current folder. + + \FixControlConfig.json <-- The file contains the configuration of controls along with mandatory/optional parameters which are required for implementing the fix for control. + + \Services <-- The folder contains the PowerShell files which are used to implement the fix for control. + \.ps1 <-- The file contains PowerShell code to implement the fix for control. The file can be referred for review. + + \FixControlConfig-.json <-- This file is generated when repair command is run. The file contains the input values provided by user while running the repair command. The file can be referred for review. \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/FixControl/WriteFixControlFiles.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/FixControl/WriteFixControlFiles.ps1 new file mode 100644 index 000000000..4c4f319ba --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/FixControl/WriteFixControlFiles.ps1 @@ -0,0 +1,218 @@ +Set-StrictMode -Version Latest +class WriteFixControlFiles: FileOutputBase +{ + hidden static [WriteFixControlFiles] $Instance = $null; + hidden static [string] $FixFolderPath = "FixControlScripts"; + hidden static [string] $FixFilePath = "\Core\FixControl\Services\"; + + hidden static [string] $RunScriptMessage = "# AzSK repair function uses files from the 'Services' sub-folder in this folder"; + hidden static [string] $RunAzureServicesSecurity = ' +# Repair Azure resources +Repair-AzSKAzureServicesSecurity ` + -ParameterFilePath "$PSScriptRoot\FixControlConfig.json" #` + #-ResourceGroupNames "" ` + #-ResourceTypeNames "" ` + #-ResourceNames "" ` + #-ControlIds ""'; + hidden static [string] $RunSubscriptionSecurity = ' +# Repair Azure subscription +Repair-AzSKSubscriptionSecurity ` + -ParameterFilePath "$PSScriptRoot\FixControlConfig.json" #` + #-ControlIds ""'; + + static [WriteFixControlFiles] GetInstance() + { + if ( $null -eq [WriteFixControlFiles]::Instance) + { + [WriteFixControlFiles]::Instance = [WriteFixControlFiles]::new(); + } + + return [WriteFixControlFiles]::Instance + } + + [void] RegisterEvents() + { + $this.UnregisterEvents(); + + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [WriteFixControlFiles]::GetInstance(); + try + { + $currentInstance.SetRunIdentifier([AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1)); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandCompleted, { + $currentInstance = [WriteFixControlFiles]::GetInstance(); + try + { + $currentInstance.CommandCompletedAction($Event.SourceArgs); + $currentInstance.FilePath = ""; + } + catch + { + $currentInstance.PublishException($_); + } + }); + } + + hidden [SVTEventContext[]] GetFixControlEventContext([SVTEventContext[]] $arguments, [ref]$fixFileNames) + { + $resultContext = @(); + $fixFileNames.Value = @(); + + $arguments | Where-Object { $_.ControlItem.Enabled -and ($null -ne $_.ControlItem.FixControl) -and $_.ControlResults -and $_.ControlResults.Count -ne 0 } | + ForEach-Object { + $eventContext = $_; + + if(($eventContext.ControlResults | Where-Object { $_.EnableFixControl } | Measure-Object).Count -ne 0) + { + $mapping = $null; + if($eventContext.IsResource()) + { + $mapping = ([SVTMapping]::Mapping | + Where-Object { $_.ResourceTypeName -eq $eventContext.ResourceContext.ResourceTypeName } | + Select-Object -First 1); + } + else + { + $mapping = [SVTMapping]::SubscriptionMapping; + } + + if($mapping -and (-not [string]::IsNullOrWhiteSpace($mapping.FixFileName)) -and (-not [string]::IsNullOrWhiteSpace($mapping.FixClassName))) + { + $resultContext += $eventContext; + if($fixFileNames.Value -notcontains $mapping.FixFileName) + { + $fixFileNames.Value += $mapping.FixFileName; + } + } + } + }; + + return $resultContext; + } + + hidden [void] InitializeFolder([TenantContext] $subContext, [string[]] $fixControlFileNames, [string] $runScriptContent) + { + $this.SetFolderPath($subContext); + Copy-Item ("$PSScriptRoot\" + [WriteFixControlFiles]::FixFolderPath) $this.FolderPath -Recurse + + $this.SetFilePath($subContext, [WriteFixControlFiles]::FixFolderPath, "RunFixScript.ps1"); + Add-Content -Value $runScriptContent -Path $this.FilePath + + $this.SetFilePath($subContext, [WriteFixControlFiles]::FixFolderPath, "FixControlConfig.json"); + + $parentFolderPath = (Get-Item -Path $PSScriptRoot).Parent.Parent.FullName; + $parentFolderPath += [WriteFixControlFiles]::FixFilePath; + $fixControlFileNames | ForEach-Object { + mkdir -Path ($this.FolderPath + "\Services\") | Out-Null + Copy-Item ($parentFolderPath + $_) ($this.FolderPath + "\Services\" + $_) + }; + } + + [void] CommandCompletedAction([SVTEventContext[]] $arguments) + { + if($arguments -and $arguments.Count -ne 0) + { + $fixControlEventContext = @(); + [string[]] $fixControlFileNames = @(); + + $fixControlEventContext += $this.GetFixControlEventContext($arguments, [ref]$fixControlFileNames); + + if($fixControlEventContext.Count -ne 0) + { + $output = @(); + $hasSubControls = $false; + $hasResourceControls = $false; + + $fixControlEventContext | Group-Object { $_.TenantContext.tenantId } | + ForEach-Object { + $sub = $_.Group; + $subObject = [FixControlConfig]@{ + TenantContext = $sub[0].TenantContext; + }; + $output += $subObject; + + $sub | Where-Object { -not $_.IsResource() } | + ForEach-Object { + $hasSubControls = $true; + $subObject.SubscriptionControls += $this.CreateControlParam($_); + }; + + $sub | Where-Object { $_.IsResource() } | Group-Object { $_.ResourceContext.ResourceGroupName } | + ForEach-Object { + $rgObject = [ResourceGroupConfig]@{ + ResourceGroupName = $_.Name; + }; + $hasResourceControls = $true; + $subObject.ResourceGroups += $rgObject; + $_.Group | Group-Object { $_.ResourceContext.ResourceName } | + ForEach-Object { + $resource = $_.Group[0]; + $resObject = [ResourceConfig]@{ + ResourceName = $resource.ResourceContext.ResourceName; + ResourceType = $resource.ResourceContext.ResourceType; + ResourceTypeName = $resource.ResourceContext.ResourceTypeName; + }; + + $rgObject.Resources += $resObject; + + $resObject.Controls += $this.CreateControlParam($_.Group); + }; + }; + }; + + if($output.Count -ne 0) + { + $runScriptContent = [WriteFixControlFiles]::RunScriptMessage; + if($hasSubControls) + { + $runScriptContent += [WriteFixControlFiles]::RunSubscriptionSecurity; + } + + if($hasResourceControls) + { + $runScriptContent += [WriteFixControlFiles]::RunAzureServicesSecurity; + } + + $this.InitializeFolder(($fixControlEventContext | Select-Object -First 1).TenantContext, $fixControlFileNames, $runScriptContent); + [Helpers]::ConvertToJsonCustom($output, 15, 15) | Out-File $this.FilePath + } + } + } + } + + hidden [ControlParam[]] CreateControlParam([SVTEventContext[]] $resources) + { + $result = @(); + $resources | Group-Object { $_.ControlItem.Id } | + ForEach-Object { + $context = $_.Group[0]; + $controlObject = [ControlParam]@{ + ControlID = $context.ControlItem.ControlID; + Id = $context.ControlItem.Id; + ControlSeverity = $context.ControlItem.ControlSeverity; + FixControlImpact = $context.ControlItem.FixControl.FixControlImpact; + Description = $context.ControlItem.Description; + Enabled = $context.ControlItem.Enabled; + }; + + $result += $controlObject; + + $_.Group | ForEach-Object { + $_.ControlResults | Where-Object { $_.EnableFixControl } | ForEach-Object { + $controlObject.ChildResourceParams += [ChildResourceParam]@{ + ChildResourceName = $_.ChildResourceName; + Parameters = $_.FixControlParameters; + }; + }; + }; + }; + return $result; + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/GenericListener/GenericListener.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/GenericListener/GenericListener.ps1 new file mode 100644 index 000000000..3c0cbb3e0 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/GenericListener/GenericListener.ps1 @@ -0,0 +1,159 @@ +Set-StrictMode -Version Latest + +class GenericListener: ListenerBase +{ + hidden static [GenericListener] $Instance = $null; + + hidden [GenericListenerBase[]] $ExtendedListeners = @(); + + + GenericListener() + { + + } + + static [GenericListener] GetInstance() + { + if($null -eq [GenericListener]::Instance) + { + [GenericListener]::Instance = [GenericListener]::new(); + } + return [GenericListener]::Instance; + } + + [void] RegisterEvents() + { + $this.UnregisterEvents(); + + # Mandatory: Generate Run Identifier Event + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [GenericListener]::GetInstance(); + try + { + $rootEventArgs = [AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1); + $currentInstance.SetRunIdentifier($rootEventArgs); + $currentInstance.LoadExtendedListeners($rootEventArgs); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandStarted, { + $currentInstance = [GenericListener]::GetInstance(); + try + { + $params = @{}; + $params.Add("EventArgs", $Event.SourceArgs); + $currentInstance.CallListenersMethod("SVTCommandStarted",$params); + } + catch{ + $currentInstance.PublishException($_); + } + + }); + + + $this.RegisterEvent([AzSKRootEvent]::CommandStarted, { + $currentInstance = [GenericListener]::GetInstance(); + try + { + $params = @{}; + $params.Add("EventArgs", $Event.SourceArgs); + $currentInstance.CallListenersMethod("GenericCommandStarted",$params); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + + $this.RegisterEvent([AzSKRootEvent]::CommandCompleted, { + $currentInstance = [GenericListener]::GetInstance(); + try + { + $params = @{}; + $params.Add("EventArgs", $Event.SourceArgs); + $currentInstance.CallListenersMethod("GenericCommandCompleted",$params); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandCompleted, { + $currentInstance = [GenericListener]::GetInstance(); + try + { + $params = @{}; + $params.Add("EventArgs", $Event.SourceArgs); + $currentInstance.CallListenersMethod("SVTCommandCompleted",$params); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::EvaluationStarted, { + $currentInstance = [GenericListener]::GetInstance(); + try + { + $params = @{}; + $params.Add("EventArgs", $Event.SourceArgs); + $currentInstance.CallListenersMethod("FeatureEvaluationStarted",$params); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::EvaluationCompleted, { + $currentInstance = [GenericListener]::GetInstance(); + try + { + $params = @{}; + $params.Add("EventArgs", $Event.SourceArgs); + $currentInstance.CallListenersMethod("FeatureEvaluationCompleted",$params); + } + catch + { + $currentInstance.PublishException($_); + } + }); + } + + [void] LoadExtendedListeners([AzSKRootEventArgument] $rootEventArgs) + { + if(($this.ExtendedListeners | Measure-Object).Count -le 0) + { + $ListenerFilePaths = [ConfigurationManager]::RegisterExtListenerFiles(); + if(($ListenerFilePaths | Measure-Object).Count -gt 0) + { + $ListenerFilePaths | ForEach-Object { + $listenerPath = $_; + . $listenerPath + $listenerFileName = [System.IO.Path]::GetFileName($listenerPath); + $listenerClassName = $listenerFileName.trimend(".ext.ps1") + "Ext" + $listenerObject = New-Object -TypeName $listenerClassName -ArgumentList $this, $rootEventArgs + $this.ExtendedListeners += $listenerObject; + } + } + } + } + + [void] CallListenersMethod($methodName, $parameters) + { + if(($this.ExtendedListeners | Measure-Object).Count -gt 0) + { + $this.ExtendedListeners | ForEach-Object { + $listenerObject = $_ + $listenerObject.$methodName($parameters); + } + } + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/GenericListener/GenericListenerBase.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/GenericListener/GenericListenerBase.ps1 new file mode 100644 index 000000000..bdc772a12 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/GenericListener/GenericListenerBase.ps1 @@ -0,0 +1,50 @@ +Set-StrictMode -Version Latest + +class GenericListenerBase +{ + hidden [ListenerBase] $ParentInstance = $null; + + hidden [AzSKRootEventArgument] $EventArgs = $null; + + + GenericListenerBase($_ParentInstance, $_EventArgs) + { + $this.ParentInstance = $_ParentInstance; + $this.EventArgs = $_EventArgs; + } + + [void] SVTCommandStarted ([PSObject] $params) + { + return; + } + + [void] SVTCommandCompleted ([PSObject] $params) + { + return; + } + + [void] GenericCommandStarted ([PSObject] $params) + { + return; + } + + [void] GenericCommandCompleted ([PSObject] $params) + { + return; + } + + [void] FeatureEvaluationStarted ([PSObject] $params) + { + return; + } + + [void] FeatureEvaluationCompleted ([PSObject] $params) + { + return; + } +} + + + + + diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/ListenerHelper.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/ListenerHelper.ps1 new file mode 100644 index 000000000..50fe1cf35 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/ListenerHelper.ps1 @@ -0,0 +1,48 @@ +Set-StrictMode -Version Latest +#Class to register appropriate listeners based on environment +class ListenerHelper +{ + static ListenerHelper() + { + } + + static [void] RegisterListeners() + { + [WriteFolderPath]::GetInstance().RegisterEvents(); + [WriteDetailedLog]::GetInstance().RegisterEvents(); + [WriteSummaryFile]::GetInstance().RegisterEvents(); + [WritePsConsole]::GetInstance().RegisterEvents(); + [WriteDataFile]::GetInstance().RegisterEvents(); + [OMSOutput]::GetInstance().RegisterEvents(); + [EventHubOutput]::GetInstance().RegisterEvents(); + [WebhookOutput]::GetInstance().RegisterEvents(); + [AIOrgTelemetry]::GetInstance().RegisterEvents(); + [UsageTelemetry]::GetInstance().RegisterEvents(); + [RemoteReportsListener]::GetInstance().RegisterEvents(); + [WriteEnvironmentFile]::GetInstance().RegisterEvents(); + [WriteFixControlFiles]::GetInstance().RegisterEvents(); + [SecurityRecommendationReport]::GetInstance().RegisterEvents(); + [GenericListener]::GetInstance().RegisterEvents(); + } + + + static [void] UnregisterListeners() + { + [WriteFolderPath]::GetInstance().UnregisterEvents(); + [WriteDetailedLog]::GetInstance().UnregisterEvents(); + [WriteSummaryFile]::GetInstance().UnregisterEvents(); + [WritePsConsole]::GetInstance().UnregisterEvents(); + [WriteDataFile]::GetInstance().UnregisterEvents(); + [OMSOutput]::GetInstance().UnregisterEvents(); + [EventHubOutput]::GetInstance().UnregisterEvents(); + [WebhookOutput]::GetInstance().UnregisterEvents(); + [AIOrgTelemetry]::GetInstance().UnregisterEvents(); + [UsageTelemetry]::GetInstance().UnregisterEvents(); + [RemoteReportsListener]::GetInstance().UnregisterEvents(); + [WriteEnvironmentFile]::GetInstance().UnregisterEvents(); + [WriteFixControlFiles]::GetInstance().UnregisterEvents(); + [SecurityRecommendationReport]::GetInstance().UnregisterEvents(); + [GenericListener]::GetInstance().UnregisterEvents(); + } +} +#[ListenerHelper]::RegisterListeners(); diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/OMS/OMSOutput.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/OMS/OMSOutput.ps1 new file mode 100644 index 000000000..db4a01fc4 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/OMS/OMSOutput.ps1 @@ -0,0 +1,289 @@ +Set-StrictMode -Version Latest + +class OMSOutput: ListenerBase +{ + hidden static [OMSOutput] $Instance = $null; + #Default source is kept as SDL / PowerShell. + static [string] $DefaultOMSSource = "SDL" + #This value must be set in respective environment i.e. CICD,CA + hidden static [bool] $IsIssueLogged = $false + #Is there an actual OMS workspace we will send events to? + hidden [bool] $bSendingOMSEvents + OMSOutput() + { + $this.bSendingOMSEvents = $false #Gets set later when command-started event fires. + } + + [void] SetSendingOMSEvents() + { + $this.bSendingOMSEvents = $true + } + + [bool] IsSendingOMSEvents() + { + return $this.bSendingOMSEvents + } + + static [OMSOutput] GetInstance() + { + if($null -eq [OMSOutput]::Instance) + { + [OMSOutput]::Instance = [OMSOutput]::new(); + } + return [OMSOutput]::Instance; + } + + [void] RegisterEvents() + { + $this.UnregisterEvents(); + + # Mandatory: Generate Run Identifier Event + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [OMSOutput]::GetInstance(); + try + { + $currentInstance.SetRunIdentifier([AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1)); + [OMSOutput]::IsIssueLogged = $false + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandStarted, { + $currentInstance = [OMSOutput]::GetInstance(); + try + { + [OMSHelper]::SetOMSDetails($currentInstance); #This will also set the IsSendingOMSEvents flag. + + if ($currentInstance.IsSendingOMSEvents()) #All similar checks except this one can be outside the try-catch. + { + $currentInstance.CommandAction($Event,"Command Started"); + } + } + catch{ + $currentInstance.PublishException($_); + } + + #TODO: Disabling OMS inventory call. Need to rework on performance part. + # if(-not ([OMSHelper]::isOMSSettingValid -eq -1 -and [OMSHelper]::isAltOMSSettingValid -eq -1)) + # { + # try + # { + # $invocationContext = [System.Management.Automation.InvocationInfo] $currentInstance.InvocationContext + # if(!$invocationContext.BoundParameters.ContainsKey("tenantId")) {return;} + # [OMSHelper]::PostResourceInventory($currentInstance.GetAzSKContextDetails()) + # } + # catch + # { + # $currentInstance.PublishException($_); + # } + # } + }); + + + $this.RegisterEvent([AzSKRootEvent]::CommandStarted, { + $currentInstance = [OMSOutput]::GetInstance(); + #BUGBUG: Should there be a SetOMSDetails() here as well? (See above.) + if ($currentInstance.IsSendingOMSEvents()) + { + try + { + $currentInstance.CommandAction($Event,"Command Started"); + } + catch + { + $currentInstance.PublishException($_); + } + } + }); + + + $this.RegisterEvent([AzSKRootEvent]::CommandCompleted, { + $currentInstance = [OMSOutput]::GetInstance(); + if ($currentInstance.IsSendingOMSEvents()) + { + try + { + $currentInstance.CommandAction($Event,"Command Completed"); + + } + catch + { + $currentInstance.PublishException($_); + } + } + }); + + $this.RegisterEvent([SVTEvent]::CommandCompleted, { + $currentInstance = [OMSOutput]::GetInstance(); + if ($currentInstance.IsSendingOMSEvents()) + { + try + { + + $currentInstance.CommandAction($Event,"Command Completed"); + } + catch + { + $currentInstance.PublishException($_); + } + } + }); + + + $this.RegisterEvent([SVTEvent]::EvaluationCompleted, { + $currentInstance = [OMSOutput]::GetInstance(); + if ($currentInstance.IsSendingOMSEvents()) + { + try + { + $invocationContext = [System.Management.Automation.InvocationInfo] $currentInstance.InvocationContext + $SVTEventContexts = [SVTEventContext[]] $Event.SourceArgs + #foreach($svtEventContext in $SVTEventContexts) + #{ + # $currentInstance.WriteControlResult($svtEventContext); + #} + $currentInstance.WriteControlResult($SVTEventContexts); + } + catch + { + $currentInstance.PublishException($_); + } + } + }); + + + # $this.RegisterEvent([SVTEvent]::WriteInventory, { + # $currentInstance = [OMSOutput]::GetInstance(); + # try + # { + # [OMSHelper]::SetOMSDetails($currentInstance); + # if(-not ([OMSHelper]::isOMSSettingValid -eq -1 -and [OMSHelper]::isAltOMSSettingValid -eq -1)) + # { + # $invocationContext = [System.Management.Automation.InvocationInfo] $currentInstance.InvocationContext + # $SVTEventContexts = [SVTEventContext[]] $Event.SourceArgs + + # [OMSHelper]::PostApplicableControlSet($SVTEventContexts,$currentInstance.GetAzSKContextDetails()); + # } + # } + # catch + # { + # $currentInstance.PublishException($_); + # } + # }); + } + + hidden [void] WriteControlResult([SVTEventContext[]] $eventContextAll) + { + try + { + $settings = [ConfigurationManager]::GetAzSKSettings() + $tempBodyObjectsAll = [System.Collections.ArrayList]::new() + + try{ + + if((-not [string]::IsNullOrWhiteSpace($settings.OMSWorkspaceId)) -or (-not [string]::IsNullOrWhiteSpace($settings.AltOMSWorkspaceId))) + { + $eventContextAll | ForEach-Object{ + $eventContext = $_ + $tempBodyObjects = [OMSHelper]::GetOMSBodyObjects($eventContext,$this.GetAzSKContextDetails()) + + $tempBodyObjects | ForEach-Object{ + Set-Variable -Name tempBody -Value $_ -Scope Local + $tempBodyObjectsAll.Add($tempBody) + } + } + + $body = $tempBodyObjectsAll | ConvertTo-Json + $omsBodyByteArray = ([System.Text.Encoding]::UTF8.GetBytes($body)) + + #publish to primary workspace + if(-not [string]::IsNullOrWhiteSpace($settings.OMSWorkspaceId) -and [OMSHelper]::isOMSSettingValid -ne -1) + { + [OMSHelper]::PostOMSData($settings.OMSWorkspaceId, $settings.OMSSharedKey, $omsBodyByteArray, $settings.OMSType, 'OMS') + } + + #publish to secondary workspace + if(-not [string]::IsNullOrWhiteSpace($settings.AltOMSWorkspaceId) -and [OMSHelper]::isAltOMSSettingValid -ne -1) + { + [OMSHelper]::PostOMSData($settings.AltOMSWorkspaceId, $settings.AltOMSSharedKey, $omsBodyByteArray, $settings.OMSType, 'AltOMS') + } + } + + + } + catch + { + if(-not [OMSOutput]::IsIssueLogged) #TODO: consider keeping track of failed attempts and stop attempting to send to OMS? (May need to tweak SetSendingToOMS/IsSendingToOMS logic) + { + $this.PublishCustomMessage("An error occurred while pushing data to OMS. Please check logs for more details. AzSK control evaluation results will not be sent to the configured OMS workspace from this environment until the error is resolved.", [MessageType]::Warning); + $this.PublishException($_); + [OMSOutput]::IsIssueLogged = $true + } + } + } + catch + { + [Exception] $ex = [Exception]::new("Error sending events to OMS. The following exception occurred: `r`n$($_.Exception.Message) `r`nFor more on AzSK OMS setup, refer: https://aka.ms/devopskit/ca", $_.Exception) + throw [SuppressedException] $ex + } + + } + + hidden [AzSKContextDetails] GetAzSKContextDetails() + { + #TODO-Perf: Can we not cache this for reuse after creating it once? (Perhaps cache in the OMSOutput object for reuse per-cmdlet run?) + $AzSKContext = [AzSKContextDetails]::new(); + $AzSKContext.RunIdentifier= $this.RunIdentifier; + $commandMetadata = $this.GetCommandMetadata(); + if($commandMetadata) + { + $AzSKContext.RunIdentifier += "_" + $commandMetadata.ShortName; + } + $AzSKContext.Version = $scannerVersion = $this.GetCurrentModuleVersion() + $settings = [ConfigurationManager]::GetAzSKSettings() + + if(-not [string]::IsNullOrWhiteSpace($settings.OMSSource)) + { + $AzSKContext.Source = $settings.OMSSource + } + else + { + $AzSKContext.Source = [OMSOutput]::DefaultOMSSource + } + $AzSKContext.PolicyOrgName = [ConfigurationManager]::GetAzSKConfigData().PolicyOrgName + + return $AzSKContext + } + + hidden [void] CommandAction($event,$eventName) + { + $arg = $event.SourceArgs | Select-Object -First 1; + + $commandModel = [CommandModel]::new() + $commandModel.EventName = $eventName + $commandModel.RunIdentifier = $this.RunIdentifier + $commandModel.ModuleVersion= $this.GetCurrentModuleVersion(); + $commandModel.ModuleName = $this.GetModuleName(); + $commandModel.MethodName = $this.InvocationContext.InvocationName; + $commandModel.Parameters =$(($this.InvocationContext.BoundParameters | Out-String).TrimEnd()) + + if([Helpers]::CheckMember($arg,"TenantContext")) + { + $commandModel.tenantId = $arg.TenantContext.tenantId + $commandModel.TenantName = $arg.TenantContext.TenantName + } + if([Helpers]::CheckMember($arg,"PartialScanIdentifier")) + { + $commandModel.PartialScanIdentifier = $arg.PartialScanIdentifier + } + [OMSHelper]::WriteControlResult($commandModel,[OMSHelper]::CommandEventType) + } +} + + + + + diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/RemoteReports/AIOrgTelemetry.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/RemoteReports/AIOrgTelemetry.ps1 new file mode 100644 index 000000000..517ae4b99 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/RemoteReports/AIOrgTelemetry.ps1 @@ -0,0 +1,383 @@ +Set-StrictMode -Version Latest + +#There is only one instance of this per session! +class AIOrgTelemetry: ListenerBase { + [Microsoft.ApplicationInsights.TelemetryClient] $TelemetryClient; + + hidden AIOrgTelemetry() { + $this.TelemetryClient = [Microsoft.ApplicationInsights.TelemetryClient]::new() + } + + hidden static [AIOrgTelemetry] $Instance = $null; + + static [AIOrgTelemetry] GetInstance() { + if ( $null -eq [AIOrgTelemetry]::Instance -or $null -eq [AIOrgTelemetry]::Instance.TelemetryClient) { + [AIOrgTelemetry]::Instance = [AIOrgTelemetry]::new(); + } + return [AIOrgTelemetry]::Instance + } + + [void] RegisterEvents() { + $this.UnregisterEvents(); + + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [AIOrgTelemetry]::GetInstance(); + try + { + $runIdentifier = [AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1) + $currentInstance.SetRunIdentifier($runIdentifier); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::EvaluationCompleted, { + $currentInstance = [AIOrgTelemetry]::GetInstance(); + try + { + $invocationContext = [System.Management.Automation.InvocationInfo] $currentInstance.InvocationContext + $SVTEventContexts = [SVTEventContext[]] $Event.SourceArgs + $featureGroup = [RemoteReportHelper]::GetFeatureGroup($SVTEventContexts) + if($featureGroup -eq [FeatureGroup]::Subscription){ + $currentInstance.PushSubscriptionScanResults($SVTEventContexts) + }elseif($featureGroup -eq [FeatureGroup]::Service){ + $currentInstance.PushServiceScanResults($SVTEventContexts) + }else{ + } + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([AzSKGenericEvent]::Exception, { + $currentInstance = [AIOrgTelemetry]::GetInstance(); + try + { + [System.Management.Automation.ErrorRecord] $er = ($Event.SourceArgs | Select-Object -First 1) + [AIOrgTelemetryHelper]::TrackException($er, $currentInstance.InvocationContext) + } + catch + { + # Handling error while registration of Exception event. + # No need to break execution + } + }); + + $this.RegisterEvent([AzSKRootEvent]::CommandError, { + $currentInstance = [AIOrgTelemetry]::GetInstance(); + try + { + [System.Management.Automation.ErrorRecord] $er = $Event.SourceArgs.ExceptionMessage + [AIOrgTelemetryHelper]::TrackException($er, $currentInstance.InvocationContext) + } + catch + { + # Handling error while registration of CommandError event at AzSKRoot. + # No need to break execution + } + }); + + $this.RegisterEvent([SVTEvent]::CommandError, { + $currentInstance = [AIOrgTelemetry]::GetInstance(); + try + { + [System.Management.Automation.ErrorRecord] $er = $Event.SourceArgs.ExceptionMessage + [AIOrgTelemetryHelper]::TrackException($er, $currentInstance.InvocationContext) + } + catch + { + # Handling error while registration of CommandError event at SVT. + # No need to break execution + } + }); + + $this.RegisterEvent([SVTEvent]::EvaluationError, { + $currentInstance = [AIOrgTelemetry]::GetInstance(); + try + { + [System.Management.Automation.ErrorRecord] $er = $Event.SourceArgs.ExceptionMessage + [AIOrgTelemetryHelper]::TrackException($er, $currentInstance.InvocationContext) + } + catch + { + # Handling error while registration of EvaluationError event at SVT. + # No need to break execution + } + }); + + $this.RegisterEvent([SVTEvent]::ControlError, { + $currentInstance = [AIOrgTelemetry]::GetInstance(); + try + { + [System.Management.Automation.ErrorRecord] $er = $Event.SourceArgs.ExceptionMessage + [AIOrgTelemetryHelper]::TrackException($er, $currentInstance.InvocationContext) + } + catch + { + # Handling error while registration of ControlError event at SVT. + # No need to break execution + } + }); + + $this.RegisterEvent([SVTEvent]::CommandStarted, { + $currentInstance = [RemoteReportsListener]::GetInstance(); + try + { + $scanSource = [RemoteReportHelper]::GetScanSource(); + if($scanSource -ne [ScanSource]::Runbook) { return; } + $tenantId = ([AccountHelper]::GetCurrentRmContext()).Subscription.Id; + $resources= Get-AzResource + $resourceGroups = Get-AzResourceGroup + $telemetryEvents = [System.Collections.ArrayList]::new() + foreach($res in $resources){ + $rgTags = ($resourceGroups | where-object {$_.Name -eq $res.ResourceGroupName}).Tags; + $resourceProperties = @{ + "Name" = $res.Name; + "ResourceId" = $res.ResourceId; + "ResourceName" = $res.Name; + "ResourceType" = $res.ResourceType; + "ResourceGroupName" = $res.ResourceGroupName; + "Location" = $res.Location; + "tenantId" = $tenantId; + "Tags" = [Helpers]::FetchTagsString($res.Tags); + "Env" = $res.Tags.Env; + "ComponentID" = $res.Tags.ComponentID; + "RGComponentID" = $rgTags.ComponentID; + "RGEnv" = $rgTags.Env; + } + $telemetryEvent = "" | Select-Object Name, Properties, Metrics + $telemetryEvent.Name = "Resource Inventory" + $telemetryEvent.Properties = $resourceProperties + $telemetryEvents.Add($telemetryEvent) | Out-Null + } + [AIOrgTelemetryHelper]::TrackEvents($telemetryEvents); + + } + catch{ + $currentInstance.PublishException($_); + } + }); + } + + hidden [void] PushSubscriptionScanResults([SVTEventContext[]] $SVTEventContexts) + { + $SVTEventContextFirst = $SVTEventContexts[0] + $baseProperties = @{ + "RunIdentifier" = $this.RunIdentifier; + [TelemetryKeys]::FeatureGroup = [FeatureGroup]::Subscription; + "ScanKind" = [RemoteReportHelper]::GetSubscriptionScanKind( + $this.InvocationContext.MyCommand.Name, + $this.InvocationContext.BoundParameters); + "SubscriptionMetadata" = [Helpers]::ConvertToJsonCustomCompressed($SVTEventContextFirst.TenantContext.SubscriptionMetadata); + } + $this.PushControlResults($SVTEventContexts, $baseProperties) + } + + hidden [void] PushServiceScanResults([SVTEventContext[]] $SVTEventContexts) + { + $SVTEventContextFirst = $SVTEventContexts[0] + $baseProperties = @{ + "RunIdentifier" = $this.RunIdentifier; + [TelemetryKeys]::FeatureGroup = [FeatureGroup]::Service; + "ScanKind" = [RemoteReportHelper]::GetServiceScanKind( + $this.InvocationContext.MyCommand.Name, + $this.InvocationContext.BoundParameters); + "Feature" = $SVTEventContextFirst.FeatureName; + "ResourceGroup" = $SVTEventContextFirst.ResourceContext.ResourceGroupName; + "ResourceName" = $SVTEventContextFirst.ResourceContext.ResourceName; + "ResourceId" = $SVTEventContextFirst.ResourceContext.ResourceId; + "ResourceMetadata" = [Helpers]::ConvertToJsonCustomCompressed($SVTEventContextFirst.ResourceContext.ResourceMetadata); + } + $this.PushControlResults($SVTEventContexts, $baseProperties) + } + + hidden [void] PushControlResults([SVTEventContext[]] $SVTEventContexts, [hashtable] $BaseProperties){ + $telemetryEvents = [System.Collections.ArrayList]::new() + foreach($context in $SVTEventContexts){ + $propertiesCollection = $this.AttachControlProperties($BaseProperties, $context) + foreach($properties in $propertiesCollection){ + $telemetryEvent = "" | Select-Object Name, Properties, Metrics + $telemetryEvent.Name = "Control Scanned" + $telemetryEvent.Properties = $properties + $telemetryEvent = [AIOrgTelemetry]::SetCommonProperties($telemetryEvent); + $telemetryEvents.Add($telemetryEvent) | Out-Null + } + } + [AIOrgTelemetryHelper]::TrackEvents($telemetryEvents); + } + + + hidden [hashtable[]] AttachControlProperties([hashtable] $BaseProperties, [SVTEventContext] $context){ + if($null -eq $context) {return ([hashtable[]]([System.Collections.ArrayList]::new()))} + $properties = @{} + if ($null -ne $BaseProperties) { + $properties = $BaseProperties.Clone() + } + $propertiesArray = [System.Collections.ArrayList]::new() + $properties.Add("ControlIntId", $context.ControlItem.Id); + $properties.Add("ControlId", $context.ControlItem.ControlID); + $properties.Add("ControlSeverity", $context.ControlItem.ControlSeverity); + $properties.Add("IsBaselineControl", $context.ControlItem.IsBaselineControl) + if (!$context.ControlItem.Enabled) { + $properties.Add("VerificationResult", [VerificationResult]::Disabled) + $properties.Add("AttestationStatus", [AttestationStatus]::None) + $propertiesArray.Add($properties) | Out-Null + }else{ + $results = $context.ControlResults + if($results.Count -eq 1){ + $properties.Add("HasAttestationWritePermissions", $results[0].CurrentSessionContext.Permissions.HasAttestationWritePermissions) + $properties.Add("HasAttestationReadPermissions", $results[0].CurrentSessionContext.Permissions.HasAttestationReadPermissions) + $properties.Add("ActualVerificationResult", $results[0].ActualVerificationResult) + $properties.Add("AttestationStatus", $results[0].AttestationStatus) + $properties.Add("VerificationResult", $results[0].VerificationResult) + $properties.Add("HasRequiredAccess", $results[0].CurrentSessionContext.Permissions.HasRequiredAccess) + if($null -ne $context.ResourceContext){ + if($context.ResourceContext.ResourceName -eq $results[0].ChildResourceName -or [string]::IsNullOrWhiteSpace($results[0].ChildResourceName)){ + $properties.Add("IsNestedResource", 'No') + $properties.Add("NestedResourceName", "NA") + }else{ + $properties.Add("IsNestedResource", 'Yes') + $properties.Add("NestedResourceName", $results[0].ChildResourceName) + } + } + if(($null -ne $results[0].StateManagement) -and ($null -ne $results[0].StateManagement.AttestedStateData)) { + $properties.Add("AttestedBy", $results[0].StateManagement.AttestedStateData.AttestedBy) + $properties.Add("Justification", $results[0].StateManagement.AttestedStateData.Justification) + $properties.Add("AttestedState", [Helpers]::ConvertToJsonCustomCompressed($results[0].StateManagement.AttestedStateData.DataObject)) + $properties.Add("AttestedDate", ($results[0].StateManagement.AttestedStateData.AttestedDate).Tostring("yyyy_MM_dd_hh_mm")) + $properties.Add("ExpiryDate", ([DateTime]$results[0].StateManagement.AttestedStateData.ExpiryDate).Tostring("yyyy_MM_dd_hh_mm")) + } + if(($null -ne $results[0].StateManagement) -and ($null -ne $results[0].StateManagement.CurrentStateData)) { + $properties.Add("CurrentState", [Helpers]::ConvertToJsonCustomCompressed($results[0].StateManagement.CurrentStateData.DataObject)) + } + $propertiesArray.Add($properties) | Out-Null + }elseif($results.Count -gt 1){ + $properties.Add("IsNestedResource", 'Yes') + foreach($result in $results){ + $propertiesIn = $properties.Clone() + $propertiesIn.Add("ActualVerificationResult", $result.ActualVerificationResult) + $propertiesIn.Add("AttestationStatus", $result.AttestationStatus) + $propertiesIn.Add("VerificationResult", $result.VerificationResult) + $propertiesIn.Add("NestedResourceName", $result.ChildResourceName) + $propertiesIn.Add("HasRequiredAccess", $result.CurrentSessionContext.Permissions.HasRequiredAccess) + if(($null -ne $result.StateManagement) -and ($null -ne $result.StateManagement.AttestedStateData)) { + $propertiesIn.Add("AttestedBy", $result.StateManagement.AttestedStateData.AttestedBy) + $propertiesIn.Add("Justification", $result.StateManagement.AttestedStateData.Justification) + $propertiesIn.Add("AttestedState", [Helpers]::ConvertToJsonCustomCompressed($result.StateManagement.AttestedStateData.DataObject)) + $propertiesIn.Add("AttestedDate", ($result.StateManagement.AttestedStateData.AttestedDate).Tostring("yyyy_MM_dd_hh_mm")) + $propertiesIn.Add("ExpiryDate", ([DateTime]$result.StateManagement.AttestedStateData.ExpiryDate).Tostring("yyyy_MM_dd_hh_mm")) + } + if(($null -ne $result.StateManagement) -and ($null -ne $result.StateManagement.CurrentStateData)) { + $propertiesIn.Add("CurrentState", [Helpers]::ConvertToJsonCustomCompressed($result.StateManagement.CurrentStateData.DataObject)) + } + $propertiesArray.Add($propertiesIn) | Out-Null + } + } + } + $returnObj = [hashtable[]] $propertiesArray + return $returnObj; + } + + static [psobject] SetCommonProperties([psobject] $telemetryEvent) + { + try + { + $NA = "NA"; + try { + $telemetryEvent.properties.Add("ScanSource", [RemoteReportHelper]::GetScanSource()); + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try { + $module = Get-Module 'AzSK*' | Select-Object -First 1 + $telemetryEvent.properties.Add("ScannerModuleName", $module.Name); + $telemetryEvent.properties.Add("ScannerVersion", $module.Version.ToString()); + $telemetryEvent.properties.Add("OrgVersion", [ConfigurationManager]::GetAzSKConfigData().GetLatestAzSKVersion($module.Name).ToString()); + $telemetryEvent.properties.Add("PolicyOrgName", [ConfigurationManager]::GetAzSKConfigData().PolicyOrgName) + $AzSKLatestVersion= [ConfigurationManager]::GetAzSKConfigData().GetAzSKLatestPSGalleryVersion($module.Name) + $telemetryEvent.properties.Add("LatestVersion", $AzSKLatestVersion); + + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try { + $azureContext = [AccountHelper]::GetCurrentRmContext() + try { + $telemetryEvent.properties.Add([TelemetryKeys]::tenantId, $azureContext.Subscription.Id) + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try { + $telemetryEvent.properties.Add([TelemetryKeys]::TenantName, $azureContext.Subscription.Name) + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try { + $telemetryEvent.properties.Add("AzureEnv", $azureContext.Environment.Name) + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try { + $telemetryEvent.properties.Add("TenantId", $azureContext.Tenant.Id) + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try { + $telemetryEvent.properties.Add("AccountId", $azureContext.Account.Id) + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try { + if ($telemetryEvent.Properties.ContainsKey("RunIdentifier")) { + $actualRunId = $telemetryEvent.Properties["RunIdentifier"] + if ($telemetryEvent.Properties.ContainsKey("UniqueRunIdentifier")) { + $telemetryEvent.Properties["UniqueRunIdentifier"] = [RemoteReportHelper]::Mask($azureContext.Account.Id + '##' + $actualRunId.ToString()) + } + else + { + $telemetryEvent.properties.Add("UniqueRunIdentifier", [RemoteReportHelper]::Mask($azureContext.Account.Id + '##' + $actualRunId.ToString())) + } + } + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try { + $telemetryEvent.properties.Add("AccountType", $azureContext.Account.Type); + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + return $telemetryEvent; + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/RemoteReports/RemoteReportsListener.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/RemoteReports/RemoteReportsListener.ps1 new file mode 100644 index 000000000..98d66818c --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/RemoteReports/RemoteReportsListener.ps1 @@ -0,0 +1,309 @@ +Set-StrictMode -Version Latest + +#This is used to send events to the controls API (to directly save to org DB) +class RemoteReportsListener: ListenerBase { + + hidden RemoteReportsListener() { + } + + hidden static [RemoteReportsListener] $Instance = $null; + + static [RemoteReportsListener] GetInstance() { + if ( $null -eq [RemoteReportsListener]::Instance ) { + [RemoteReportsListener]::Instance = [RemoteReportsListener]::new(); + } + return [RemoteReportsListener]::Instance + } + + [void] RegisterEvents() { + $this.UnregisterEvents(); + + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [RemoteReportsListener]::GetInstance(); + try + { + $runIdentifier = [AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1) + $currentInstance.SetRunIdentifier($runIdentifier); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandStarted, { + $currentInstance = [RemoteReportsListener]::GetInstance(); + try + { + + $scanSource = [RemoteReportHelper]::GetScanSource(); + if($scanSource -ne [ScanSource]::Runbook) { return; } + [ResourceInventory]::FetchResources(); + [RemoteReportsListener]::ReportAllResources(); + $invocationContext = [System.Management.Automation.InvocationInfo] $currentInstance.InvocationContext + if(!$invocationContext.BoundParameters.ContainsKey("tenantId")) {return;} + $resources = "" | Select-Object "tenantId", "ResourceGroups" + $resources.tenantId = $invocationContext.BoundParameters["tenantId"] + $resources.ResourceGroups = [System.Collections.ArrayList]::new() + $supportedResourceTypes = [SVTMapping]::GetSupportedResourceMap() + # # Not considering nested resources to reduce complexity + $filteredResources = [ResourceInventory]::FilteredResources | Where-Object { $supportedResourceTypes.ContainsKey($_.ResourceType.ToLower()) } + $grouped = $filteredResources | Group-Object {$_.ResourceGroupName} | Select-Object Name, Group + foreach($group in $grouped){ + $resourceGroup = "" | Select-Object Name, Resources + $resourceGroup.Name = $group.Name + $resourceGroup.Resources = [System.Collections.ArrayList]::new() + foreach($item in $group.Group){ + $resource = "" | Select-Object Name, ResourceId, Feature + if($item.Name.Contains("/")){ + $splitName = $item.Name.Split("/") + $resource.Name = $splitName[$splitName.Length - 1] + } + else{ + $resource.Name = $item.Name; + } + $resource.ResourceId = $item.ResourceId + $resource.Feature = $supportedResourceTypes[$item.ResourceType.ToLower()] + $resourceGroup.Resources.Add($resource) | Out-Null + } + $resources.ResourceGroups.Add($resourceGroup) | Out-Null + } + [RemoteApiHelper]::PostResourceInventory($resources) + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::EvaluationCompleted, { + $currentInstance = [RemoteReportsListener]::GetInstance(); + try + { + $settings = [ConfigurationManager]::GetAzSKConfigData(); + if(!$settings.PublishVulnDataToApi) {return;} + $invocationContext = [System.Management.Automation.InvocationInfo] $currentInstance.InvocationContext + $SVTEventContexts = [SVTEventContext[]] $Event.SourceArgs + $featureGroup = [RemoteReportHelper]::GetFeatureGroup($SVTEventContexts) + if($featureGroup -eq [FeatureGroup]::Subscription){ + [RemoteReportsListener]::ReportSubscriptionScan($currentInstance, $invocationContext, $SVTEventContexts) + }elseif($featureGroup -eq [FeatureGroup]::Service){ + [RemoteReportsListener]::ReportServiceScan($currentInstance, $invocationContext, $SVTEventContexts) + }else{ + + } + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([AzSKRootEvent]::PublishCustomData, { + $currentInstance = [RemoteReportsListener]::GetInstance(); + try + { + $CustomDataObj = $Event.SourceArgs + $CustomObjectData=$CustomDataObj| Select-Object -exp Messages|select -exp DataObject + if($CustomObjectData.Name -eq "SubSVTObject") + { + $subSVTObject = $CustomObjectData.Value; + $currentInstance.FetchRBACTelemetry($subSVTObject); + [RemoteApiHelper]::PostRBACTelemetry(($subSVTObject.CustomObject.Value)); + } + elseif($CustomObjectData.Name -eq "FeatureControlTelemetry") + { + [RemoteApiHelper]::PushFeatureControlsTelemetry($CustomObjectData.Value); + } + #| select -exp Value; + + } + catch + { + $currentInstance.PublishException($_); + } + }); + + + } + + + static [void] ReportAllResources() + { + $currentInstance = [RemoteReportsListener]::GetInstance(); + $invocationContext = [System.Management.Automation.InvocationInfo] $currentInstance.InvocationContext + $tenantId = ([AccountHelper]::GetCurrentRmContext()).Subscription.Id; + $resourceGroups = Get-AzResourceGroup + $resourcesDetails = @(); + $resourcesFlat = [ResourceInventory]::RawResources + foreach($res in $resourcesFlat){ + $resourceGroup = ($resourceGroups | where-object {$_.ResourceGroupName -eq $res.ResourceGroupName}); + $resEnv = ""; + $resComponentId = ""; + $rgEnv = ""; + $rgComponentId = ""; + if([Helpers]::CheckMember($resourceGroup, "Tags")) { + $rgTags = $resourceGroup.Tags; + if($rgTags.ContainsKey("Env")) + { + $rgEnv = $rgTags.Env; + } + if($rgTags.ContainsKey("ComponentID")) + { + $rgComponentId = $rgTags.ComponentID; + } + } + if([Helpers]::CheckMember($res, "Tags")) + { + $resTags = $res.Tags; + if($resTags.ContainsKey("Env")) + { + $resEnv = $resTags.Env; + } + if($resTags.ContainsKey("ComponentID")) + { + $resComponentId = $resTags.ComponentID; + } + } + $resourceProperties = @{ + "Name" = $res.Name; + "ResourceId" = $res.ResourceId; + "ResourceName" = $res.Name; + "ResourceType" = $res.ResourceType; + "ResourceGroupName" = $res.ResourceGroupName; + "Location" = $res.Location; + "tenantId" = $tenantId; + "Sku" = $res.Sku; + "Tags" = [Helpers]::FetchTagsString($res.Tags); + "Env" = $resEnv; + "ComponentID" = $resComponentId; + "RGComponentID" = $rgComponentId; + "RGEnv" = $rgEnv; + } + $resourcesDetails += $resourceProperties; + } + [RemoteApiHelper]::PostResourceFlatInventory($resourcesDetails) + } + + + static [void] ReportSubscriptionScan( + [RemoteReportsListener] $publisher, ` + [System.Management.Automation.InvocationInfo] $invocationContext, ` + [SVTEventContext[]] $SVTEventContexts) + { + $SVTEventContext = $SVTEventContexts[0] + $scanResult = [SubscriptionScanInfo]::new() + $scanResult.ScanKind = [RemoteReportHelper]::GetSubscriptionScanKind($invocationContext.MyCommand.Name, $invocationContext.BoundParameters) + $scanResult.tenantId = $SVTEventContext.TenantContext.tenantId + $scanResult.TenantName = $SVTEventContext.TenantContext.TenantName + $scanResult.Source = [RemoteReportHelper]::GetScanSource() + $scanResult.ScannerVersion = $publisher.GetCurrentModuleVersion() + # Using module version as control version by default + $scanResult.ControlVersion = $publisher.GetCurrentModuleVersion() + $scanResult.Metadata = [Helpers]::ConvertToJsonCustomCompressed($SVTEventContext.TenantContext.SubscriptionMetadata) + if(($SVTEventContexts | Measure-Object).Count -gt 0 -and ($SVTEventContexts[0].ControlResults | Measure-Object).Count -gt 0) + { + $TempCtrlResult = $SVTEventContexts[0].ControlResults[0]; + $scanResult.HasAttestationWritePermissions = $TempCtrlResult.CurrentSessionContext.Permissions.HasAttestationWritePermissions + $scanResult.HasAttestationReadPermissions = $TempCtrlResult.CurrentSessionContext.Permissions.HasAttestationReadPermissions + $scanResult.IsLatestPSModule = $TempCtrlResult.CurrentSessionContext.IsLatestPSModule + } + $results = [System.Collections.ArrayList]::new() + $SVTEventContexts | ForEach-Object { + $context = $_ + if ($context.ControlItem.Enabled) { + $result = [RemoteReportHelper]::BuildSubscriptionControlResult($context.ControlResults[0], $context.ControlItem) + $results.Add($result) + } + else { + $result = [SubscriptionControlResult]::new() + $result.ControlId = $context.ControlItem.ControlID + $result.ControlIntId = $context.ControlItem.Id + $result.ActualVerificationResult = [VerificationResult]::Disabled + $result.AttestationStatus = [AttestationStatus]::None + $result.VerificationResult = [VerificationResult]::Disabled + $result.MaximumAllowedGraceDays = $context.MaximumAllowedGraceDays + $results.Add($result) + } + } + $scanResult.ControlResults = [SubscriptionControlResult[]] $results + [RemoteApiHelper]::PostSubscriptionScanResult($scanResult) + } + + static [void] ReportServiceScan( + [RemoteReportsListener] $publisher, ` + [System.Management.Automation.InvocationInfo] $invocationContext, ` + [SVTEventContext[]] $SVTEventContexts) + { + $SVTEventContextFirst = $SVTEventContexts[0] + $scanResult = [ServiceScanInfo]::new() + $scanResult.ScanKind = [RemoteReportHelper]::GetServiceScanKind($invocationContext.MyCommand.Name, $invocationContext.BoundParameters) + $scanResult.tenantId = $SVTEventContextFirst.TenantContext.tenantId + $scanResult.TenantName = $SVTEventContextFirst.TenantContext.TenantName + $scanResult.Source = [RemoteReportHelper]::GetScanSource() + $scanResult.ScannerVersion = $publisher.GetCurrentModuleVersion() + # Using module version as control version by default + $scanResult.ControlVersion = $publisher.GetCurrentModuleVersion() + $scanResult.Feature = $SVTEventContextFirst.FeatureName + $scanResult.ResourceGroup = $SVTEventContextFirst.ResourceContext.ResourceGroupName + $scanResult.ResourceName = $SVTEventContextFirst.ResourceContext.ResourceName + $scanResult.ResourceId = $SVTEventContextFirst.ResourceContext.ResourceId + $scanResult.Metadata = [Helpers]::ConvertToJsonCustomCompressed($SVTEventContextFirst.ResourceContext.ResourceMetadata) + + if(($SVTEventContexts | Measure-Object).Count -gt 0 -and ($SVTEventContexts[0].ControlResults | Measure-Object).Count -gt 0) + { + $TempCtrlResult = $SVTEventContexts[0].ControlResults[0]; + $scanResult.HasAttestationWritePermissions = $TempCtrlResult.CurrentSessionContext.Permissions.HasAttestationWritePermissions + $scanResult.HasAttestationReadPermissions = $TempCtrlResult.CurrentSessionContext.Permissions.HasAttestationReadPermissions + $scanResult.IsLatestPSModule = $TempCtrlResult.CurrentSessionContext.IsLatestPSModule + } + $results = [System.Collections.ArrayList]::new() + $SVTEventContexts | ForEach-Object { + $SVTEventContext = $_ + if (!$SVTEventContext.ControlItem.Enabled) { + $result = [ServiceControlResult]::new() + $result.ControlId = $SVTEventContext.ControlItem.ControlID + $result.ControlIntId = $SVTEventContext.ControlItem.Id + $result.ControlSeverity = $SVTEventContext.ControlItem.ControlSeverity + $result.ActualVerificationResult = [VerificationResult]::Disabled + $result.AttestationStatus = [AttestationStatus]::None + $result.VerificationResult = [VerificationResult]::Disabled + $results.Add($result) + } + elseif ($SVTEventContext.ControlResults.Count -eq 1 -and ` + ($scanResult.ResourceName -eq $SVTEventContext.ControlResults[0].ChildResourceName -or ` + [string]::IsNullOrWhiteSpace($SVTEventContext.ControlResults[0].ChildResourceName))) + { + $result = [RemoteReportHelper]::BuildServiceControlResult($SVTEventContext.ControlResults[0], ` + $false, $SVTEventContext.ControlItem) + $results.Add($result) + } + elseif ($SVTEventContext.ControlResults.Count -eq 1 -and ` + $scanResult.ResourceName -ne $SVTEventContext.ControlResults[0].ChildResourceName) + { + $result = [RemoteReportHelper]::BuildServiceControlResult($SVTEventContext.ControlResults[0], ` + $true, $SVTEventContext.ControlItem) + $results.Add($result) + } + elseif ($SVTEventContext.ControlResults.Count -gt 1) + { + $SVTEventContext.ControlResults | Foreach-Object { + $result = [RemoteReportHelper]::BuildServiceControlResult($_ , ` + $true, $SVTEventContext.ControlItem) + $results.Add($result) + } + } + } + + $scanResult.ControlResults = [ServiceControlResult[]] $results + [RemoteApiHelper]::PostServiceScanResult($scanResult) + } + + hidden [void] FetchRBACTelemetry($svtObject) + { + $svtObject.GetRoleAssignments(); + $svtObject.PublishRBACTelemetryData(); + $svtObject.GetPIMRoles(); + + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/RemoteReports/TelemetryStrings.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/RemoteReports/TelemetryStrings.ps1 new file mode 100644 index 000000000..5c18c98af --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/RemoteReports/TelemetryStrings.ps1 @@ -0,0 +1,49 @@ +Set-StrictMode -Version Latest +class TelemetryKeys { + static [string] $tenantId = "tenantId"; + static [string] $TenantName = "TenantName"; + static [string] $FeatureGroup = "FeatureGroup"; + static [string] $Feature = "Feature"; + static [string] $ResourceName = "ResourceName"; + static [string] $ResourceGroup = "ResourceGroup"; + static [string] $IsInterrupted = "IsInterrupted"; + static [string] $InterruptedReason = "InterruptedReason"; + static [string] $InterruptedReasonException = "InterruptedReasonException"; + static [string] $TimeTakenInMs = "TimeTakenInMs"; + static [string] $ControlId = "ControlId"; + static [string] $ControlStatus = "ControlStatus"; + static [string] $ControlFailedReason = "ControlFailedReason"; + static [string] $NestedComplaintCount = "NestedComplaintCount"; + static [string] $NestedNonComplaintCount = "NestedNonComplaintCount"; + static [string] $NestedTotalCount = "NestedTotalCount"; + static [string] $NestedResourceName = "NestedResourceName"; + static [string] $IsNestedResourceCheck = "IsNestedResourceCheck"; +} + +class TelemetryEvents { + static [string] $OperationInterrupted = "Operation Interrupted"; + static [string] $OperationCompleted = "Operation Completed"; + static [string] $ControlScanned = "Control Scanned"; + static [string] $NestedResourceControlScanned = "Nested Resource Control Scanned"; + static [string] $AutoHealAttempted = "Auto Heal Attempted"; +} + +class TelemetryMessages { + static [string] $Yes = "Yes"; + static [string] $InterruptedReasonException = "Exception"; + static [string] $InterruptedReasonControlJSONNotFound = "ControlJSONNotFound"; + static [string] $ControlPassed = "Passed"; + static [string] $ControlFailed = "Failed"; + static [string] $ControlManual = "Manual"; + static [string] $ControlVerify = "Verify"; + static [string] $ControlDisabled = "Disabled"; + static [string] $ControlNotApplicable = "N/A"; +} + +enum TraceLevel { + Verbose + Information + Warning + Error + Fatal +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/RemoteReports/UsageTelemetry.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/RemoteReports/UsageTelemetry.ps1 new file mode 100644 index 000000000..1c71f47c8 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/RemoteReports/UsageTelemetry.ps1 @@ -0,0 +1,641 @@ +Set-StrictMode -Version Latest + +#There's only one instance of this per session. +class UsageTelemetry: ListenerBase { + [Microsoft.ApplicationInsights.TelemetryClient] $TelemetryClient; + hidden UsageTelemetry() { + $this.TelemetryClient = [Microsoft.ApplicationInsights.TelemetryClient]::new() + $this.TelemetryClient.InstrumentationKey = [Constants]::UsageTelemetryKey + } + + hidden static [UsageTelemetry] $Instance = $null; + + static [UsageTelemetry] GetInstance() { + if ( $null -eq [UsageTelemetry]::Instance -or $null -eq [UsageTelemetry]::Instance.TelemetryClient) { + [UsageTelemetry]::Instance = [UsageTelemetry]::new(); + } + return [UsageTelemetry]::Instance + } + + [void] RegisterEvents() { + $this.UnregisterEvents(); + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [UsageTelemetry]::GetInstance(); + try + { + $runIdentifier = [AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1) + $currentInstance.SetRunIdentifier($runIdentifier); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::EvaluationCompleted, { + if(-not [UsageTelemetry]::IsAnonymousTelemetryActive()) { return; } + $currentInstance = [UsageTelemetry]::GetInstance(); + try + { + $invocationContext = [System.Management.Automation.InvocationInfo] $currentInstance.InvocationContext + $SVTEventContexts = [SVTEventContext[]] $Event.SourceArgs + $featureGroup = [RemoteReportHelper]::GetFeatureGroup($SVTEventContexts) + if($featureGroup -eq [FeatureGroup]::Subscription){ + [UsageTelemetry]::PushSubscriptionScanResults($currentInstance, $SVTEventContexts) + }elseif($featureGroup -eq [FeatureGroup]::Service){ + [UsageTelemetry]::PushServiceScanResults($currentInstance, $SVTEventContexts) + }else{ + + } + } + catch + { + $currentInstance.PublishException($_); + } + $currentInstance.TelemetryClient.Flush() + }); + + $this.RegisterEvent([AzSKGenericEvent]::Exception, { + if(-not [UsageTelemetry]::IsAnonymousTelemetryActive()) { return; } + $currentInstance = [UsageTelemetry]::GetInstance(); + try + { + [System.Management.Automation.ErrorRecord] $er = ($Event.SourceArgs | Select-Object -First 1) + + [UsageTelemetry]::PushException($currentInstance, @{}, @{}, $er); + } + catch + { + # Handling error while registration of Exception event. + # No need to break execution + } + }); + + $this.RegisterEvent([AzSKRootEvent]::CommandError, { + if(-not [UsageTelemetry]::IsAnonymousTelemetryActive()) { return; } + $currentInstance = [UsageTelemetry]::GetInstance(); + try + { + [System.Management.Automation.ErrorRecord] $er = [RemoteReportHelper]::Mask($Event.SourceArgs.ExceptionMessage) + [UsageTelemetry]::PushException($currentInstance, @{}, @{}, $er); + } + catch + { + # Handling error while registration of CommandError event at AzSKRoot. + # No need to break execution + } + }); + + $this.RegisterEvent([SVTEvent]::CommandError, { + if(-not [UsageTelemetry]::IsAnonymousTelemetryActive()) { return; } + $currentInstance = [UsageTelemetry]::GetInstance(); + try + { + [System.Management.Automation.ErrorRecord] $er = [RemoteReportHelper]::Mask($Event.SourceArgs.ExceptionMessage) + [UsageTelemetry]::PushException($currentInstance, @{}, @{}, $er); + } + catch + { + # Handling error while registration of CommandError event at SVT. + # No need to break execution + } + }); + + $this.RegisterEvent([SVTEvent]::EvaluationError, { + if(-not [UsageTelemetry]::IsAnonymousTelemetryActive()) { return; } + $currentInstance = [UsageTelemetry]::GetInstance(); + try + { + [System.Management.Automation.ErrorRecord] $er = [RemoteReportHelper]::Mask($Event.SourceArgs.ExceptionMessage) + [UsageTelemetry]::PushException($currentInstance, @{}, @{}, $er); + } + catch + { + # Handling error while registration of EvaluationError event at SVT. + # No need to break execution + } + }); + + $this.RegisterEvent([SVTEvent]::ControlError, { + if(-not [UsageTelemetry]::IsAnonymousTelemetryActive()) { return; } + $currentInstance = [UsageTelemetry]::GetInstance(); + try + { + [System.Management.Automation.ErrorRecord] $er = [RemoteReportHelper]::Mask($Event.SourceArgs.ExceptionMessage) + [UsageTelemetry]::PushException($currentInstance, @{}, @{}, $er); + } + catch + { + # Handling error while registration of ControlError event at SVT. + # No need to break execution + } + }); + + $this.RegisterEvent([AzSKRootEvent]::PolicyMigrationCommandStarted, { + if(-not [UsageTelemetry]::IsAnonymousTelemetryActive()) { return; } + $currentInstance = [UsageTelemetry]::GetInstance(); + try{ + $Properties = @{ + "OrgName" = [RemoteReportHelper]::Mask($Event.SourceArgs[0]); + } + [UsageTelemetry]::SetCommonProperties($currentInstance, $Properties); + $event = [Microsoft.ApplicationInsights.DataContracts.EventTelemetry]::new() + $event.Name = "Policy Migration Started" + $Properties.Keys | ForEach-Object { + try{ + $event.Properties.Add($_, $Properties[$_].ToString()); + } + catch{ + #Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + #No need to break execution + } + } + $currentInstance.TelemetryClient.TrackEvent($event); + } + catch{ + } + }); + + $this.RegisterEvent([AzSKRootEvent]::PolicyMigrationCommandCompleted, { + if(-not [UsageTelemetry]::IsAnonymousTelemetryActive()) { return; } + $currentInstance = [UsageTelemetry]::GetInstance(); + try{ + $Properties = @{ + "OrgName" = [RemoteReportHelper]::Mask($Event.SourceArgs[0]); + } + [UsageTelemetry]::SetCommonProperties($currentInstance, $Properties); + $event = [Microsoft.ApplicationInsights.DataContracts.EventTelemetry]::new() + $event.Name = "Policy Migration Completed" + $Properties.Keys | ForEach-Object { + try{ + $event.Properties.Add($_, $Properties[$_].ToString()); + } + catch{ + } + } + $currentInstance.TelemetryClient.TrackEvent($event); + } + catch{ + } + }); + } + + static [bool] IsAnonymousTelemetryActive() + { + $azskSettings = [ConfigurationManager]::GetAzSKSettings(); + if($azskSettings.UsageTelemetryLevel -eq "Anonymous") + { + return $true; + } + else + { + return $false; + } + } + + static [void] PushSubscriptionScanResults( + [UsageTelemetry] $Publisher, ` + [SVTEventContext[]] $SVTEventContexts) + { + $eventData = @{ + [TelemetryKeys]::FeatureGroup = [FeatureGroup]::Subscription; + "ScanKind" = [RemoteReportHelper]::GetSubscriptionScanKind( + $Publisher.InvocationContext.MyCommand.Name, + $Publisher.InvocationContext.BoundParameters); + } + $subscriptionscantelemetryEvents = [System.Collections.ArrayList]::new() + + $SVTEventContexts | ForEach-Object { + $context = $_ + [hashtable] $eventDataClone = $eventData.Clone(); + $eventDataClone.Add("ControlIntId", $context.ControlItem.Id); + $eventDataClone.Add("ControlId", $context.ControlItem.ControlID); + $eventDataClone.Add("ControlSeverity", $context.ControlItem.ControlSeverity); + if ($context.ControlItem.Enabled) { + $eventDataClone.Add("ActualVerificationResult", $context.ControlResults[0].ActualVerificationResult) + $eventDataClone.Add("AttestationStatus", $context.ControlResults[0].AttestationStatus) + $eventDataClone.Add("VerificationResult", $context.ControlResults[0].VerificationResult) + } + else { + $eventDataClone.Add("ActualVerificationResult", [VerificationResult]::Disabled) + $eventDataClone.Add("AttestationStatus", [AttestationStatus]::None) + $eventDataClone.Add("VerificationResult", [VerificationResult]::Disabled) + } + #[UsageTelemetry]::PushEvent($Publisher, $eventDataClone, @{}) + $telemetryEvent = "" | Select-Object Name, Properties, Metrics + $telemetryEvent.Name = "Control Scanned" + $telemetryEvent.Properties = $eventDataClone + $telemetryEvent = [UsageTelemetry]::SetCommonProperties($telemetryEvent,$Publisher); + $subscriptionscantelemetryEvents.Add($telemetryEvent) + } + [AIOrgTelemetryHelper]::PublishEvent($subscriptionscantelemetryEvents,"Usage") + } + + static [void] PushServiceScanResults( + [UsageTelemetry] $Publisher, ` + [SVTEventContext[]] $SVTEventContexts) + { + $NA = "NA" + $SVTEventContextFirst = $SVTEventContexts[0] + $eventData = @{ + [TelemetryKeys]::FeatureGroup = [FeatureGroup]::Service; + "ScanKind" = [RemoteReportHelper]::GetServiceScanKind( + $Publisher.InvocationContext.MyCommand.Name, + $Publisher.InvocationContext.BoundParameters); + "Feature" = $SVTEventContextFirst.FeatureName; + "ResourceGroup" = [RemoteReportHelper]::Mask($SVTEventContextFirst.ResourceContext.ResourceGroupName); + "ResourceName" = [RemoteReportHelper]::Mask($SVTEventContextFirst.ResourceContext.ResourceName); + "ResourceId" = [RemoteReportHelper]::Mask($SVTEventContextFirst.ResourceContext.ResourceId); + } + $servicescantelemetryEvents = [System.Collections.ArrayList]::new() + + $SVTEventContexts | ForEach-Object { + $SVTEventContext = $_ + [hashtable] $eventDataClone = $eventData.Clone() + $eventDataClone.Add("ControlIntId", $SVTEventContext.ControlItem.Id); + $eventDataClone.Add("ControlId", $SVTEventContext.ControlItem.ControlID); + $eventDataClone.Add("ControlSeverity", $SVTEventContext.ControlItem.ControlSeverity); + if (!$SVTEventContext.ControlItem.Enabled) { + $eventDataClone.Add("ActualVerificationResult", [VerificationResult]::Disabled) + $eventDataClone.Add("AttestationStatus", [AttestationStatus]::None) + $eventDataClone.Add("VerificationResult", [VerificationResult]::Disabled) + #[UsageTelemetry]::PushEvent($Publisher, $eventDataClone, @{}) + + $telemetryEvent = "" | Select-Object Name, Properties, Metrics + $telemetryEvent.Name = "Control Scanned" + $telemetryEvent.Properties = $eventDataClone + $telemetryEvent = [UsageTelemetry]::SetCommonProperties($telemetryEvent,$Publisher); + $servicescantelemetryEvents.Add($telemetryEvent) + + } + elseif ($SVTEventContext.ControlResults.Count -eq 1 -and ` + ($SVTEventContextFirst.ResourceContext.ResourceName -eq $SVTEventContext.ControlResults[0].ChildResourceName -or ` + [string]::IsNullOrWhiteSpace($SVTEventContext.ControlResults[0].ChildResourceName))) + { + $eventDataClone.Add("ActualVerificationResult", $SVTEventContext.ControlResults[0].ActualVerificationResult) + $eventDataClone.Add("AttestationStatus", $SVTEventContext.ControlResults[0].AttestationStatus) + $eventDataClone.Add("VerificationResult", $SVTEventContext.ControlResults[0].VerificationResult) + $eventDataClone.Add("IsNestedResource", 'No') + $eventDataClone.Add("NestedResourceName", $NA) + #[UsageTelemetry]::PushEvent($Publisher, $eventDataClone, @{}) + + $telemetryEvent = "" | Select-Object Name, Properties, Metrics + $telemetryEvent.Name = "Control Scanned" + $telemetryEvent.Properties = $eventDataClone + $telemetryEvent = [UsageTelemetry]::SetCommonProperties($telemetryEvent,$Publisher); + $servicescantelemetryEvents.Add($telemetryEvent) + } + elseif ($SVTEventContext.ControlResults.Count -eq 1 -and ` + $SVTEventContextFirst.ResourceContext.ResourceName -ne $SVTEventContext.ControlResults[0].ChildResourceName) + { + $eventDataClone.Add("ActualVerificationResult", $SVTEventContext.ControlResults[0].ActualVerificationResult) + $eventDataClone.Add("AttestationStatus", $SVTEventContext.ControlResults[0].AttestationStatus) + $eventDataClone.Add("VerificationResult", $SVTEventContext.ControlResults[0].VerificationResult) + $eventDataClone.Add("IsNestedResource", 'Yes') + $eventDataClone.Add("NestedResourceName", [RemoteReportHelper]::Mask($SVTEventContext.ControlResults[0].ChildResourceName)) + #[UsageTelemetry]::PushEvent($Publisher, $eventDataClone, @{}) + + $telemetryEvent = "" | Select-Object Name, Properties, Metrics + $telemetryEvent.Name = "Control Scanned" + $telemetryEvent.Properties = $eventDataClone + $telemetryEvent = [UsageTelemetry]::SetCommonProperties($telemetryEvent,$Publisher); + $servicescantelemetryEvents.Add($telemetryEvent) + } + elseif ($SVTEventContext.ControlResults.Count -gt 1) + { + $eventDataClone.Add("IsNestedResource", 'Yes') + $SVTEventContext.ControlResults | Foreach-Object { + [hashtable] $eventDataCloneL2 = $eventDataClone.Clone() + $eventDataCloneL2.Add("ActualVerificationResult", $_.ActualVerificationResult) + $eventDataCloneL2.Add("AttestationStatus", $_.AttestationStatus) + $eventDataCloneL2.Add("VerificationResult", $_.VerificationResult) + $eventDataCloneL2.Add("NestedResourceName", [RemoteReportHelper]::Mask($_.ChildResourceName)) + #[UsageTelemetry]::PushEvent($Publisher, $eventDataCloneL2, @{}) + + $telemetryEvent = "" | Select-Object Name, Properties, Metrics + $telemetryEvent.Name = "Control Scanned" + $telemetryEvent.Properties = $eventDataCloneL2 + $telemetryEvent = [UsageTelemetry]::SetCommonProperties($telemetryEvent,$Publisher); + $servicescantelemetryEvents.Add($telemetryEvent) + } + } + } + [AIOrgTelemetryHelper]::PublishEvent($servicescantelemetryEvents,"Usage") + } + + static [void] PushEvent([UsageTelemetry] $Publisher, ` + [hashtable] $Properties, [hashtable] $Metrics) + { + try{ + [UsageTelemetry]::SetCommonProperties($Publisher, $Properties); + $event = [Microsoft.ApplicationInsights.DataContracts.EventTelemetry]::new() + $event.Name = "Control Scanned" + $Properties.Keys | ForEach-Object { + try{ + $event.Properties.Add($_, $Properties[$_].ToString()); + } + catch{ + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + $Metrics.Keys | ForEach-Object { + try{ + $event.Metrics.Add($_, $Metrics[$_]); + } + catch{ + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + $Publisher.TelemetryClient.TrackEvent($event); + } + catch{ + # Eat the current exception which typically happens when network or other API issue while sending telemetry events + # No need to break execution + } + } + + static [void] PushException([UsageTelemetry] $Publisher, ` + [hashtable] $Properties, [hashtable] $Metrics, ` + [System.Management.Automation.ErrorRecord] $ErrorRecord) + { + try{ + [UsageTelemetry]::SetCommonProperties($Publisher, $Properties); + $ex = [Microsoft.ApplicationInsights.DataContracts.ExceptionTelemetry]::new() + $ex.Exception = [System.Exception]::new( [RemoteReportHelper]::Mask($ErrorRecord.Exception.ToString())) + try{ + $ex.Properties.Add("ScriptStackTrace", [UsageTelemetry]::AnonScriptStackTrace($ErrorRecord.ScriptStackTrace)) + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + $Properties.Keys | ForEach-Object { + try{ + $ex.Properties.Add($_, $Properties[$_].ToString()); + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + $Metrics.Keys | ForEach-Object { + try{ + $ex.Metrics.Add($_, $Metrics[$_]); + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + $Publisher.TelemetryClient.TrackException($ex) + $Publisher.TelemetryClient.Flush() + } + catch{ + # Handled exception occurred while publishing exception + # No need to break execution + } + } + + hidden static [void] SetCommonProperties([UsageTelemetry] $Publisher, [hashtable] $Properties) + { + try{ + $NA = "NA"; + $Properties.Add("InfoVersion", "V1"); + try{ + $Properties.Add("ScanSource", [RemoteReportHelper]::GetScanSource()); + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try{ + $Properties.Add("ScannerVersion", $Publisher.GetCurrentModuleVersion()); + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try{ + $Properties.Add("ControlVersion", $Publisher.GetCurrentModuleVersion()); + } + catch + { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try{ + # $azureContext = [AccountHelper]::GetCurrentRmContext() + # try{ + # $Properties.Add([TelemetryKeys]::tenantId, [RemoteReportHelper]::Mask($azureContext.Subscription.Id)) + # } + # catch { + # # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # # No need to break execution + # } + # try{ + # $Properties.Add([TelemetryKeys]::TenantName, [RemoteReportHelper]::Mask($azureContext.Subscription.Name)) + # } + # catch { + # # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # # No need to break execution + # } + # try{ + # $Properties.Add("AzureEnv", $azureContext.Environment.Name) + # } + # catch { + # # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # # No need to break execution + # } + # try{ + # $Properties.Add("TenantId", [RemoteReportHelper]::Mask($azureContext.Tenant.Id)) + # } + # catch { + # # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # # No need to break execution + # } + # try{ + # $Properties.Add("AccountId", [RemoteReportHelper]::Mask($azureContext.Account.Id)) + # } + # catch { + # # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # # No need to break execution + # } + # try{ + # $Properties.Add("RunIdentifier", [RemoteReportHelper]::Mask($azureContext.Account.Id + '##' + $Publisher.RunIdentifier)); + # } + # catch + # { + # $Properties.Add("RunIdentifier", $Publisher.RunIdentifier); + # } + # try{ + # $Properties.Add("AccountType", $azureContext.Account.Type) + # } + # catch { + # # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # # No need to break execution + # } + try{ + $OrgName = [ConfigurationManager]::GetAzSKConfigData().PolicyOrgName + $Properties.Add("OrgName", [RemoteReportHelper]::Mask($OrgName)) + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + catch{ + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + catch{ + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + + hidden static [string] AnonScriptStackTrace([string] $ScriptStackTrace) + { + try{ + $ScriptStackTrace = $ScriptStackTrace.Replace($env:USERNAME, "USERNAME") + $lines = $ScriptStackTrace.Split([System.Environment]::NewLine, [System.StringSplitOptions]::RemoveEmptyEntries) + $newLines = $lines | ForEach-Object { + $line = $_ + $lineSplit = $line.Split(@(", "), [System.StringSplitOptions]::RemoveEmptyEntries); + if($lineSplit.Count -eq 2){ + $filePath = $lineSplit[1]; + $startMarker = $filePath.IndexOf("AzSK") + if($startMarker -gt 0){ + $anonFilePath = $filePath.Substring($startMarker, $filePath.Length - $startMarker) + $newLine = $lineSplit[0] + ", " + $anonFilePath + $newLine + } + else{ + $line + } + } + else{ + $line + } + } + return ($newLines | Out-String) + } + catch{ + return $ScriptStackTrace + } + } + + static [psobject] SetCommonProperties([psobject] $EventObj,[UsageTelemetry] $Publisher) + { + try{ + $NA = "NA"; + $eventObj.properties.Add("InfoVersion", "V1"); + try{ + $eventObj.properties.Add("ScanSource", [RemoteReportHelper]::GetScanSource()); + } + catch{ + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try{ + $eventObj.properties.Add("ScannerModuleName", $Publisher.GetModuleName()); + } + catch{ + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try{ + $eventObj.properties.Add("ScannerVersion", $Publisher.GetCurrentModuleVersion()); + } + catch{ + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try{ + $eventObj.properties.Add("ControlVersion", $Publisher.GetCurrentModuleVersion()); + } + catch{ + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + try{ + # $azureContext = [AccountHelper]::GetCurrentRmContext() + # try{ + # $eventObj.properties.Add([TelemetryKeys]::tenantId, [RemoteReportHelper]::Mask($azureContext.Subscription.Id)) + # } + # catch{ + # # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # # No need to break execution + # } + # try{ + # $eventObj.properties.Add([TelemetryKeys]::TenantName, [RemoteReportHelper]::Mask($azureContext.Subscription.Name)) + # } + # catch{ + # # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # # No need to break execution + # } + # try{ + # $eventObj.properties.Add("AzureEnv", $azureContext.Environment.Name) + # } + # catch{ + # # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # # No need to break execution + # } + # try{ + # $eventObj.properties.Add("TenantId", [RemoteReportHelper]::Mask($azureContext.Tenant.Id)) + # } + # catch{ + # # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # # No need to break execution + # } + # try{ + # $eventObj.properties.Add("AccountId", [RemoteReportHelper]::Mask($azureContext.Account.Id)) + # } + # catch{ + # # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # # No need to break execution + # } + # try{ + # $eventObj.properties.Add("RunIdentifier", [RemoteReportHelper]::Mask($azureContext.Account.Id + '##' + $Publisher.RunIdentifier)); + # } + # catch{ + # $eventObj.properties.Add("RunIdentifier", $Publisher.RunIdentifier); + # } + # try{ + # $eventObj.properties.Add("AccountType", $azureContext.Account.Type) + # } + # catch{ + # # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # # No need to break execution + # } + try{ + $OrgName = [ConfigurationManager]::GetAzSKConfigData().PolicyOrgName + $eventObj.properties.Add("OrgName", [RemoteReportHelper]::Mask($OrgName)) + } + catch { + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + catch{ + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + } + catch{ + # Eat the current exception which typically happens when the property already exist in the object and try to add the same property again + # No need to break execution + } + + return $eventObj; + } + +} + diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/SecurityRecommendationReport.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/SecurityRecommendationReport.ps1 new file mode 100644 index 000000000..7009ea147 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/SecurityRecommendationReport.ps1 @@ -0,0 +1,141 @@ +Set-StrictMode -Version Latest +#Listner to write CA scan status on completion of resource scan +class SecurityRecommendationReport: ListenerBase +{ + hidden static [SecurityRecommendationReport] $Instance = $null; + static [SecurityRecommendationReport] GetInstance() + { + if ( $null -eq [SecurityRecommendationReport]::Instance) + { + [SecurityRecommendationReport]::Instance = [SecurityRecommendationReport]::new(); + } + return [SecurityRecommendationReport]::Instance + } + + [void] RegisterEvents() + { + $this.UnregisterEvents(); + + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [SecurityRecommendationReport]::GetInstance(); + try + { + $currentInstance.SetRunIdentifier([AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1)); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + + + $this.RegisterEvent([AzSKRootEvent]::CommandCompleted, { + $currentInstance = [SecurityRecommendationReport]::GetInstance(); + try + { + $messages = $Event.SourceArgs.Messages; + if(($messages | Measure-Object).Count -gt 0 -and $Event.SourceArgs.Messages[0].Message -eq "RecommendationData") + { + $reportTemplateFileContent = [ConfigurationHelper]::LoadOfflineConfigFile("SecurityRecommendationReport.html", $false); + $reportObject = [RecommendedSecurityReport] $Event.SourceArgs.Messages[0].DataObject; + + if([string]::IsNullOrWhiteSpace($reportObject.ResourceGroupName)) + { + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#rgName#]", "Not Specified"); + } + else { + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#rgName#]", "[$($reportObject.ResourceGroupName)]"); + } + + if(($reportObject.Input.Features | Measure-Object).Count -le 0) + { + #$currentInstance.WriteMessage("Features: Not Specified", [MessageType]::Default); + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#features#]", "Not Specified"); + } + else { + $featuresString = [String]::Join(",", $reportObject.Input.Features); + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#features#]", "[$featuresString]"); + } + + if(($reportObject.Input.Categories | Measure-Object).Count -le 0) + { + #$currentInstance.WriteMessage("Categories: Not Specified", [MessageType]::Default); + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#categories#]", "Not Specified"); + } + else { + $categoriesString = [String]::Join(",", $reportObject.Input.Categories); + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#categories#]", "[$categoriesString]"); + } + if($null -ne $reportObject.Recommendations.CurrentFeatureGroup) + { + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#cgranking#]", "$($reportObject.Recommendations.CurrentFeatureGroup.Ranking)"); + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#cgInstCount#]", "$($reportObject.Recommendations.CurrentFeatureGroup.TotalOccurances)"); + $featuresString = [String]::Join(",", $reportObject.Recommendations.CurrentFeatureGroup.Features); + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#cgF#]", "$featuresString"); + $categoriesString = [String]::Join(",", $reportObject.Recommendations.CurrentFeatureGroup.Categories); + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#cgC#]", "$categoriesString"); + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#cgPass#]", "$($reportObject.Recommendations.CurrentFeatureGroup.TotalSuccessCount)"); + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#cgFail#]", "$($reportObject.Recommendations.CurrentFeatureGroup.TotalFailCount)"); + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#up#]", "$($reportObject.Recommendations.CurrentFeatureGroup.UsagePercentage)"); + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#frate#]", "$($reportObject.Recommendations.CurrentFeatureGroup.FailureRate)"); + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#omu#]", "$($reportObject.Recommendations.CurrentFeatureGroup.OtherMostUsed)"); + } + else { + #TODO: need to hide the div + #$currentInstance.WriteMessage("Cannot find exact matching combination for the current user input.", [MessageType]::Default); + } + if(($reportObject.Recommendations.RecommendedFeatureGroups | Measure-Object).Count -gt 0) + { + $recommendationTemplate = @" + +
+
+
+
+
+
+
+
+
Category Group Ranking: [#cgranking#]
No. of instances with same combination: [#instcount#]
Feature combination:[#fc#]
Category Combination: [#cc#]
Measures: [Total Pass: [#pass#] [Total Fail: [#fail#]]
Usage Percentage: [#up#]
Failure Rate: [#frate#]
SimilarCombinations: [#omu#]
+
+"@; + $recommendationHtml = ""; + $i = 0; + $orderedRecommendations = $reportObject.Recommendations.RecommendedFeatureGroups | Sort-Object -Property Ranking + $orderedRecommendations | ForEach-Object { + $recommendation = $_; + $i = $i + 1; + $recommendationPart = $recommendationTemplate.Replace("[#i#]", $i); + $recommendationPart = $recommendationPart.Replace("[#cgranking#]","$($recommendation.Ranking)"); + $recommendationPart = $recommendationPart.Replace("[#instcount#]","$($recommendation.TotalOccurances)"); + $featuresString = [String]::Join(",", $recommendation.Features); + $recommendationPart = $recommendationPart.Replace("[#fc#]","$featuresString"); + $categoriesString = [String]::Join(",", $recommendation.Categories); + $recommendationPart = $recommendationPart.Replace("[#cc#]","$categoriesString"); + $recommendationPart = $recommendationPart.Replace("[#pass#]","$($recommendation.TotalSuccessCount) ]"); + $recommendationPart = $recommendationPart.Replace("[#fail#]","$($recommendation.TotalFailCount)"); + $recommendationPart = $recommendationPart.Replace("[#up#]","$($recommendation.UsagePercentage)"); + $recommendationPart = $recommendationPart.Replace("[#frate#]","$($recommendation.FailureRate)"); + $recommendationPart = $recommendationPart.Replace("[#omu#]","$($recommendation.OtherMostUsed)"); + $recommendationHtml += $recommendationPart + } + $reportTemplateFileContent = $reportTemplateFileContent.Replace("[#recommendations#]", "$recommendationHtml"); + + } + $reportFilePath = [WriteFolderPath]::GetInstance().FolderPath + "/SecurityRecommendationReport-" + $currentInstance.RunIdentifier + ".html"; + $reportTemplateFileContent | Out-File -FilePath $reportFilePath -Force; + + # $currentInstance.WriteMessage(($dataObject | ConvertTo-Json -Depth 10), [MessageType]::Info) + } + } + catch + { + $currentInstance.PublishException($_); + } + }); + } + +} + + diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/README.txt b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/README.txt new file mode 100644 index 000000000..f9e0f6812 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/README.txt @@ -0,0 +1,65 @@ +*** This README file describes how to interpret the different files created when AzSK cmdlets are executed *** + +Each AzSK cmdlet writes output to a folder whose location is determined as below: + +-------------------------------------------------------------- +AzSK-Root-Output-Folder = %LocalAppData%\Microsoft\AzSK.AADLogs + E.g., "C:\Users\\AppData\Local\Microsoft\AzSK.AADLogs" + +-------------------------------------------------------------- +Sub-Folder = Org_\_ + E.g., "Org_[yourOrganizationName]\20170321_183800_GSS)" + + +-------------------------------------------------------------- +Thus, the full path to an output folder for a specific cmdlet might look like: + E.g., "C:\Users\userName\AppData\Local\Microsoft\AzSK.AADLogs\Org_[yourSubscriptionName]\20170321_183800_GSS" + +By default, cmdlets open this folder upon completion of the cmdlet (we assume you'd be interested in examining the control evaluation status, etc.) + + +============================================================== +The contents of the output folder are organized as under: + + \SecurityReport-.csv + [This is the summary CSV file listing all applicable controls and their evaluation status. This file will be generated only for scan cmdlets like Get-AzSKAzureServicesSecurityStatus, Get-AzSKSubscriptionSecurityStatus etc. The CSV contains many useful columns such as recommendation, attestation details, a pointer to the control evaluation LOG file for the resource, etc.] + + + \AttestationReport-.csv + [This is the summary CSV file listing all applicable controls and their attestation details. This file will be generated only for the cmdlet Get-AzSKInfo -tenantId -InfoType AttestationInfo.] + + + \ + [This folder corresponds to the project or organization that was evaluated. If multiple projects were scanned, there is one folder for each project.] + + \.LOG + [This is the detailed/raw output log of controls evaluated for a given resource type within a project.] + + + \Etc + [This contains some other logs capturing the runtime context of the command.] + + \PowerShellOutput.LOG + [This is the raw PS console output captured in a file.] + + \EnvironmentDetails.LOG + [This is the log file containing environment data of current PowerShell session.] + \SecurityEvaluationData.json + [This is the detailed security data for each control that was evaluated. This file will be generated only for SVT cmdlets like Get-AzSKAADTenantSecurityStatus etc.] + + + \FixControlScripts + [This folder contains scripts to fix failing controls where fix-script is supported. The folder is generated only when the 'GenerateFixScript' switch is passed and one or more failed controls support automated fixing.] + + \README.txt + [This is help file describes the 'FixControlScripts' folder.] + + +-------------------------------------------------------------- +You can use these outputs as follows - + 1) The SecurityReport.CSV file provides a gist of the control evaluation results. Investigate those that say 'Verify' or 'Failed'. + 2) For 'Failed' or 'Verify' controls, look in the .LOG file (search for the text 'Failed' or by control-id) to help you understand why the control has failed. + 3) For 'Verify' controls, you will also find the SecurityEvaluationData.JSON file in the \Etc sub-folder handy. + 4) For some controls, you can also use the 'Recommendation' field in the control output to quickly get to the PS command to address the issue. + 5) Make changes as needed to the subscription/resource configs based on steps 2, 3 and 4. + 6) Rerun the cmdlet and verify that the controls you just attempted fixes for are passing now. diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteDataFile.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteDataFile.ps1 new file mode 100644 index 000000000..abb840d36 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteDataFile.ps1 @@ -0,0 +1,75 @@ +Set-StrictMode -Version Latest +class WriteDataFile: FileOutputBase +{ + hidden static [WriteDataFile] $Instance = $null; + hidden [int] $JsonDepth = 10; + + static [WriteDataFile] GetInstance() + { + if ( $null -eq [WriteDataFile]::Instance) + { + [WriteDataFile]::Instance = [WriteDataFile]::new(); + } + + return [WriteDataFile]::Instance + } + + [void] RegisterEvents() + { + $this.UnregisterEvents(); + + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [WriteDataFile]::GetInstance(); + try + { + $currentInstance.SetRunIdentifier([AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1)); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandStarted, { + $currentInstance = [WriteDataFile]::GetInstance(); + try + { + $currentInstance.SetFilePath($Event.SourceArgs.TenantContext, [FileOutputBase]::ETCFolderPath, ("SecurityEvaluationData-" + $currentInstance.RunIdentifier + ".json")); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandCompleted, { + $currentInstance = [WriteDataFile]::GetInstance(); + try + { + $currentInstance.WriteToJson($Event.SourceArgs); + $currentInstance.FilePath = ""; + } + catch + { + $currentInstance.PublishException($_); + } + }); + + } + + [void] WriteToJson([SVTEventContext[]] $arguments) + { + if ([string]::IsNullOrEmpty($this.FilePath)) { + return; + } + + if($arguments) + { + if (($arguments | Measure-Object).Count -gt 0) + { + [Helpers]::ConvertToJsonCustom($arguments) | Out-File $this.FilePath + #[Helpers]::ConvertToPson($arguments) | Out-File $($this.FilePath.Replace(".json", ".pson")) + } + } + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteDetailedLog.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteDetailedLog.ps1 new file mode 100644 index 000000000..7a445376f --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteDetailedLog.ps1 @@ -0,0 +1,308 @@ +Set-StrictMode -Version Latest +class WriteDetailedLog: FileOutputBase +{ + hidden static [WriteDetailedLog] $Instance = $null; + static [WriteDetailedLog] GetInstance() + { + if ( $null -eq [WriteDetailedLog]::Instance) + { + [WriteDetailedLog]::Instance = [WriteDetailedLog]::new(); + } + + return [WriteDetailedLog]::Instance + } + + [void] RegisterEvents() + { + $this.UnregisterEvents(); + + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [WriteDetailedLog]::GetInstance(); + try + { + $currentInstance.SetRunIdentifier([AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1)); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::EvaluationStarted, { + $currentInstance = [WriteDetailedLog]::GetInstance(); + try + { + if($Event.SourceArgs.IsResource()) + { + $currentInstance.SetFilePath($Event.SourceArgs.TenantContext, $Event.SourceArgs.ResourceContext.ResourceGroupName, ($Event.SourceArgs.FeatureName + ".LOG")); + $startHeading = ([Constants]::ModuleStartHeading -f $Event.SourceArgs.FeatureName, $Event.SourceArgs.ResourceContext.ResourceGroupName, $Event.SourceArgs.ResourceContext.ResourceName); + } + else + { + $currentInstance.SetFilePath($Event.SourceArgs.TenantContext, $Event.SourceArgs.TenantContext.TenantName, ($Event.SourceArgs.FeatureName + ".LOG")); + $startHeading = ([Constants]::ModuleStartHeadingSub -f $Event.SourceArgs.FeatureName, $Event.SourceArgs.TenantContext.TenantName, $Event.SourceArgs.TenantContext.tenantId); + } + $currentInstance.AddOutputLog($startHeading); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::EvaluationCompleted, { + $currentInstance = [WriteDetailedLog]::GetInstance(); + try + { + $props = $Event.SourceArgs[0]; + if($props) + { + if($props.IsResource()) + { + $currentInstance.AddOutputLog(([Constants]::CompletedAnalysis -f $props.FeatureName, $props.ResourceContext.ResourceGroupName, $props.ResourceContext.ResourceName)); + } + else + { + $currentInstance.AddOutputLog(([Constants]::CompletedAnalysisSub -f $props.FeatureName, $props.TenantContext.TenantName, $props.TenantContext.tenantId)); + } + } + else + { + $currentInstance.AddOutputLog([Constants]::SingleDashLine + "`r`nNo detailed data found.`r`n" + [Constants]::DoubleDashLine); + } + $currentInstance.AddOutputLog([Constants]::HashLine); + + $currentInstance.FilePath = ""; + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::ControlStarted, { + $currentInstance = [WriteDetailedLog]::GetInstance(); + try + { + $currentInstance.AddOutputLog([Constants]::DoubleDashLine); + $currentInstance.AddOutputLog("[$($Event.SourceArgs.ControlItem.ControlID)]: $($Event.SourceArgs.ControlItem.Description)"); + $currentInstance.AddOutputLog([Constants]::SingleDashLine); + if($Event.SourceArgs.IsResource()) + { + $currentInstance.AddOutputLog(("Checking: [{0}]-[$($Event.SourceArgs.ControlItem.Description)] for resource [{1}]" -f + $Event.SourceArgs.FeatureName, + $Event.SourceArgs.ResourceContext.ResourceName), + $true); + } + else + { + $currentInstance.AddOutputLog(("Checking: [{0}]-[$($Event.SourceArgs.ControlItem.Description)] for subscription [{1}]" -f + $Event.SourceArgs.FeatureName, + $Event.SourceArgs.TenantContext.TenantName), + $true); + } + } + catch + { + $currentInstance.PublishException($_); + } + }); + + + $this.RegisterEvent([SVTEvent]::ControlCompleted, { + $currentInstance = [WriteDetailedLog]::GetInstance(); + try + { + $currentInstance.WriteControlResult([SVTEventContext] ($Event.SourceArgs | Select-Object -First 1 )); + $currentInstance.AddOutputLog(([Constants]::DoubleDashLine + " `r`n")); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([AzSKRootEvent]::CommandProcessing, { + $currentInstance = [WriteDetailedLog]::GetInstance(); + try + { + if($Event.SourceArgs.Messages) + { + $currentInstance.SetFilePath($Event.SourceArgs.TenantContext, $Event.SourceArgs.TenantContext.TenantName, "Detailed.LOG"); + $Event.SourceArgs.Messages | ForEach-Object { + $currentInstance.AddOutputLog($_); + } + } + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([AzSKRootEvent]::CommandCompleted, { + $currentInstance = [WriteDetailedLog]::GetInstance(); + try + { + if($Event.SourceArgs.Messages) + { + $currentInstance.SetFilePath($Event.SourceArgs.TenantContext, $Event.SourceArgs.TenantContext.TenantName, "Detailed.LOG"); + $Event.SourceArgs.Messages | ForEach-Object { + $currentInstance.AddOutputLog($_); + } + } + $currentInstance.FilePath = ""; + } + catch + { + $currentInstance.PublishException($_); + } + }); + } + + hidden [void] AddOutputLog([string] $message, [bool] $includeTimeStamp) + { + if([string]::IsNullOrEmpty($message) -or [string]::IsNullOrEmpty($this.FilePath)) + { + return; + } + + if($includeTimeStamp) + { + $message = (Get-Date -format "MM\/dd\/yyyy HH:mm:ss") + "-" + $message + } + + Add-Content -Value $message -Path $this.FilePath + } + + hidden [void] AddOutputLog([string] $message) + { + $this.AddOutputLog($message, $false); + } + + hidden [void] AddOutputLog([MessageData] $messageData) + { + if($messageData) + { + if (-not [string]::IsNullOrEmpty($messageData.Message)) + { + $this.AddOutputLog($messageData.Message); + #$this.AddOutputLog("`r`n" + $messageData.Message); + } + + if ($messageData.DataObject) { + if (-not [string]::IsNullOrEmpty($messageData.Message)) + { + #$this.AddOutputLog("`r`n"); + } + $this.AddOutputLog([Helpers]::ConvertObjectToString($messageData.DataObject, $false)); + } + + } + } + + hidden [void] WriteControlResult([SVTEventContext] $eventContext) + { + if($eventContext.ControlResults -and $eventContext.ControlResults.Count -ne 0) + { + $controlDesc = $eventContext.ControlItem.Description; + $eventContext.ControlResults | Foreach-Object { + if(-not [string]::IsNullOrWhiteSpace($_.ChildResourceName)) + { + $this.AddOutputLog("`r`n"+([Constants]::SingleDashLine)); + $this.AddOutputLog(("Checking: [{0}]-[$controlDesc] for resource [{1}]" -f + $eventContext.FeatureName, + $_.ChildResourceName), + $true); + } + + $_.Messages | ForEach-Object { + $this.AddOutputLog($_); + } + + # Add attestation data to log + if($_.StateManagement -and $_.StateManagement.AttestedStateData) + { + $this.AddOutputLog([Constants]::SingleDashLine); + + $stateObject = $_.StateManagement.AttestedStateData; + $this.AddOutputLog("Justification: $($stateObject.Justification)"); + $this.AddOutputLog("Attested by: [$($stateObject.AttestedBy)] on [$($stateObject.AttestedDate)]"); + if($_.AttestationStatus -eq [AttestationStatus]::None) + { + $this.AddOutputLog("**State drift occurred**: The attested state doesn't match with the current state. Attestation status has been reset."); + if(-not [string]::IsNullOrWhiteSpace($stateObject.Message)) + { + $this.AddOutputLog($stateObject.Message); + } + + if ($stateObject.DataObject) + { + $this.AddOutputLog("Attestation Data"); + $this.AddOutputLog([Helpers]::ConvertObjectToString($stateObject.DataObject, $false)); + } + } + else + { + $this.AddOutputLog("Attestation status: [$($_.AttestationStatus)]"); + } + if($_.VerificationResult -eq [VerificationResult]::NotScanned) + { + if($stateObject.DataObject) + { + $this.AddOutputLog("Attestation Data"); + $this.AddOutputLog("Attested Data:"+[Helpers]::ConvertObjectToString($stateObject.DataObject, $false)); + } + else + { + $this.AddOutputLog("Attested Data: None"); + } + if(![string]::IsNullOrWhiteSpace($stateObject.ExpiryDate)) + { + $this.AddOutputLog("Attestation expiry date: [$($stateObject.ExpiryDate)]"); + } + } + } + + #$this.AddOutputLog("`r`n"); + if($_.VerificationResult -ne [VerificationResult]::NotScanned) + { + $this.AddOutputLog([Constants]::SingleDashLine); + + if($eventContext.IsResource()) + { + $resourceName = $eventContext.ResourceContext.ResourceName; + if(-not [string]::IsNullOrWhiteSpace($_.ChildResourceName)) + { + $resourceName = $_.ChildResourceName; + } + + $this.AddOutputLog(("**{3}**: [{0}]-[{2}] for resource: [{1}]" -f + $eventContext.FeatureName, + $resourceName, + $eventContext.ControlItem.Description, + $_.VerificationResult.ToString())); + } + else + { + $this.AddOutputLog(("**{3}**: [{0}]-[{2}] for subscription: [{1}]" -f + $eventContext.FeatureName, + $eventContext.TenantContext.TenantName, + $eventContext.ControlItem.Description, + $_.VerificationResult.ToString())); + } + } + } + } + else + { + #$this.AddOutputLog("`r`n"); + $this.AddOutputLog([Constants]::SingleDashLine); + $this.AddOutputLog(("**Disabled**: [{0}]-[{1}]" -f + $eventContext.FeatureName, + $eventContext.ControlItem.Description)); + } + + $this.AddOutputLog([Constants]::SingleDashLine); + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteEnvironmentFile.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteEnvironmentFile.ps1 new file mode 100644 index 000000000..2591fb8ba --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteEnvironmentFile.ps1 @@ -0,0 +1,107 @@ +Set-StrictMode -Version Latest +class WriteEnvironmentFile: FileOutputBase +{ + hidden static [WriteEnvironmentFile] $Instance = $null; + static [WriteEnvironmentFile] GetInstance() + { + if ($null -eq [WriteEnvironmentFile]::Instance) + { + [WriteEnvironmentFile]::Instance = [WriteEnvironmentFile]::new(); + } + + return [WriteEnvironmentFile]::Instance + } + + [void] RegisterEvents() + { + $this.UnregisterEvents(); + + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [WriteEnvironmentFile]::GetInstance(); + try + { + $currentInstance.SetRunIdentifier([AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1)); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandStarted, { + $currentInstance = [WriteEnvironmentFile]::GetInstance(); + try + { + $currentInstance.CommandStartedAction($Event.SourceArgs.TenantContext); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([AzSKRootEvent]::CommandStarted, { + $currentInstance = [WriteEnvironmentFile]::GetInstance(); + try + { + $currentInstance.CommandStartedAction($Event.SourceArgs.TenantContext); + } + catch + { + $currentInstance.PublishException($_); + } + }); + } + + hidden [void] AddOutputLog([string] $message, [bool] $includeTimeStamp) + { + if([string]::IsNullOrEmpty($message) -or [string]::IsNullOrEmpty($this.FilePath)) + { + return; + } + + if($includeTimeStamp) + { + $message = (Get-Date -format "MM\/dd\/yyyy HH:mm:ss") + "-" + $message + } + + Add-Content -Value $message -Path $this.FilePath + } + + hidden [void] AddOutputLog([string] $message) + { + $this.AddOutputLog($message, $false); + } + + [void] CommandStartedAction([TenantContext] $context) + { + $this.SetFilePath($context, [FileOutputBase]::ETCFolderPath, "EnvironmentDetails.LOG"); + $this.AddOutputLog([Constants]::DoubleDashLine); + + $currentVersion = $this.GetCurrentModuleVersion(); + $moduleName = $this.GetModuleName(); + $this.AddOutputLog("Environment log"); + $this.AddOutputLog("$moduleName Version: $currentVersion"); + $this.AddOutputLog([Constants]::DoubleDashLine); + + $this.AddOutputLog("Method Name: $($this.InvocationContext.MyCommand.Name) `r`nInput Parameters: "); + $this.AddOutputLog([Helpers]::ConvertObjectToString($this.InvocationContext.BoundParameters, $false)); + $this.AddOutputLog([Constants]::DoubleDashLine); + + $loadedModules = (Get-Module | Select-Object -Property Name, Path, Description, Version); + $this.AddOutputLog("Loaded PowerShell modules"); + $this.AddOutputLog([Helpers]::ConvertObjectToString($loadedModules, $false)); + $this.AddOutputLog([Constants]::DoubleDashLine); + + #$rmContext = [AccountHelper]::GetCurrentRmContext() + + # $this.AddOutputLog("Logged-in user context"); + # $this.AddOutputLog([Helpers]::ConvertObjectToString(($rmContext.Account | Select-Object -Property Id, Type, ExtendedProperties), $false)); + # $this.AddOutputLog([Constants]::DoubleDashLine); + + # $this.AddOutputLog("AzureRM context"); + # $this.AddOutputLog([Helpers]::ConvertObjectToString(($rmContext | Select-Object -Property Environment, Subscription, Tenant), $false)); + $this.AddOutputLog([Constants]::DoubleDashLine); + } + +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteFolderPath.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteFolderPath.ps1 new file mode 100644 index 000000000..2f511457a --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteFolderPath.ps1 @@ -0,0 +1,62 @@ +Set-StrictMode -Version Latest +class WriteFolderPath: FileOutputBase +{ + hidden static [WriteFolderPath] $Instance = $null; + static [WriteFolderPath] GetInstance() + { + if ($null -eq [WriteFolderPath]::Instance) + { + [WriteFolderPath]::Instance = [WriteFolderPath]::new(); + } + + return [WriteFolderPath]::Instance + } + + [void] RegisterEvents() + { + $this.UnregisterEvents(); + + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [WriteFolderPath]::GetInstance(); + try + { + $currentInstance.SetRunIdentifier([AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1)); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandStarted, { + $currentInstance = [WriteFolderPath]::GetInstance(); + try + { + $currentInstance.CommandStartedAction($Event.SourceArgs.TenantContext); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([AzSKRootEvent]::CommandStarted, { + $currentInstance = [WriteFolderPath]::GetInstance(); + try + { + $currentInstance.CommandStartedAction($Event.SourceArgs.TenantContext); + } + catch + { + $currentInstance.PublishException($_); + } + }); + } + + [void] CommandStartedAction([TenantContext] $context) + { + $this.SetFolderPath($context); + Copy-Item $PSScriptRoot\README.txt $this.FolderPath + } + +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WritePsConsole.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WritePsConsole.ps1 new file mode 100644 index 000000000..2c2c306da --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WritePsConsole.ps1 @@ -0,0 +1,654 @@ +Set-StrictMode -Version Latest +class WritePsConsole: FileOutputBase +{ + hidden static [WritePsConsole] $Instance = $null; + hidden [string] $SummaryMarkerText = "------"; + static [WritePsConsole] GetInstance() + { + if ($null -eq [WritePsConsole]::Instance) + { + [WritePsConsole]::Instance = [WritePsConsole]::new(); + } + + return [WritePsConsole]::Instance + } + + [void] RegisterEvents() + { + $this.UnregisterEvents(); + + # Mandatory: Generate Run Identifier Event + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + $currentInstance.SetRunIdentifier([AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1)); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + + $this.RegisterEvent([AzSKGenericEvent]::CustomMessage, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + if($Event.SourceArgs) + { + $messages = @(); + $messages += $Event.SourceArgs; + $messages | ForEach-Object { + $currentInstance.WriteMessageData($_); + } + } + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([AzSKGenericEvent]::Exception, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + $exceptionObj = $Event.SourceArgs | Select-Object -First 1 + #if(($null -ne $exceptionObj) -and ($null -ne $exceptionObj.Exception) -and (-not [String]::IsNullOrEmpty($exceptionObj.Exception.Message))) + #{ + # $currentInstance.WriteMessage($exceptionObj.Exception.Message, [MessageType]::Error); + # Write-Debug $exceptionObj + #} + #else + #{ + $currentInstance.WriteMessage($exceptionObj, [MessageType]::Error); + #} + } + catch + { + #Consuming the exception intentionally to prevent infinite loop of errors + #$currentInstance.PublishException($_); + } + }); + + + $this.RegisterEvent([AzSKRootEvent]::CustomMessage, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + if($Event.SourceArgs -and $Event.SourceArgs.Messages) + { + $Event.SourceArgs.Messages | ForEach-Object { + $currentInstance.WriteMessageData($_); + } + } + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([AzSKRootEvent]::CommandStarted, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + $currentInstance.CommandStartedAction($Event); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([AzSKRootEvent]::CommandError, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + $currentInstance.WriteMessage($Event.SourceArgs.ExceptionMessage, [MessageType]::Error); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([AzSKRootEvent]::CommandCompleted, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + $currentInstance.WriteMessage([Constants]::DoubleDashLine, [MessageType]::Info) + $currentInstance.WriteMessage("Logs have been exported to: '$([WriteFolderPath]::GetInstance().FolderPath)'", [MessageType]::Info) + $currentInstance.WriteMessage([Constants]::DoubleDashLine, [MessageType]::Info) + + $currentInstance.FilePath = ""; + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([AzSKRootEvent]::CommandCompleted, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + $messages = $Event.SourceArgs.Messages; + if(($messages | Measure-Object).Count -gt 0 -and $Event.SourceArgs.Messages[0].Message -eq "RecommendationData") + { + $reportObject = [RecommendedSecurityReport] $Event.SourceArgs.Messages[0].DataObject; + $currentInstance.WriteMessage([Constants]::DoubleDashLine, [MessageType]::Info) + $currentInstance.WriteMessage("Current Combination", [MessageType]::Info) + $currentInstance.WriteMessage([Constants]::DoubleDashLine, [MessageType]::Info) + if([string]::IsNullOrWhiteSpace($reportObject.ResourceGroupName)) + { + $currentInstance.WriteMessage("ResourceGroup Name: Not Specified", [MessageType]::Default); + } + else { + $currentInstance.WriteMessage("ResourceGroup Name: [$($reportObject.ResourceGroupName)]", [MessageType]::Default); + } + + if(($reportObject.Input.Features | Measure-Object).Count -le 0) + { + $currentInstance.WriteMessage("Features: Not Specified", [MessageType]::Default); + } + else { + $featuresString = [String]::Join(",", $reportObject.Input.Features); + $currentInstance.WriteMessage("Features: [$featuresString]", [MessageType]::Default); + } + + if(($reportObject.Input.Categories | Measure-Object).Count -le 0) + { + $currentInstance.WriteMessage("Categories: Not Specified", [MessageType]::Default); + } + else { + $categoriesString = [String]::Join(",", $reportObject.Input.Categories); + $currentInstance.WriteMessage("Categories: [$categoriesString]", [MessageType]::Default); + } + $currentInstance.WriteMessage([Constants]::UnderScoreLineLine, [MessageType]::Info) + $currentInstance.WriteMessage("Analysis & Recommendations:", [MessageType]::Info); + $currentInstance.WriteMessage([Constants]::DoubleDashLine, [MessageType]::Info); + $currentInstance.WriteMessage("Analysis of current feature group:", [MessageType]::Info); + if($null -ne $reportObject.Recommendations.CurrentFeatureGroup) + { + $currentInstance.WriteMessage("Current Group Ranking: $($reportObject.Recommendations.CurrentFeatureGroup.Ranking)", [MessageType]::Default); + $currentInstance.WriteMessage("No. of instances with same combination: $($reportObject.Recommendations.CurrentFeatureGroup.TotalOccurances)", [MessageType]::Default); + $featuresString = [String]::Join(",", $reportObject.Recommendations.CurrentFeatureGroup.Features); + $currentInstance.WriteMessage("Current Combination Features: $featuresString", [MessageType]::Default); + $categoriesString = [String]::Join(",", $reportObject.Recommendations.CurrentFeatureGroup.Categories); + $currentInstance.WriteMessage("Current Combination Categories: $categoriesString", [MessageType]::Default); + $currentInstance.WriteMessage("Measures: [Total Pass#: $($reportObject.Recommendations.CurrentFeatureGroup.TotalSuccessCount)] [Total Fail#: $($reportObject.Recommendations.CurrentFeatureGroup.TotalFailCount)] ", [MessageType]::Default); + } + else { + $currentInstance.WriteMessage("Cannot find exact matching combination for the current user input.", [MessageType]::Default); + } + $currentInstance.WriteMessage([Constants]::SingleDashLine, [MessageType]::Info); + $currentInstance.WriteMessage("Recommendations based on categories:", [MessageType]::Info); + if(($reportObject.Recommendations.RecommendedFeatureGroups | Measure-Object).Count -gt 0) + { + $orderedRecommendations = $reportObject.Recommendations.RecommendedFeatureGroups | Sort-Object -Property Ranking + $orderedRecommendations | ForEach-Object { + $recommendation = $_; + $currentInstance.WriteMessage("Category Group Ranking: $($recommendation.Ranking)", [MessageType]::Default); + $currentInstance.WriteMessage("No. of instances with same combination: $($recommendation.TotalOccurances)", [MessageType]::Default); + $featuresString = [String]::Join(",", $recommendation.Features); + $currentInstance.WriteMessage("Feature combination: $featuresString", [MessageType]::Default); + $categoriesString = [String]::Join(",", $recommendation.Categories); + $currentInstance.WriteMessage("Category Combination: $categoriesString", [MessageType]::Default); + $currentInstance.WriteMessage("Measures: [Total Pass#: $($recommendation.TotalSuccessCount)] [Total Fail#: $($recommendation.TotalFailCount)] ", [MessageType]::Default); + $currentInstance.WriteMessage([Constants]::SingleDashLine, [MessageType]::Info); + } + } + + $currentInstance.WriteMessage(($dataObject | ConvertTo-Json -Depth 10), [MessageType]::Info) + } + else { + $currentInstance.WriteMessage([Constants]::DoubleDashLine, [MessageType]::Info) + $currentInstance.WriteMessage("Logs have been exported to: '$([WriteFolderPath]::GetInstance().FolderPath)'", [MessageType]::Info) + $currentInstance.WriteMessage([Constants]::DoubleDashLine, [MessageType]::Info) + } + $currentInstance.FilePath = ""; + } + catch + { + $currentInstance.PublishException($_); + } + }); + + # SVT events + $this.RegisterEvent([SVTEvent]::CommandStarted, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + $currentInstance.CommandStartedAction($Event); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandError, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + $currentInstance.WriteMessage($Event.SourceArgs.ExceptionMessage, [MessageType]::Error); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandCompleted, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + if(($Event.SourceArgs | Measure-Object).Count -gt 0) + { + $controlsScanned = ($Event.SourceArgs.ControlResults|Where-Object{$_.VerificationResult -ne[VerificationResult]::NotScanned}|Measure-Object).Count -gt 0 + if($controlsScanned) + { + # Print summary + $currentInstance.PrintSummaryData($Event); + } + + $AttestControlParamFound = $currentInstance.InvocationContext.BoundParameters["AttestControls"]; + if($null -eq $AttestControlParamFound -and $controlsScanned) + { + $currentInstance.WriteMessage([Constants]::DoubleDashLine, [MessageType]::Info) + $currentInstance.WriteMessage([Constants]::RemediationMsg, [MessageType]::Info) + $currentInstance.WriteMessage([Constants]::AttestationReadMsg + [ConfigurationManager]::GetAzSKConfigData().AzSKRGName, [MessageType]::Info) + + } + $currentInstance.WriteMessage([Constants]::SingleDashLine, [MessageType]::Info) + } + + $currentInstance.WriteMessage("Status and detailed logs have been exported to path - $([WriteFolderPath]::GetInstance().FolderPath)", [MessageType]::Info) + $currentInstance.WriteMessage([Constants]::DoubleDashLine, [MessageType]::Info) + + $currentInstance.FilePath = ""; + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::EvaluationStarted, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + if($Event.SourceArgs.IsResource()) + { + $startHeading = ([Constants]::ModuleStartHeading -f $Event.SourceArgs.FeatureName, $Event.SourceArgs.ResourceContext.ResourceGroupName, $Event.SourceArgs.ResourceContext.ResourceName); + } + else + { + $startHeading = ([Constants]::ModuleStartHeadingSub -f $Event.SourceArgs.FeatureName, $Event.SourceArgs.TenantContext.TenantName, $Event.SourceArgs.TenantContext.tenantId); + } + $currentInstance.WriteMessage($startHeading, [MessageType]::Info); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::EvaluationCompleted, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + if($Event.SourceArgs -and $Event.SourceArgs.Count -ne 0) + { + $props = $Event.SourceArgs[0]; + if($props.IsResource()) + { + $currentInstance.WriteMessage(([Constants]::CompletedAnalysis -f $props.FeatureName, $props.ResourceContext.ResourceGroupName, $props.ResourceContext.ResourceName), [MessageType]::Update); + } + else + { + $currentInstance.WriteMessage(([Constants]::CompletedAnalysisSub -f $props.FeatureName, $props.TenantContext.TenantName, $props.TenantContext.tenantId), [MessageType]::Update); + } + } + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::EvaluationError, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + $currentInstance.WriteMessage($Event.SourceArgs.ExceptionMessage, [MessageType]::Error); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::ControlStarted, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + if($Event.SourceArgs.IsResource()) + { + $AnalysingControlHeadingMsg =([Constants]::AnalysingControlHeading -f $Event.SourceArgs.FeatureName, $Event.SourceArgs.ControlItem.Description,$Event.SourceArgs.ResourceContext.ResourceName) + } + else + { + $AnalysingControlHeadingMsg =([Constants]::AnalysingControlHeadingSub -f $Event.SourceArgs.FeatureName, $Event.SourceArgs.ControlItem.Description,$Event.SourceArgs.TenantContext.TenantName) + } + $currentInstance.WriteMessage($AnalysingControlHeadingMsg, [MessageType]::Info) + } + catch + { + $currentInstance.PublishException($_); + } + }); + + <#$this.RegisterEvent([AzSKRootEvent]::PublishCustomData, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + Write-Host -ForegroundColor White "Hello World2!" + } + catch + { + $currentInstance.PublishException($_); + } + });#> + + $this.RegisterEvent([SVTEvent]::ControlDisabled, { + $currentInstance = [WritePsConsole]::GetInstance(); + try + { + $currentInstance.WriteMessage(("**Disabled**: [{0}]-[{1}]" -f + $Event.SourceArgs.FeatureName, + $Event.SourceArgs.ControlItem.Description), [MessageType]::Warning); + } + catch + { + $currentInstance.PublishException($_); + } + }); + } + + #Write message on powershell console with appropriate color + [void] WriteMessage([PSObject] $message, [MessageType] $messageType) + { + if(-not $message) + { + return; + } + + $colorCode = [System.ConsoleColor]::White + + switch($messageType) + { + ([MessageType]::Critical) { + $colorCode = [System.ConsoleColor]::Red + } + ([MessageType]::Error) { + $colorCode = [System.ConsoleColor]::Red + } + ([MessageType]::Warning) { + $colorCode = [System.ConsoleColor]::Yellow + } + ([MessageType]::Info) { + $colorCode = [System.ConsoleColor]::Cyan + } + ([MessageType]::Update) { + $colorCode = [System.ConsoleColor]::Green + } + ([MessageType]::Deprecated) { + $colorCode = [System.ConsoleColor]::DarkYellow + } + ([MessageType]::Default) { + $colorCode = [System.ConsoleColor]::White + } + } + + # FilePath check ensures to print detailed error objects on PS host + $formattedMessage = [Helpers]::ConvertObjectToString($message, (-not [string]::IsNullOrEmpty($this.FilePath))); + Write-Host $formattedMessage -ForegroundColor $colorCode + #if($message.GetType().FullName -eq "System.Management.Automation.ErrorRecord") + #{ + $this.AddOutputLog([Helpers]::ConvertObjectToString($message, $false)); + #} + #else + #{ + # $this.AddOutputLog($message); + #} + } + + hidden [void] WriteMessage([PSObject] $message) + { + $this.WriteMessage($message, [MessageType]::Info); + } + + hidden [void] WriteMessageData([MessageData] $messageData) + { + if($messageData) + { + $this.WriteMessage(("`r`n" + $messageData.Message), $messageData.MessageType); + if($messageData.DataObject) + { + #if (-not [string]::IsNullOrEmpty($messageData.Message)) + #{ + # $this.WriteMessage("`r`n"); + #} + + $this.WriteMessage($messageData.DataObject, $messageData.MessageType); + } + } + } + + hidden [void] AddOutputLog([string] $message, [bool] $includeTimeStamp) + { + if([string]::IsNullOrEmpty($message) -or [string]::IsNullOrEmpty($this.FilePath)) + { + return; + } + + if($includeTimeStamp) + { + $message = (Get-Date -format "MM\/dd\/yyyy HH:mm:ss") + "-" + $message + } + + Add-Content -Value $message -Path $this.FilePath + } + + hidden [void] AddOutputLog([string] $message) + { + $this.AddOutputLog($message, $false); + } + + hidden [void] PrintSummaryData($event) + { + [SVTSummary[]] $summary = @(); + $event.SourceArgs | ForEach-Object { + $item = $_ + if ($item -and $item.ControlResults) + { + $item.ControlResults | ForEach-Object{ + $summary += [SVTSummary]@{ + VerificationResult = $_.VerificationResult; + ControlSeverity = $item.ControlItem.ControlSeverity; + }; + }; + } + }; + + if($summary.Count -ne 0) + { + $summaryResult = @(); + + $severities = @(); + $severities += $summary | Select-Object -Property ControlSeverity | Select-Object -ExpandProperty ControlSeverity -Unique; + + $verificationResults = @(); + $verificationResults += $summary | Select-Object -Property VerificationResult | Select-Object -ExpandProperty VerificationResult -Unique; + + if($severities.Count -ne 0) + { + # Create summary matrix + $totalText = "Total"; + $MarkerText = "MarkerText"; + $rows = @(); + $rows += $severities; + $rows += $MarkerText; + $rows += $totalText; + $rows += $MarkerText; + $rows | ForEach-Object { + $result = [PSObject]::new(); + Add-Member -InputObject $result -Name "Summary" -MemberType NoteProperty -Value $_.ToString() + Add-Member -InputObject $result -Name $totalText -MemberType NoteProperty -Value 0 + + [Enum]::GetNames([VerificationResult]) | Where-Object { $verificationResults -contains $_ } | + ForEach-Object { + Add-Member -InputObject $result -Name $_.ToString() -MemberType NoteProperty -Value 0 + }; + $summaryResult += $result; + }; + + $totalRow = $summaryResult | Where-Object { $_.Summary -eq $totalText } | Select-Object -First 1; + + $summary | Group-Object -Property ControlSeverity | ForEach-Object { + $item = $_; + $summaryItem = $summaryResult | Where-Object { $_.Summary -eq $item.Name } | Select-Object -First 1; + if($summaryItem) + { + $summaryItem.Total = $_.Count; + if($totalRow) + { + $totalRow.Total += $_.Count + } + $item.Group | Group-Object -Property VerificationResult | ForEach-Object { + $propName = $_.Name; + $summaryItem.$propName += $_.Count; + if($totalRow) + { + $totalRow.$propName += $_.Count + } + }; + } + }; + $markerRows = $summaryResult | Where-Object { $_.Summary -eq $MarkerText } + $markerRows | ForEach-Object { + $markerRow = $_ + Get-Member -InputObject $markerRow -MemberType NoteProperty | ForEach-Object { + $propName = $_.Name; + $markerRow.$propName = $this.SummaryMarkerText; + } + }; + if($summaryResult.Count -ne 0) + { + $this.WriteMessage(($summaryResult | Format-Table | Out-String), [MessageType]::Info) + } + } + } + } + + hidden [void] CommandStartedAction($event) + { + $arg = $event.SourceArgs | Select-Object -First 1; + + $this.SetFilePath($arg.TenantContext, [FileOutputBase]::ETCFolderPath, "PowerShellOutput.LOG"); + + $currentVersion = $this.GetCurrentModuleVersion(); + $moduleName = $this.GetModuleName(); + $methodName = $this.InvocationContext.InvocationName; + $verbndnoun = $methodName.Split('-') + $aliasName = [CommandHelper]::Mapping | Where {$_.Verb -eq $verbndnoun[0] -and $_.Noun -eq $verbndnoun[1] } + + $this.WriteMessage([Constants]::DoubleDashLine + "`r`n$moduleName Version: $currentVersion `r`n" + [Constants]::DoubleDashLine , [MessageType]::Info); + # Version check message + if($arg.Messages) + { + $arg.Messages | ForEach-Object { + $this.WriteMessageData($_); + } + } + + if($aliasName) + { + $aliasName = $aliasName.ShortName + + #Get List of parameters used with short alias + $paramlist = @() + $paramlist = $this.GetParamList() + + #Get command with short alias + $cmID = $this.GetShortCommand($aliasName,$paramlist); + + $this.WriteMessage("Method Name: $methodName ($aliasName)`r`nInput Parameters: $(($paramlist | Out-String).TrimEnd()) `r`n`nYou can also use: $cmID `r`n" + [Constants]::DoubleDashLine , [MessageType]::Info); + } + else + { + $this.WriteMessage("Method Name: $methodName `r`nInput Parameters: $(($this.InvocationContext.BoundParameters | Out-String).TrimEnd()) `r`n" + [Constants]::DoubleDashLine , [MessageType]::Info); + } + + + $this.WriteMessage([ConfigurationManager]::GetAzSKConfigData().PolicyMessage,[MessageType]::Warning) + + } + + hidden [string] GetShortCommand($aliasName,$paramlist) + { + $aliasshort = $aliasName.ToLower() + $cmID = "$aliasshort " + #Looping on parameters and adding them to the short alias with key and value and if no alias found adding it as it is + foreach($item in $paramlist) + { + $ky = $item.Alias + $vl = $item.Value + + if($vl -eq $true) + { + $vl = "" + } + if($ky) + { + $cmID += "-$ky $vl " + } + else + { + $ky = $item.Name + $cmID += "-$ky $vl " + } + } + return $cmID; + } + + hidden [psobject] GetParamList() + { + $paramlist = @() + #Looping on parameters and creating list of smallest alias and creating parameter detail object + $this.InvocationContext.BoundParameters.Keys | % { + $key = $this.InvocationContext.MyCommand.Parameters.$_.Aliases #| Where {$_.Length -lt 5} + $key = $key | sort length -Descending | select -Last 1 + $val = $this.InvocationContext.BoundParameters[$_] + + $myObject = New-Object System.Object + + $myObject | Add-Member -type NoteProperty -name Name -Value $_ + $myObject | Add-Member -type NoteProperty -name Alias -Value $key + $myObject | Add-Member -type NoteProperty -name Value -Value $val + + $paramlist += $myObject + } + return $paramlist; + } + +} + +class SVTSummary +{ + [VerificationResult] $VerificationResult = [VerificationResult]::Manual; + [string] $ControlSeverity = [ControlSeverity]::High; +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteSummaryFile.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteSummaryFile.ps1 new file mode 100644 index 000000000..ac517a801 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/UserReports/WriteSummaryFile.ps1 @@ -0,0 +1,307 @@ +Set-StrictMode -Version Latest +class WriteSummaryFile: FileOutputBase +{ + hidden static [WriteSummaryFile] $Instance = $null; + + static [WriteSummaryFile] GetInstance() + { + if ( $null -eq [WriteSummaryFile]::Instance) + { + [WriteSummaryFile]::Instance = [WriteSummaryFile]::new(); + } + + return [WriteSummaryFile]::Instance + } + + [void] RegisterEvents() + { + $this.UnregisterEvents(); + + $this.RegisterEvent([AzSKRootEvent]::GenerateRunIdentifier, { + $currentInstance = [WriteSummaryFile]::GetInstance(); + try + { + $currentInstance.SetRunIdentifier([AzSKRootEventArgument] ($Event.SourceArgs | Select-Object -First 1)); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandStarted, { + $currentInstance = [WriteSummaryFile]::GetInstance(); + try + { + $currentInstance.SetFilePath($Event.SourceArgs.TenantContext, ("SecurityReport-" + $currentInstance.RunIdentifier + ".csv")); + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([SVTEvent]::CommandCompleted, { + $currentInstance = [WriteSummaryFile]::GetInstance(); + + if(($Event.SourceArgs.ControlResults|Where-Object{$_.VerificationResult -ne[VerificationResult]::NotScanned}|Measure-Object).Count -gt 0) + { + $currentInstance.SetFilePath($Event.SourceArgs[0].TenantContext, ("SecurityReport-" + $currentInstance.RunIdentifier + ".csv")); + } + else + { + # While running GAI -InfoType AttestationInfo, no controls are evaluated. So the value of VerificationResult is by default NotScanned for all controls. + # In that case the csv file should be renamed to AttestationReport. + $currentInstance.SetFilePath($Event.SourceArgs[0].TenantContext, ("AttestationReport-" + $currentInstance.RunIdentifier + ".csv")); + } + + # Export CSV Report + try + { + $currentInstance.WriteToCSV($Event.SourceArgs); + $currentInstance.FilePath = ""; + } + catch + { + $currentInstance.PublishException($_); + } + + }); + + $this.RegisterEvent([AzSKRootEvent]::UnsupportedResources, { + $currentInstance = [WriteSummaryFile]::GetInstance(); + try + { + $message = $Event.SourceArgs.Messages | Select-Object -First 1 + if($message -and $message.DataObject) + { + $filePath = $currentInstance.CalculateFilePath($Event.SourceArgs.TenantContext, [FileOutputBase]::ETCFolderPath, ("UnsupportedResources-" + $currentInstance.RunIdentifier + ".csv.LOG")); + $message.DataObject | Export-Csv $filePath -NoTypeInformation + } + } + catch + { + $currentInstance.PublishException($_); + } + }); + + $this.RegisterEvent([AzSKRootEvent]::WriteCSV, { + $currentInstance = [WriteSummaryFile]::GetInstance(); + try + { + $fileName = 'Control Details'; + $folderPath = ''; + $fileExtension = 'csv'; + + $message = $Event.SourceArgs.Messages | Select-Object -First 1 + if($message -and $message.DataObject) + { + if(-not [string]::IsNullOrEmpty($message.DataObject.FileName)) + { + $fileName = $message.DataObject.FileName + } + if(-not [string]::IsNullOrEmpty($message.DataObject.FolderPath)) + { + $folderPath = $message.DataObject.FolderPath + } + if(-not [string]::IsNullOrEmpty($message.DataObject.FileExtension)) + { + $fileExtension = $message.DataObject.FileExtension + } + + $filePath = $currentInstance.CalculateFilePath($Event.SourceArgs.TenantContext, $folderPath, ($fileName + "." + $fileExtension)); + $message.DataObject.MessageData | Export-Csv $filePath -NoTypeInformation + } + } + catch + { + $currentInstance.PublishException($_); + } + }); + # Event for Writing File Detailed Log + $this.RegisterEvent([AzSKRootEvent]::WriteExcludedResources,{ + $currentInstance = [WriteSummaryFile]::GetInstance(); + try + { + $message = $Event.SourceArgs.Messages | Select-Object -First 1 + $printMessage=""; + if($message -and $message.DataObject) + { + $filePath = $currentInstance.CalculateFilePath($Event.SourceArgs.TenantContext, [FileOutputBase]::ETCFolderPath, ("ExcludedResources-" + $currentInstance.RunIdentifier + ".txt.LOG")); + + $ExcludedType = $message.DataObject.ExcludedResourceType + if($ExcludedType -eq 'All') + { + $ExcludedType = 'None' + } + $ExcludedRGs = $message.DataObject.ExcludedResourceGroupNames + + $ExcludeResourceName = $message.DataObject.ExcludeResourceNames + $ExcludedResources = $message.DataObject.ExcludedResources + $ExcludedRGsResources = $ExcludedResources | Where-Object {$_.ResourceGroupName -in $ExcludedRGs} + $ExcludedTypeResources = $ExcludedResources | Select-Object -ExpandProperty ResourceTypeMapping |Where-Object {$_.ResourceTypeName -in $ExcludedType} + $ExplicitlyExcludedResource =$ExcludedResources| Where-Object {$_.ResourceName -in $ExcludeResourceName} + $printMessage += [Constants]::DoubleDashLine +"`r`nNumber of resource groups excluded: $(($ExcludedRGs | Measure-Object).Count | Out-String)" + $printMessage += "`r`nNumber of resources excluded: $(($ExcludedResources | Measure-Object).Count | Out-String)" + $printMessage += "`r`n`nDistribution of resources being excluded is as follows:"+"`r`n"+[Constants]::SingleDashLine + $printMessage += "`r`nNumber of resources excluded due to excluding resource groups: $(($ExcludedRGsResources | Measure-Object).Count | Out-String)" + $printMessage += "`r`nNumber of resources excluded due to excluding resource type '$ExcludedType': $(($ExcludedTypeResources | Measure-Object).Count | Out-String)" + $printMessage += "`r`nNumber of resources excluded explicitly: $(($ExplicitlyExcludedResource| Measure-Object).Count|Out-String)" + $printMessage += "`r`n"+[Constants]::SingleDashLine +"`r`n"+[Constants]::DoubleDashLine+"`r`nFollowing are the list of resource groups and resources being excluded" + $printMessage += "`r`n"+[Constants]::SingleDashLine+"`r`nResource groups excluded:" + $detailedList += "`r`n-------------------------" + if(($ExcludedRGs | Measure-Object).Count -gt 0) + { + $detailedList +="`r`n$($ExcludedRGS |Sort-Object|Format-Table |Out-String)" + } + else + { + $detailedList += "`r`n N/A" + } + $detailedList += "`r`nResources excluded:" + $detailedList += "`r`n-------------------------" + if(($ExcludedResources | Measure-Object).Count -gt 0) + { + $detailedList += "`r`n$($ExcludedResources| Sort-Object -Property "ResourceGroupName"|Select-Object -Property ResourceName,ResourceGroupName -ExpandProperty ResourceTypeMapping| Select-Object -Property ResourceName,ResourceGroupName,ResourceTypeName,ResourceType|Format-Table | Out-String)" + } + else + { + $detailedList += "`r`n N/A" + } + $printMessage += $detailedList + + Add-Content -Value $printMessage -Path $filePath + + } + + } + catch + { + $currentInstance.PublishException($_); + } + }); + } + + + + [void] WriteToCSV([SVTEventContext[]] $arguments) + { + if ([string]::IsNullOrEmpty($this.FilePath)) { + return; + } + [CsvOutputItem[]] $csvItems = @(); + $anyAttestedControls = $null -ne ($arguments | + Where-Object { + $null -ne ($_.ControlResults | Where-Object { $_.AttestationStatus -ne [AttestationStatus]::None } | Select-Object -First 1) + } | Select-Object -First 1); + + #$anyFixableControls = $null -ne ($arguments | Where-Object { $_.ControlItem.FixControl } | Select-Object -First 1); + + $arguments | ForEach-Object { + $item = $_ + if ($item -and $item.ControlResults) { + $item.ControlResults | ForEach-Object{ + $csvItem = [CsvOutputItem]@{ + ControlID = $item.ControlItem.ControlID; + ControlSeverity = $item.ControlItem.ControlSeverity; + Description = $item.ControlItem.Description; + FeatureName = $item.FeatureName; + ChildResourceName = $_.ChildResourceName; + Recommendation = $item.ControlItem.Recommendation; + + }; + if($_.VerificationResult -ne [VerificationResult]::NotScanned) + { + $csvItem.Status = $_.VerificationResult.ToString(); + } + if($this.InvocationContext.BoundParameters['IncludeUserComments'] -eq $True) + { + $csvItem.UserComments=$_.UserComments; + } + #if($anyFixableControls) + #{ + if($item.ControlItem.FixControl) + { + $csvItem.SupportsAutoFix = "Yes"; + } + else + { + $csvItem.SupportsAutoFix = "No"; + } + #} + + if($item.ControlItem.IsBaselineControl) + { + $csvItem.IsBaselineControl = "Yes"; + } + else + { + $csvItem.IsBaselineControl = "No"; + } + + if($anyAttestedControls) + { + $csvItem.ActualStatus = $_.ActualVerificationResult.ToString(); + } + + if($item.IsResource()) + { + $csvItem.ResourceName = $item.ResourceContext.ResourceName; + $csvItem.ResourceGroupName = $item.ResourceContext.ResourceGroupName; + $csvItem.ResourceId = $item.ResourceContext.ResourceId; + $csvItem.DetailedLogFile = "/$([Helpers]::SanitizeFolderName($item.ResourceContext.ResourceGroupName))/$($item.FeatureName).LOG"; + } + else + { + $csvItem.ResourceId = $item.TenantContext.scope; + $csvItem.DetailedLogFile = "/$([Helpers]::SanitizeFolderName($item.TenantContext.TenantName))/$($item.FeatureName).LOG" + } + + if($_.AttestationStatus -ne [AttestationStatus]::None) + { + $csvItem.AttestedSubStatus = $_.AttestationStatus.ToString(); + if($null -ne $_.StateManagement -and $null -ne $_.StateManagement.AttestedStateData) + { + $csvItem.AttesterJustification = $_.StateManagement.AttestedStateData.Justification + $csvItem.AttestedBy = $_.StateManagement.AttestedStateData.AttestedBy + if(![string]::IsNullOrWhiteSpace($_.StateManagement.AttestedStateData.ExpiryDate)) + { + $csvItem.AttestationExpiryDate = $_.StateManagement.AttestedStateData.ExpiryDate + } + } + } + if($_.IsControlInGrace -eq $true) + { + $csvItem.IsControlInGrace = "Yes" + } + else + { + $csvItem.IsControlInGrace = "No" + } + $csvItems += $csvItem; + } + } + } + + if ($csvItems.Count -gt 0) { + # Remove Null properties + $nonNullProps = @(); + + [CsvOutputItem].GetMembers() | Where-Object { $_.MemberType -eq [System.Reflection.MemberTypes]::Property } | ForEach-Object { + $propName = $_.Name; + if(($csvItems | Where-object { -not [string]::IsNullOrWhiteSpace($_.$propName) } | Measure-object).Count -ne 0) + { + $nonNullProps += $propName; + } + }; + if($this.InvocationContext.BoundParameters['IncludeUserComments'] -eq $true -and -not ([Helpers]::CheckMember($nonNullProps, "UserComments"))) + { + $nonNullProps += "UserComments"; + } + $csvItems | Select-Object -Property $nonNullProps | Export-Csv $this.FilePath -NoTypeInformation + } + } + +} + + diff --git a/src/AzSK.AAD/0.9.0/Framework/Listeners/Webhook/WebhookOutput.ps1 b/src/AzSK.AAD/0.9.0/Framework/Listeners/Webhook/WebhookOutput.ps1 new file mode 100644 index 000000000..a87babba0 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Listeners/Webhook/WebhookOutput.ps1 @@ -0,0 +1,195 @@ +Set-StrictMode -Version Latest + +class WebhookOutput: ListenerBase +{ + hidden static [WebhookOutput] $Instance = $null; + #Default source is kept as SDL / PowerShell. + #This value must be set in respective environment i.e. CICD,CC + [string] $WebhookSource; + + WebhookOutput() + { + + } + + + static [WebhookOutput] GetInstance() + { + if($null -eq [WebhookOutput]::Instance) + { + [WebhookOutput]::Instance = [WebhookOutput]::new(); + } + return [WebhookOutput]::Instance; + } + + [void] RegisterEvents() + { + $this.UnregisterEvents(); + + $this.RegisterEvent([SVTEvent]::EvaluationCompleted, { + $currentInstance = [WebhookOutput]::GetInstance(); + try + { + $currentInstance.WriteControlResult([SVTEventContext[]] ($Event.SourceArgs)); + } + catch + { + $currentInstance.PublishException($_); + } + }); + } + + hidden [void] WriteControlResult([SVTEventContext[]] $eventContextAll) + { + try + { + $settings = [ConfigurationManager]::GetAzSKSettings() + $tempBodyObjectsAll = [System.Collections.ArrayList]::new() + + if(-not [string]::IsNullOrWhiteSpace($settings.WebhookSource)) + { + $this.WebhookSource = $settings.WebhookSource + } + + if(-not [string]::IsNullOrWhiteSpace($settings.WebhookUrl)) + { + $eventContextAll | ForEach-Object{ + $eventContext = $_ + $tempBodyObjects = $this.GetWebhookBodyObjects($this.WebhookSource,$eventContext) #need to prioritize this + $tempBodyObjects | ForEach-Object{ + Set-Variable -Name tempBody -Value $_ -Scope Local + $tempBodyObjectsAll.Add($tempBody) + + } + } + + PostWebhookData ` + -webHookUrl $settings.WebhookUrl ` + -authZHeaderName $settings.WebhookAuthZHeaderName ` + -authZHeaderValue $settings.WebhookAuthZHeaderValue ` + -eventBody $tempBodyObjectsAll ` + -logType $settings.WebhookType + #Currently logType param is not used + + } + } + catch + { + [Exception] $ex = [Exception]::new(("Invalid Webhook Settings: " + $_.Exception.ToString()), $_.Exception) + throw [SuppressedException] $ex + } + } + + hidden [PSObject[]] GetWebhookBodyObjects([string] $Source,[SVTEventContext] $eventContext) + { + [PSObject[]] $output = @(); + [array] $eventContext.ControlResults | ForEach-Object{ + Set-Variable -Name ControlResult -Value $_ -Scope Local + $out = "" | Select-Object ResourceType, ResourceGroup, Reference, ResourceName, ChildResourceName, ControlStatus, ActualVerificationResult, ControlId, TenantName, tenantId, FeatureName, Source, Recommendation, ControlSeverity, TimeTakenInMs, AttestationStatus, AttestedBy, Justification + if($eventContext.IsResource()) + { + $out.ResourceType=$eventContext.ResourceContext.ResourceType + $out.ResourceGroup=$eventContext.ResourceContext.ResourceGroupName + $out.ResourceName=$eventContext.ResourceContext.ResourceName + $out.ChildResourceName=$ControlResult.ChildResourceName + } + + $out.Reference=$eventContext.Metadata.Reference + $out.ControlStatus=$ControlResult.VerificationResult.ToString() + $out.ActualVerificationResult=$ControlResult.ActualVerificationResult.ToString() + $out.ControlId=$eventContext.ControlItem.ControlID + $out.TenantName=$eventContext.TenantContext.TenantName + $out.tenantId=$eventContext.TenantContext.tenantId + $out.FeatureName=$eventContext.FeatureName + $out.Recommendation=$eventContext.ControlItem.Recommendation + $out.ControlSeverity=$eventContext.ControlItem.ControlSeverity.ToString() + $out.Source=$Source + + #mapping the attestation properties + if($null -ne $ControlResult -and $null -ne $ControlResult.StateManagement -and $null -ne $ControlResult.StateManagement.AttestedStateData) + { + $attestedData = $ControlResult.StateManagement.AttestedStateData; + $out.AttestationStatus = $ControlResult.AttestationStatus.ToString(); + $out.AttestedBy = $attestedData.AttestedBy; + $out.Justification = $attestedData.Justification; + } + + #$out.TimeTakenInMs=[int] $Metrics["TimeTakenInMs"] + $output += $out + } + return $output + } +} + + +function PostWebhookData($webHookUrl, $authZHeaderName, $authZHeaderValue, $eventBody, $logType) +{ + #$now = [DateTime]::Now.DateTime + #$eventDataX = @{"event" = "Hello Splunk! This is AzSK speaking @ $now"} | ConvertTo-Json + + $eventJson = @{ + "event" = $eventBody + } | ConvertTo-Json + + $defaultSecurityProtocol = $null + try + { + [TertiaryBool] $AllowSelfSignedWebhookCertificate = [TertiaryBool]::NotSet; + $AllowSelfSignedWebhookCertificate = [ConfigurationManager]::GetLocalAzSKSettings().AllowSelfSignedWebhookCertificate; + if($AllowSelfSignedWebhookCertificate -eq [TertiaryBool]::NotSet) + { + $serverAllowSelfSignedWebhookCertificate = [ConfigurationManager]::GetAzSKConfigData().AllowSelfSignedWebhookCertificate; + if($serverAllowSelfSignedWebhookCertificate) + { + $AllowSelfSignedWebhookCertificate = [TertiaryBool]::True; + } + else + { + $AllowSelfSignedWebhookCertificate = [TertiaryBool]::False; + } + } + if($AllowSelfSignedWebhookCertificate -eq [TertiaryBool]::NotSet) + { + $AllowSelfSignedWebhookCertificate = [TertiaryBool]::False; + } + + if($AllowSelfSignedWebhookCertificate -eq [TertiaryBool]::False) + { + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } + } + $defaultSecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol + [System.Net.ServicePointManager]::SecurityProtocol = ` + [System.Net.SecurityProtocolType]::Tls12 + + if (-not [String]::IsNullOrWhiteSpace($authZHeaderValue)) + { + $response = Invoke-WebRequest ` + -Uri $webhookUrl ` + -Method "Post" ` + -Body $eventJson ` + -Header @{ $authZHeaderName = $authZHeaderValue} + } + else + { + $response = Invoke-WebRequest ` + -Uri $webhookUrl ` + -Method "Post" ` + -Body $eventJson + } + } + catch + { + $msg = $_.Exception.Message + $status = $_.Exception.Status + $hr = "{0:x8}" -f ($_.Exception.HResult) + $innerException = $_.Exception.InnerException + #Just issue a warning as about being unable to send notification... + Write-Warning("`n`t[$status] `n`t[0x$hr] `n`t[$msg] `n`t[$innerException]") + } + finally + { + # Set securityProtocol and CertValidation behavior back to default state. + [System.Net.ServicePointManager]::SecurityProtocol = $defaultSecurityProtocol + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Managers/AzSKPDFExtension.ps1 b/src/AzSK.AAD/0.9.0/Framework/Managers/AzSKPDFExtension.ps1 new file mode 100644 index 000000000..7a1296560 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Managers/AzSKPDFExtension.ps1 @@ -0,0 +1,350 @@ +Set-StrictMode -Version Latest + +class AzSKPDFExtension +{ + static [void] GeneratePDF([string] $reportFolderPath, [PSObject] $subscriptionObject, [PSObject] $dataObject, [bool] $isLandscape) + { + # Get Context Info + $executedBy = ([AccountHelper]::GetCurrentRmContext()).Account + + # Verify whether word is installed on machine + + If (test-path HKLM:SOFTWARE\Classes\Word.Application) + { + # Initialize word file + try + { + $Word = New-Object -ComObject word.application + $Word.Visible = $false; + $AzSKReportDoc = $Word.Documents.Add(); + if($isLandscape) + { + $AzSKReportDoc.PageSetup.Orientation = 1 + } + else + { + $AzSKReportDoc.PageSetup.Orientation = 0 + } + + $pdfPath = "$reportFolderPath\SecurityReport.pdf" + $margin = 36 # 1.26 cm + $AzSKReportDoc.PageSetup.LeftMargin = $margin + $AzSKReportDoc.PageSetup.RightMargin = $margin + #$AzSKReportDoc.PageSetup.TopMargin = $margin + $AzSKReportDoc.PageSetup.BottomMargin = $margin + + $isSubscriptionCore = $false + + $selection = $Word.Selection + $selection.WholeStory + $selection.Style = "No Spacing" + + # Region Front Page + [AzSKPDFExtension]::WriteText($selection, 'DevSecOps Kit for Azure (AzSK)','Title', $true, $true, $false) + [AzSKPDFExtension]::WriteText($selection, 'Security Report','TOC Heading', $true, $true, $false) + $selection.InsertBreak(6) + $selection.InsertBreak(6) + $selection.InsertBreak(6) + $selection.InsertBreak(6) + $selection.InsertBreak(6) + + $TitleTableRange = $selection.Range(); + $AzSKReportDoc.Tables.Add($TitleTableRange,11,2) | Out-Null + $AzSKTitleTable = $AzSKReportDoc.Tables.item(1) + [AzSKPDFExtension]::WriteHeaderTableCell($AzSKTitleTable, 1, 'Subscription Name', $subscriptionObject.TenantName) + [AzSKPDFExtension]::WriteHeaderTableCell($AzSKTitleTable, 2, 'tenantId', $subscriptionObject.tenantId) + [AzSKPDFExtension]::WriteHeaderTableCell($AzSKTitleTable, 3, 'AzSK Version', $dataObject.MyCommand.Version.ToString()) + [AzSKPDFExtension]::WriteHeaderTableCell($AzSKTitleTable, 4, 'Generated by', $dataObject.MyCommand.ModuleName.ToString()) + [AzSKPDFExtension]::WriteHeaderTableCell($AzSKTitleTable, 5, 'Generated on', (get-date).ToUniversalTime().ToString("MMMM dd, yyyy HH:mm") + " (UTC)") + [AzSKPDFExtension]::WriteHeaderTableCell($AzSKTitleTable, 6, 'Requested by', $executedBy.Id.ToString() + " (" + $executedBy.Type.ToString() + ")") + [AzSKPDFExtension]::WriteHeaderTableCell($AzSKTitleTable, 7, 'Command Executed', $dataObject.Line.Trim()) + [AzSKPDFExtension]::WriteHeaderTableCell($AzSKTitleTable, 8, 'Documentation', 'http://aka.ms/azskdocs') + [AzSKPDFExtension]::WriteHeaderTableCell($AzSKTitleTable, 9, 'FAQ', 'http://aka.ms/azskdocs/faq') + [AzSKPDFExtension]::WriteHeaderTableCell($AzSKTitleTable, 10, 'Support DL', [ConfigurationManager]::GetAzSKConfigData().SupportDL) + + $AzSKTitleTable.Borders.OutsideLineStyle = 1 + $AzSKTitleTable.Style = 'Table Grid Light' + $AzSKTitleTable.Borders.OutsideLineStyle = 1 + $AzSKTitleTable.Borders.InsideLineStyle = 0 + $AzSKTitleTable.Columns.AutoFit() + + $Word.Selection.Start= $AzSKReportDoc.Content.End + + $selection.InsertBreak(7) + #end region + + # Region TOC + [AzSKPDFExtension]::WriteText($selection, 'Contents','TOC Heading', $false, $true, $false) + $range = $Selection.Range + $toc = $AzSKReportDoc.TablesOfContents.Add($range) + $selection.TypeParagraph() + $selection.InsertBreak(7) + + # End region TOC + + # Region Headers/Footers + + #$Section = $AzSKReportDoc.Sections.Item(1) + #$Header = $Section.Footers.Item(1) + #$Header.Range.Text = (get-date).ToUniversalTime().ToString("HH:mm MMMM dd, yyyy") + "(UTC)" + #$Header.Range.Font.Size = 9 + #$Header.Range.ParagraphFormat.Alignment = 2 + $AzSKReportDoc.Sections(1).Footers(1).PageNumbers.Add(2) + + # End region Headers/Footers + + #region -> Add the CSV report + $selection.TypeText("Security Report Summary"); + $selection.Style = 'Heading 1' + $selection.TypeParagraph() + $selection.Style = 'No Spacing' + $selection.InsertBreak(6) + + $ReportRange = $selection.Range(); + + $reportCSVFilePath = @(); + $reportCSVFilePath += Get-ChildItem -Path $reportFolderPath -Filter "*.CSV" -Recurse + if($reportCSVFilePath.Length -le 0) + { + [AzSKPDFExtension]::WriteText($selection, 'Unable to find the required security report under the report folder.','No Spacing', $false, $true, $false) + [AzSKPDFExtension]::WriteText($selection, 'Or','No Spacing', $true, $true, $false) + [AzSKPDFExtension]::WriteText($selection, 'No controls have been found to evaluate for the Subscription.','No Spacing', $false, $true, $false) + #throw "Didn't find the required security report under the report folder."; + } + else + { + $controls = Import-Csv -Path $reportCSVFilePath[0].FullName + $isAttestedResult = $false + if(($controls | Measure-Object).Count -gt 0) + { + $Number_Of_Controls = (($controls | Measure-Object).Count +1) + if($controls[0] | Get-Member -Name "AttestedSubStatus") + { + $isAttestedResult = $true + } + + if($isAttestedResult) + { + $Number_Of_Columns = 7 # ControlID, Status, RG, ResourceName, Control Severity + } + else + { + $Number_Of_Columns = 6 + } + + $x = 2 + + $AzSKReportDoc.Tables.Add($ReportRange,$Number_Of_Controls,$Number_Of_Columns) | Out-Null + $AzSKReportTable = $AzSKReportDoc.Tables.item(2) + + $AzSKReportTable.Cell(1,1).Range.Text = "ControlId" + $AzSKReportTable.Cell(1,2).Range.Text = "Status" + $AzSKReportTable.Cell(1,3).Range.Text = "ResourceGroup" + $AzSKReportTable.Cell(1,4).Range.Text = "Resource" + $AzSKReportTable.Cell(1,5).Range.Text = "Severity" + $AzSKReportTable.Cell(1,6).Range.Text = "Description" + if($isAttestedResult) + { + $AzSKReportTable.Cell(1,7).Range.Text = "Attestation Description" + } + + Foreach($control in $controls) + { + $AzSKReportTable.Cell($x,1).Range.Text=$control.ControlId + $AzSKReportTable.Cell($x,2).Range.Text=$control.Status + if($control | Get-Member -Name "ResourceGroupName") + { + $AzSKReportTable.Cell($x,3).Range.Text=$control.ResourceGroupName + if(($control | Get-Member -Name "ChildResourceName") -and (-Not [string]::IsNullOrEmpty($control.ChildResourceName))) + { + $AzSKReportTable.Cell($x,4).Range.Text=$control.ResourceName + "/" + $control.ChildResourceName + } + else + { + $AzSKReportTable.Cell($x,4).Range.Text=$control.ResourceName + } + } + else + { + $isSubscriptionCore = $true + $AzSKReportTable.Cell($x,3).Range.Text="Subscription" + $AzSKReportTable.Cell($x,4).Range.Text="Subscription" + } + $AzSKReportTable.Cell($x,5).Range.Text=$control.ControlSeverity + $AzSKReportTable.Cell($x,6).Range.Text=$control.Description + $AzSKReportTable.Cell($x,6).Range.Font.Size = 9 + + if($isAttestedResult -and ($control.AttestedSubStatus)) + { + #$AzSKReportTable.Cell($x,7).Range.Text=$control.ActualStatus + $attstionDescription = "Attested Status: " + $control.AttestedSubStatus + "`vAttested By: " + $control.AttestedBy + "`vJustification: " + $control.AttesterJustification + $AzSKReportTable.Cell($x,7).Range.Text = $attstionDescription + $AzSKReportTable.Cell($x,7).Range.Font.Size = 9 + } + $x++ + + #if(($control | Get-Member -Name "AttestedSubStatus") -and ($control.AttestedSubStatus)) + #{ + #$AzSKReportTable.Cell($x,2).Range.Text= "Actual Status : " + $control.ActualStatus + + #$attstionDescription = "Attestation Description`vAttested Status: " + $control.AttestedSubStatus + "`vAttested By: " + $control.AttestedBy + "`vJustification: " + $control.AttesterJustification + #$AzSKReportTable.Cell($x,6).Range.Text = $attstionDescription + #$AzSKReportTable.Cell($x,6).Range.Font.Size = 9 + #$x++; + # } + } + + $AzSKReportTable.Style = 'Grid Table 4 - Accent 1' + $AzSKReportTable.Columns.Autofit() + $selection = $Word.Selection + $selection.WholeStory + $selection.Style = "No Spacing" + $wdStory = 6 + $wdMove = 0 + + $ret = $selection.EndKey($wdStory, $wdMove) + $selection.TypeParagraph() + $selection.InsertBreak(7) + } + + + #end region + + #region -> Adding PowerShell output + + Get-ChildItem -Path $reportFolderPath -Directory | Where-Object {($_.Name -eq "etc")} | ForEach-Object { + $rootfolder = $_ + [AzSKPDFExtension]::WriteText($selection, 'PowerShell Output','Heading 1', $false, $true, $false) + + Get-ChildItem -Path $rootfolder.FullName -Recurse -Filter "PowerShellOutput.LOG" | ForEach-Object { + $logfilepath = $_ + $log = Get-Content $logfilepath.FullName | Out-String + [AzSKPDFExtension]::WriteText($selection, $log,'No Spacing', $false, $true, $false) + $selection.TypeText("#################################################################"); + $selection.TypeParagraph() + } + } + + $selection.InsertBreak(7) + + #end region -> Adding PowerShell output + + #region -> Adding detailed logs + + [AzSKPDFExtension]::WriteText($selection, 'Detailed Output','Heading 1', $false, $true, $false) + $selection.InsertBreak(6) + + Get-ChildItem -Path $reportFolderPath -Directory | Where-Object {-not ($_.Name -eq "etc")} | ForEach-Object { + $rootfolder = $_ + + if($isSubscriptionCore) + { + [AzSKPDFExtension]::WriteText($selection, 'Subscription Name: '+ ($rootfolder.Name),'Heading 2', $false, $true, $false) + } + else + { + [AzSKPDFExtension]::WriteText($selection, 'Resource Group Name: ' + ($rootfolder.Name),'Heading 2', $false, $true, $false) + } + Get-ChildItem -Path $rootfolder.FullName -Recurse -Filter "*.LOG" | ForEach-Object { + $logfilepath = $_ + [AzSKPDFExtension]::WriteText($selection, 'Resource Type: ' + ($logfilepath.BaseName),'Heading 3', $false, $true, $false) + $logs = Get-Content $logfilepath.FullName + ForEach($log in $logs) + { + [AzSKPDFExtension]::WriteText($selection, ($log | Out-String),'No Spacing', $false, $false, $false) + } + + $selection.TypeParagraph() + $selection.InsertBreak(7) + } + } + + #end region + + # Update table of content + $toc.Update() + } + } + catch + { + throw $_.Exception + } + finally + { + $wdExportFormatPDF = 17 + $wdDoNotSaveChanges = 0 + $AzSKReportDoc.ExportAsFixedFormat($pdfPath,$wdExportFormatPDF) + $AzSKReportDoc.close([ref]$wdDoNotSaveChanges) + $Word.Quit() + if (test-path variable:AzSKReportDoc) + { + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($AzSKReportDoc) | Out-Null + } + if (test-path variable:word) + { + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($word) | Out-Null + } + if (test-path variable:range) + { + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($range) | Out-Null + } + if (test-path variable:ReportRange) + { + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ReportRange) | Out-Null + } + if (test-path variable:AzSKReportTable) + { + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($AzSKReportTable) | Out-Null + } + if (test-path variable:TitleTableRange) + { + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($TitleTableRange) | Out-Null + } + if (test-path variable:AzSKTitleTable) + { + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($AzSKTitleTable) | Out-Null + } + + Remove-Variable range + [gc]::collect() + [gc]::WaitForPendingFinalizers() + } + } + else + { + throw ([SuppressedException]::new(("You must have Microsoft Word application installed on machine to generate PDF report."), [SuppressedExceptionType]::Generic)) + } + } + + static [void] WriteText([PSObject] $selectionObj, [string] $textToWrite, [string] $style, [bool] $bold, [bool] $newParagraph, [bool] $newLine) + { + $selectionObj.TypeText($textToWrite); + $selectionObj.Style = $style + if($bold) + { + $selectionObj.Range.Font.Bold = 1 + } + else + { + $selectionObj.Range.Font.Bold = 0 + } + + if($newParagraph) + { + $selectionObj.TypeParagraph() + } + if($newLine) + { + $selectionObj.TypeText("`v"); + } + $selectionObj.WholeStory + $selectionObj.Style = "No Spacing" + } + + static [void] WriteHeaderTableCell([PSObject] $tableObj, [int] $row, [string] $title, [string] $value) + { + $tableObj.Cell($row,1).Range.Text = $title + $tableObj.Cell($row,1).Range.Bold = 1 + $tableObj.Cell($row,2).Range.Text = $value + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Managers/ConfigurationManager.ps1 b/src/AzSK.AAD/0.9.0/Framework/Managers/ConfigurationManager.ps1 new file mode 100644 index 000000000..dc9024ba3 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Managers/ConfigurationManager.ps1 @@ -0,0 +1,116 @@ +Set-StrictMode -Version Latest +# +# ConfigManager.ps1 +# +class ConfigurationManager +{ + hidden static [AzSKConfig] GetAzSKConfigData() + { + return [AzSKConfig]::GetInstance([ConfigurationManager]::GetAzSKSettings().UseOnlinePolicyStore, [ConfigurationManager]::GetAzSKSettings().OnlinePolicyStoreUrl, [ConfigurationManager]::GetAzSKSettings().EnableAADAuthForOnlinePolicyStore) + } + + hidden static [AzSKSettings] GetAzSKSettings() + { + return [AzSKSettings]::GetInstance() + } + + hidden static [AzSKSettings] GetLocalAzSKSettings() + { + return [AzSKSettings]::GetLocalInstance() + } + + hidden static [AzSKSettings] UpdateAzSKSettings([AzSKSettings] $localSettings) + { + return [AzSKSettings]::Update($localSettings) + } + + hidden static [SVTConfig] GetSVTConfig([string] $fileName) + { + $defaultConfigFile = [ConfigurationHelper]::LoadServerConfigFile($fileName, [ConfigurationManager]::GetAzSKSettings().UseOnlinePolicyStore, [ConfigurationManager]::GetAzSKSettings().OnlinePolicyStoreUrl, [ConfigurationManager]::GetAzSKSettings().EnableAADAuthForOnlinePolicyStore); + $extendedFileName = $fileName.Replace(".json",".ext.json"); + $extendedConfigFile = [ConfigurationHelper]::LoadServerFileRaw($extendedFileName, [ConfigurationManager]::GetAzSKSettings().UseOnlinePolicyStore, [ConfigurationManager]::GetAzSKSettings().OnlinePolicyStoreUrl, [ConfigurationManager]::GetAzSKSettings().EnableAADAuthForOnlinePolicyStore); + $finalObject = [SVTConfig] $defaultConfigFile; + if(-not [string]::IsNullOrWhiteSpace($extendedConfigFile)) + { + $IdPropName = "Id" + $finalObject = [SVTConfig]([Helpers]::MergeObjects($defaultConfigFile,$extendedConfigFile, $IdPropName)); + } + return $finalObject; + } + + hidden static [PSObject] LoadServerConfigFile([string] $fileName) + { + return [ConfigurationHelper]::LoadServerConfigFile($fileName, [ConfigurationManager]::GetAzSKSettings().UseOnlinePolicyStore, [ConfigurationManager]::GetAzSKSettings().OnlinePolicyStoreUrl, [ConfigurationManager]::GetAzSKSettings().EnableAADAuthForOnlinePolicyStore); + } + + hidden static [PSObject] LoadServerFileRaw([string] $fileName) + { + return [ConfigurationHelper]::LoadServerFileRaw($fileName, [ConfigurationManager]::GetAzSKSettings().UseOnlinePolicyStore, [ConfigurationManager]::GetAzSKSettings().OnlinePolicyStoreUrl, [ConfigurationManager]::GetAzSKSettings().EnableAADAuthForOnlinePolicyStore); + } + + hidden static [string] LoadExtensionFile([string] $svtClassName) + { + $extensionSVTClassName = $svtClassName + "Ext"; + $extensionFilePath = "" + #check for extension type only if we dont find the type already loaded in to the current session + if(-not ($extensionSVTClassName -as [type])) + { + $extensionSVTClassFileName = $svtClassName + ".ext.ps1"; + try { + $extensionFilePath = [ConfigurationManager]::DownloadExtFile($extensionSVTClassFileName) + } + catch { + [EventBase]::PublishGenericException($_); + } + } + return $extensionFilePath + } + + hidden static [string[]] RegisterExtListenerFiles() + { + $ServerConfigMetadata = [ConfigurationManager]::LoadServerConfigFile([Constants]::ServerConfigMetadataFileName) + $ListenerFilePaths = @(); + if($null -ne [ConfigurationHelper]::ServerConfigMetadata) + { + [ConfigurationHelper]::ServerConfigMetadata.OnlinePolicyList | ForEach-Object { + if([Helpers]::CheckMember($_,"Name")) + { + if($_.Name -match "Listener.ext.ps1") + { + $listenerFileName = $_.Name + try { + $extensionFilePath = [ConfigurationManager]::DownloadExtFile($listenerFileName) + # file has to be loaded here due to scope constraint + $ListenerFilePaths += $extensionFilePath + } + catch { + [EventBase]::PublishGenericException($_); + } + } + } + } + } + return $ListenerFilePaths; + } + + hidden static [string] DownloadExtFile([string] $fileName) + { + $localExtensionsFolderPath = [Constants]::AzSKExtensionsFolderPath; + $extensionFilePath = "" + + if(-not (Test-Path -Path $localExtensionsFolderPath)) + { + mkdir -Path $localExtensionsFolderPath -Force + } + + $extensionScriptCode = [ConfigurationManager]::LoadServerFileRaw($fileName); + + if(-not [string]::IsNullOrWhiteSpace($extensionScriptCode)) + { + $extensionFilePath = "$([Constants]::AzSKExtensionsFolderPath)\$fileName"; + Out-File -InputObject $extensionScriptCode -Force -FilePath $extensionFilePath -Encoding utf8; + } + + return $extensionFilePath + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Managers/FeatureFlightingManager.ps1 b/src/AzSK.AAD/0.9.0/Framework/Managers/FeatureFlightingManager.ps1 new file mode 100644 index 000000000..98626c5c0 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Managers/FeatureFlightingManager.ps1 @@ -0,0 +1,57 @@ +Set-StrictMode -Version Latest +# +# FeatureFlightingManager.ps1 +# +class FeatureFlightingManager +{ + hidden static [FeatureFlight] $FeatureFlight = $null; + + hidden static [bool] GetFeatureStatus([string] $FeatureName, [string] $tenantId) + { + $featureStatus = $true; + if($null -eq [FeatureFlightingManager]::FeatureFlight) + { + [FeatureFlightingManager]::FeatureFlight = [FeatureFlightingManager]::FetchFeatureFlightConfigData(); + } + $feature = [FeatureFlightingManager]::FeatureFlight.Features | Where-Object { $_.Name -eq $FeatureName}; + if(($feature | Measure-Object).Count -eq 1) + { + if($feature.IsEnabled -eq $true) + { + #Print preview note if the preview flag is enabled for this feature + if($feature.UnderPreview -eq $true) + { + [EventBase]::PublishGenericCustomMessage("Note: $FeatureName Feature is currently under Preview.", [MessageType]::Info); + } + $scanSource = [AzSKSettings]::GetInstance().GetScanSource(); + $compatibleScanSource = ($feature.Sources | Where-Object { $_ -eq $scanSource -or $_ -eq "*" } | Measure-Object).Count -gt 0; + #Check if the feature scan source is compatible + if(-not $compatibleScanSource) + { + $featureStatus = $false; + } + #Check if the sub is marked under disabled list for this feature + elseif(($feature.DisabledForSubs | Measure-Object).Count -gt 0 -and ($feature.DisabledForSubs | Where-Object { $_ -eq $tenantId } | Measure-Object).Count -eq 1) + { + $featureStatus = $false; + } + #Check if the sub is marked under enabled list or * for this feature + elseif(($feature.EnabledForSubs | Measure-Object).Count -gt 0 -and ($feature.EnabledForSubs | Where-Object { $_ -eq $tenantId -or $_ -eq "*"} | Measure-Object).Count -eq 0) + { + $featureStatus = $false; + } + } + else { + $featureStatus = $false; + } + } + return $featureStatus; + } + + hidden static [FeatureFlight] FetchFeatureFlightConfigData() + { + [FeatureFlight] $flightingData = [FeatureFlight]::new(); + $flightingData = [FeatureFlight] [ConfigurationManager]::LoadServerConfigFile("FeatureFlighting.json"); + return $flightingData; + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/AzSKConfig.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/AzSKConfig.ps1 new file mode 100644 index 000000000..1983b920e --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/AzSKConfig.ps1 @@ -0,0 +1,192 @@ +Set-StrictMode -Version Latest +class AzSKConfig +{ + [string] $MaintenanceMessage + [string] $AzSKRGName + [string] $AzSKRepoURL + [string] $AzSKServerVersion + [string[]] $SubscriptionMandatoryTags = @() + [string] $ERvNetResourceGroupNames + [string] $UpdateCompatibleCCVersion + [string] $AzSKApiBaseURL; + [bool] $PublishVulnDataToApi; + [string] $ControlTelemetryKey; + [bool] $EnableControlTelemetry; + [string] $PolicyMessage; + [string] $AzSKLocation; + [string] $InstallationCommand; + [string] $PublicPSGalleryUrl; + [string] $AzSKCARunbookVersion; + [string] $AzSKCAMinReqdRunbookVersion; + [string] $AzSKAlertsMinReqdVersion; + [string] $AzSKARMPolMinReqdVersion; + [string[]] $PrivacyAcceptedSources = @(); + [string] $OutputFolderPath; + [int] $BackwardCompatibleVersionCount; + [string[]] $DefaultControlExculdeTags = @() + [string[]] $DefaultControlFiltersTags = @() + [System.Version[]] $AzSKVersionList = @() + [int] $CAScanIntervalInHours; + [string] $ConfigSchemaBaseVersion; + [string] $AzSKASCMinReqdVersion; + #Bool flag to check selfsigned cert to avoid break of current configurations + [bool] $AllowSelfSignedWebhookCertificate; + [bool] $EnableDevOpsKitSetupCheck; + [bool] $UpdateToLatestVersion; + [string] $CASetupRunbookURL; + [string] $AzSKConfigURL; + [bool] $IsAlertMonitoringEnabled; + [string] $SupportDL; + [string] $RunbookScanAgentBaseVersion; + [string] $PolicyOrgName; + [bool] $StoreComplianceSummaryInUserSubscriptions; + [string] $LatestPSGalleryVersion; + [string] $SchemaTemplateURL; + [bool] $EnableAzurePolicyBasedScan; + [string] $AzSKInitiativeName; + hidden static [AzSKConfig] $Instance = $null; + + static [AzSKConfig] GetInstance([bool] $useOnlinePolicyStore, [string] $onlineStoreUri, [bool] $enableAADAuthForOnlinePolicyStore) + { + if ( $null -eq [AzSKConfig]::Instance) + { + [AzSKConfig]::Instance = [AzSKConfig]::LoadRootConfiguration($useOnlinePolicyStore,$onlineStoreUri,$enableAADAuthForOnlinePolicyStore) + } + + return [AzSKConfig]::Instance + } + + hidden static [AzSKConfig] LoadRootConfiguration([bool] $useOnlinePolicyStore, [string] $onlineStoreUri, [bool] $enableAADAuthForOnlinePolicyStore) + { + #Config filename will be static constant + return [AzSKConfig] ([ConfigurationHelper]::LoadServerConfigFile("AzSK.json", $useOnlinePolicyStore, $onlineStoreUri, $enableAADAuthForOnlinePolicyStore)); + } + + hidden [string] GetLatestAzSKVersion([string] $moduleName) + { + if([string]::IsNullOrWhiteSpace($this.AzSKServerVersion)) + { + $this.AzSKServerVersion = "0.0.0.0"; + try + { + + if((-not [string]::IsNullOrWhiteSpace($this.AzSKConfigURL)) -and (-not $this.UpdateToLatestVersion)) + { + try + { + $serverFileContent = [ConfigurationHelper]::InvokeControlsAPI($this.AzSKConfigURL, '', '', ''); + if($null -ne $serverFileContent) + { + if(-not [string]::IsNullOrWhiteSpace($serverFileContent.CurrentVersionForOrg)) + { + $this.AzSKServerVersion = $serverFileContent.CurrentVersionForOrg + } + } + } + catch + { + # If unable to fetch server config file or module version property then continue and download latest version module. + } + } + + if($this.AzSKServerVersion -eq '0.0.0.0') + { + $repoUrl = $this.AzSKRepoURL; + #Searching for the module in the repo + $Url = "$repoUrl/api/v2/Search()?`$filter=IsLatestVersion&searchTerm=%27$moduleName%27&includePrerelease=false" + [System.Uri] $validatedUri = $null; + if([System.Uri]::TryCreate($Url, [System.UriKind]::Absolute, [ref] $validatedUri)) + { + $SearchResult = @() + $SearchResult += Invoke-RestMethod -Method Get -Uri $validatedUri -UseBasicParsing + if($SearchResult.Length -and $SearchResult.Length -gt 0) + { + #filter latest module + $SearchResult = $SearchResult | Where-Object -FilterScript { + return $_.title.'#text' -eq $moduleName + } + $moduleName = $SearchResult.title.'#text' # get correct casing for the module name + $PackageDetails = Invoke-RestMethod -Method Get -UseBasicParsing -Uri $SearchResult.id + $this.AzSKServerVersion = $PackageDetails.entry.properties.version + } + } + } + } + catch + { + $this.AzSKServerVersion = "0.0.0.0"; + } + } + return $this.AzSKServerVersion; + } + + #Function to get list of AzSK version using API + hidden [System.Version[]] GetAzSKVersionList([string] $moduleName) + { + if(($this.AzSKVersionList | Measure-Object).Count -eq 0) + { + try + { + $repoUrl = $this.AzSKRepoURL; + #Searching for the module in the repo + $Url = "$repoUrl/api/v2/FindPackagesById()?id='$moduleName'&`$skip=0&`$top=40&`$orderby=Version desc" + [System.Uri] $validatedUri = $null; + if([System.Uri]::TryCreate($Url, [System.UriKind]::Absolute, [ref] $validatedUri)) + { + $searchResult = Invoke-RestMethod -Method Get -Uri $validatedUri -UseBasicParsing + $versionList =@() + if($searchResult.Length -and $searchResult.Length -gt 0) + { + $versionList += $SearchResult | Where-Object {$_.title.'#text' -eq $ModuleName + } | ForEach-Object {[System.Version] $_.properties.version } + $this.AzSKVersionList = $versionList + } + } + } + catch + { + $this.AzSKVersionList = @(); + } + } + return $this.AzSKVersionList; + } + + hidden [string] GetAzSKLatestPSGalleryVersion([string] $moduleName) + { + if([string]::IsNullOrWhiteSpace($this.LatestPSGalleryVersion)) + { + $this.LatestPSGalleryVersion = "0.0.0.0"; + try + { + if($this.LatestPSGalleryVersion -eq '0.0.0.0') + { + $repoUrl = $this.AzSKRepoURL; + #Searching for the module in the repo + $Url = "$repoUrl/api/v2/Search()?`$filter=IsLatestVersion&searchTerm=%27$moduleName%27&includePrerelease=false" + [System.Uri] $validatedUri = $null; + if([System.Uri]::TryCreate($Url, [System.UriKind]::Absolute, [ref] $validatedUri)) + { + $SearchResult = @() + $SearchResult += Invoke-RestMethod -Method Get -Uri $validatedUri -UseBasicParsing + if($SearchResult.Length -and $SearchResult.Length -gt 0) + { + #filter latest module + $SearchResult = $SearchResult | Where-Object -FilterScript { + return $_.title.'#text' -eq $moduleName + } + $moduleName = $SearchResult.title.'#text' # get correct casing for the module name + $PackageDetails = Invoke-RestMethod -Method Get -UseBasicParsing -Uri $SearchResult.id + $this.LatestPSGalleryVersion = $PackageDetails.entry.properties.version + } + } + } + } + catch + { + $this.LatestPSGalleryVersion = "0.0.0.0"; + } + } + return $this.LatestPSGalleryVersion; + + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/AzSKEvent.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/AzSKEvent.ps1 new file mode 100644 index 000000000..0d8c63979 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/AzSKEvent.ps1 @@ -0,0 +1,39 @@ +Set-StrictMode -Version Latest +class AzSKRootEvent { + static [string] $CustomMessage = "AzSK.CustomMessage"; + + static [string] $GenerateRunIdentifier = "AzSK.GenerateRunIdentifier"; + static [string] $UnsupportedResources = "AzSK.UnsupportedResources"; + static [string] $WriteCSV = "AzSK.WriteCSV"; + static [string] $PublishCustomData = "AzSK.PublishCustomData" + static [string] $WriteExcludedResources= "AzSK.WriteExcludedResources" + + + #Command level event + static [string] $CommandStarted = "AzSK.Command.Started"; + static [string] $CommandCompleted = "AzSK.Command.Completed"; + static [string] $CommandError = "AzSK.Command.Error"; + static [string] $CommandProcessing = "AzSK.Command.Processing"; + + static [string] $PolicyMigrationCommandStarted = "AzSK.Command.PolicyMigrationStarted"; + static [string] $PolicyMigrationCommandCompleted = "AzSK.Command.PolicyMigrationCompleted" +} + +class TenantContext { + [string] $tenantId = ""; + [string] $TenantName = ""; + [string] $Scope = ""; + hidden [hashtable] $SubscriptionMetadata = @{} +} + +class AzSKRootEventArgument { + [TenantContext] $TenantContext; + [MessageData[]] $Messages = @(); + hidden [System.Management.Automation.ErrorRecord] $ExceptionMessage +} +class CustomData { + [string] $Name + [PSObject] $Value +} + + diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/AzSKGenericEvent.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/AzSKGenericEvent.ps1 new file mode 100644 index 000000000..8d182ae70 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/AzSKGenericEvent.ps1 @@ -0,0 +1,75 @@ +Set-StrictMode -Version Latest +class AzSKGenericEvent +{ + static [string] $CustomMessage = "AzSK.Generic.CustomMessage"; #EventArgument: MessageData + static [string] $Exception = "AzSK.Generic.Exception"; #EventArgument: ErrorRecord +} + + + +class MessageDataBase +{ + [string] $Message = ""; + [PSObject] $DataObject; + + MessageDataBase() + { } + + MessageDataBase([string] $message, [PSObject] $dataObject) + { + if($dataObject -and ($dataObject | Measure-Object).Count -ne 0) + { + $this.DataObject = $dataObject; + $this.Message = $message; + } + else + { + # Commented throwing exception + #throw [System.ArgumentException] ("The argument 'dataObject' is null or empty"); + } + } +} + +class MessageData: MessageDataBase +{ + [MessageType] $MessageType = [MessageType]::Info; + + MessageData() + { } + + MessageData([string] $message, [MessageType] $messageType) + { + $this.Message = $message; + $this.MessageType = $messageType; + } + + MessageData([string] $message, [PSObject] $dataObject, [MessageType] $messageType) + { + $this.Message = $message; + $this.DataObject = $dataObject; + $this.MessageType = $messageType; + } + + MessageData([string] $message, [PSObject] $dataObject) + { + $this.Message = $message; + $this.DataObject = $dataObject; + } + + MessageData([string] $message) + { + $this.Message = $message; + } + + MessageData([PSObject] $dataObject) + { + $this.DataObject = $dataObject; + } + + MessageData([PSObject] $dataObject, [MessageType] $messageType) + { + $this.MessageType = $messageType; + $this.DataObject = $dataObject; + } +} + diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/AzSKSettings.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/AzSKSettings.ps1 new file mode 100644 index 000000000..87b2c4bbd --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/AzSKSettings.ps1 @@ -0,0 +1,179 @@ +Set-StrictMode -Version Latest +class AzSKSettings { + [string] $OMSWorkspaceId; + [string] $OMSSharedKey; + [string] $AltOMSWorkspaceId; + [string] $AltOMSSharedKey; + [string] $OMSType; + [string] $OMSSource; + + [string] $EventHubNamespace; + [string] $EventHubName; + [string] $EventHubSendKeyName; + [string] $EventHubSendKey; + [string] $EventHubType; + [string] $EventHubSource; + + [string] $WebhookUrl; + [string] $WebhookAuthZHeaderName; + [string] $WebhookAuthZHeaderValue; + [string] $WebhookType; + [string] $WebhookSource; + [string] $AutoUpdateCommand; + [AutoUpdate] $AutoUpdateSwitch = [AutoUpdate]::NotSet; + + [string] $OutputFolderPath; + + + [TertiaryBool] $AllowSelfSignedWebhookCertificate; + [bool] $EnableAADAuthForOnlinePolicyStore; + [bool] $UseOnlinePolicyStore; + [string] $OnlinePolicyStoreUrl; + [string] $AzureEnvironment; + [string] $UsageTelemetryLevel; + [string] $LocalControlTelemetryKey; + [bool] $LocalEnableControlTelemetry; + [bool] $PrivacyNoticeAccepted = $false; + [bool] $IsCentralScanModeOn = $false; + hidden static [AzSKSettings] $Instance = $null; + hidden static [string] $FileName = "AzSK.AAD.Settings.json"; + [bool] $StoreComplianceSummaryInUserSubscriptions; + [string] $ScanToolPath = [string]::Empty + [string] $ScanToolName = [string]::Empty + + + + hidden static SetDefaultSettings([AzSKSettings] $settings) { + #BUGBUG/TODO: Rename? What does 'Default' imply here? AzureEnvironment? + if($null -ne $settings -and [string]::IsNullOrWhiteSpace( $settings.AzureEnvironment)) + { + $settings.AzureEnvironment = [Constants]::DefaultAzureEnvironment + } + } + + static [AzSKSettings] GetInstance() { + if (-not [AzSKSettings]::Instance) + { + [AzSKSettings]::LoadAzSKSettings($false); + [AzSKSettings]::SetDefaultSettings([AzSKSettings]::Instance); + #todo: change to default env by using a fn + } + + return [AzSKSettings]::Instance + } + + static [AzSKSettings] GetLocalInstance() { + $settings = [AzSKSettings]::LoadAzSKSettings($true); + [AzSKSettings]::SetDefaultSettings($settings); + return $settings + } + + hidden static [AzSKSettings] LoadAzSKSettings([bool] $loadUserCopy) { + #Filename will be static. + #For AzSK Settings, never use online policy store. It's assumed that file will be available offline + #-------- AzSK rename code change--------# + $localAppDataSettings = $null + + # TBR : AzSDK cleanup on local machine for Local settings folder + $AzSDKAppFolderPath = $Env:LOCALAPPDATA + "\Microsoft\" + "AzSDK*" + if(Test-Path -Path $AzSDKAppFolderPath) + { + Get-ChildItem -Path $AzSDKAppFolderPath -Directory | Remove-Item -Recurse -Force + } + + if(-not $localAppDataSettings) + { + $localAppDataSettings = [ConfigurationHelper]::LoadOfflineConfigFile([AzSKSettings]::FileName) + } + + #------------------------------# + [AzSKSettings] $parsedSettings = $null; + [AzSKSettings] $localModuleSettings = $null; + [AzSKSettings] $serverSettings = $null; + $migratedPropNames = @(); + #Validate settings content is not null + if ($localAppDataSettings) { + try + { + #Step1: Try parsing the object from local app data settings. If parse is successful then there is no change to settings schema. + $parsedSettings = [AzSKSettings] $localAppDataSettings + } + catch + { + #Step2: Any error occurred while converting local json file indicates change in schema + #Load latest Settings from modules folder + $parsedSettings = [ConfigurationHelper]::LoadModuleJsonFile([AzSKSettings]::FileName) + $parsedSettings | Get-Member -MemberType Properties | + ForEach-Object { + $propertyName = $_.Name; + if([Helpers]::CheckMember($localAppDataSettings, $propertyName)) + { + $parsedSettings.$propertyName = $localAppDataSettings.$propertyName; + $migratedPropNames += $propertyName; + } + }; + if($migratedPropNames.Count -ne 0) + { + [AzSKSettings]::Update($parsedSettings); + [EventBase]::PublishGenericCustomMessage("Local AzSK settings file was not compatible with the latest version. `r`nMigrated the existing values for properties: [$([string]::Join(", ", $migratedPropNames))] ", [MessageType]::Warning); + } + } + + #Step 3: Get the latest server settings and merge with that + + if(-not $loadUserCopy) + { + [bool] $_useOnlinePolicyStore = $parsedSettings.UseOnlinePolicyStore; + [string] $_onlineStoreUri = $parsedSettings.OnlinePolicyStoreUrl; + [bool] $_enableAADAuthForOnlinePolicyStore = $parsedSettings.EnableAADAuthForOnlinePolicyStore; + $serverSettings = [ConfigurationHelper]::LoadServerConfigFile([AzSKSettings]::FileName, $_useOnlinePolicyStore, $_onlineStoreUri, $_enableAADAuthForOnlinePolicyStore); + + $mergedServerPropNames = @(); + $serverSettings | Get-Member -MemberType Properties | + ForEach-Object { + $propertyName = $_.Name; + if([string]::IsNullOrWhiteSpace($parsedSettings.$propertyName) -and -not [string]::IsNullOrWhiteSpace($serverSettings.$propertyName)) + { + $parsedSettings.$propertyName = $serverSettings.$propertyName; + $mergedServerPropNames += $propertyName; + } + }; + + [AzSKSettings]::Instance = $parsedSettings; + } + #Sever merged settings should not be persisted, as it should always take latest from the server + return $parsedSettings; + } + else + { + return $null; + } + } + + [void] Update() + { + if (-not (Test-Path $([Constants]::AzSKAppFolderPath))) + { + mkdir -Path $([Constants]::AzSKAppFolderPath) -ErrorAction Stop | Out-Null + } + + #persisting back to file + [AzSKSettings]::Instance | ConvertTo-Json | Out-File -Force -FilePath ([Constants]::AzSKAppFolderPath + "\" + [AzSKSettings]::FileName) + } + + static [void] Update([AzSKSettings] $localSettings) + { + if (-not (Test-Path $([Constants]::AzSKAppFolderPath))) + { + mkdir -Path $([Constants]::AzSKAppFolderPath) -ErrorAction Stop | Out-Null + } + + #persisting back to file + $localSettings | ConvertTo-Json | Out-File -Force -FilePath ([Constants]::AzSKAppFolderPath + "\" + [AzSKSettings]::FileName) + } + + hidden [string] GetScanSource() + { + return $this.OMSSource + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/CommandDetails.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/CommandDetails.ps1 new file mode 100644 index 000000000..d2187d490 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/CommandDetails.ps1 @@ -0,0 +1,10 @@ +Set-StrictMode -Version Latest +class CommandDetails +{ + [string] $Noun = ""; + [string] $Verb = ""; + [string] $ShortName = ""; + [bool] $IsLatestRequired = $true; + [bool] $HasAzSKComponentWritePermission = $true + [CommandType] $CommandType = [CommandType]::Azure +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/Common/ResourceInventory.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/Common/ResourceInventory.ps1 new file mode 100644 index 000000000..be17bd038 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/Common/ResourceInventory.ps1 @@ -0,0 +1,24 @@ +Set-StrictMode -Version Latest +class ResourceInventory +{ + static [PSObject[]] $RawResources; + static [PSObject[]] $FilteredResources; + + static [void] FetchResources() + { + if($null -eq [ResourceInventory]::RawResources -or $null -eq [ResourceInventory]::FilteredResources) + { + [ResourceInventory]::RawResources = Get-AzResource + $supportedResourceTypes = [SVTMapping]::GetSupportedResourceMap() + # Not considering nested resources to reduce complexity + [ResourceInventory]::FilteredResources = [ResourceInventory]::RawResources | Where-Object { $supportedResourceTypes.ContainsKey($_.ResourceType.ToLower()) } + } + } + + static [void] Clear() + { + [ResourceInventory]::RawResources = $null; + [ResourceInventory]::FilteredResources = $null; + } + +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/ContinuousAssurance/AutomationAccount.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/ContinuousAssurance/AutomationAccount.ps1 new file mode 100644 index 000000000..294d2b006 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/ContinuousAssurance/AutomationAccount.ps1 @@ -0,0 +1,89 @@ +Set-StrictMode -Version Latest +enum ScheduleFrequency +{ + Hour + Day +} + +class AutomationAccount +{ + hidden [string] $Name + hidden [string] $CoreResourceGroup + hidden [string] $ResourceGroup + hidden [string] $Location + hidden [string] $AzureADAppName + hidden [Hashtable] $RGTags + hidden [Hashtable] $AccountTags + hidden [int] $ScanIntervalInHours + hidden [PSObject] $BasicResourceInstance + hidden [PSObject] $DetailedResourceInstance +} +class UserConfig +{ + hidden [string] $ResourceGroupNames + hidden [OMSCredential] $OMSCredential + hidden [OMSCredential] $AltOMSCredential + hidden [WebhookSetting] $WebhookDetails + hidden [string] $StorageAccountName + hidden [string] $StorageAccountRG +} + +class WebhookSetting +{ + hidden [string] $Url; + hidden [string] $AuthZHeaderName; + hidden [string] $AuthZHeaderValue; +} + +class OMSCredential +{ + hidden [string] $OMSWorkspaceId + hidden [string] $OMSSharedKey +} +class SelfSignedCertificate +{ + hidden [DateTime] $CertStartDate + hidden [DateTime] $CertEndDate + hidden [DateTime] $CredStartDate + hidden [DateTime] $CredEndDate + hidden [string] $Provider + SelfSignedCertificate() + { + $this.CertStartDate = (Get-Date).AddDays(-1); + $this.CertEndDate = (Get-Date).AddMonths(6).AddDays(1); + $this.CredStartDate = (Get-Date); + $this.CredEndDate = (Get-Date).AddMonths(6); + $this.Provider = "Microsoft Enhanced RSA and AES Cryptographic Provider" + } +} + +class Runbook +{ + hidden [string] $Name + hidden [string] $Type + hidden [string] $Description + hidden [boolean] $LogProgress + hidden [boolean] $LogVerbose + hidden [string] $Key + #tags of dictionary type +} + +class RunbookSchedule +{ + hidden [string] $Name + hidden [string] $Frequency + hidden [int] $Interval + hidden [System.DateTime] $StartTime + hidden [System.DateTime] $ExpiryTime + hidden [string] $Description + hidden [string[]] $LinkedRubooks + hidden [string] $Key +} + +class Variable +{ + hidden [string] $Name + hidden [string] $Value + hidden [boolean] $IsEncrypted + hidden [string] $Description +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/ControlState.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/ControlState.ps1 new file mode 100644 index 000000000..e992b9645 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/ControlState.ps1 @@ -0,0 +1,41 @@ +Set-StrictMode -Version Latest + +class ControlState +{ + ControlState() + { + + } + + ControlState([string] $ControlId, [string] $InternalId, [string] $ChildResourceName, [string] $ActualVerificationResult, [string] $Version) + { + $this.ControlId = $ControlId; + $this.InternalId = $InternalId; + $this.ChildResourceName = $ChildResourceName; + $this.ActualVerificationResult = $ActualVerificationResult; + #setting the effective control result default value actual. It would be reset once it is computed based on user input + $this.EffectiveVerificationResult = $ActualVerificationResult; + $this.Version = $Version; + } + + [string] $ControlId + [string] $InternalId + [string] $ResourceId + [string] $HashId + [StateData] $State + [string] $ChildResourceName + [VerificationResult] $ActualVerificationResult + [VerificationResult] $EffectiveVerificationResult + [AttestationStatus] $AttestationStatus = [AttestationStatus]::None + [string] $Version +} + +class ControlStateIndexer +{ + [string] $ResourceId + [string] $HashId + [DateTime] $ExpiryTime + [string] $AttestedBy + [DateTime] $AttestedDate + [string] $Version +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/Enums.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/Enums.ps1 new file mode 100644 index 000000000..f05f86238 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/Enums.ps1 @@ -0,0 +1,164 @@ +Set-StrictMode -Version Latest +enum VerificationResult +{ + Passed + Failed + Verify + Manual + RiskAck + Error + Disabled + Exception + Remediate + Skipped + NotScanned +} + +enum AttestationStatus +{ + None + NotAnIssue + NotFixed + WillNotFix + WillFixLater + NotApplicable + StateConfirmed +} + +enum AttestControls +{ + None + All + AlreadyAttested + NotAttested +} + +enum MessageType +{ + Critical + Error + Warning + Info + Update + Deprecated + Default +} + +enum ControlSeverity +{ + Critical + High + Medium + Low +} + + +enum ScanSource +{ + SpotCheck + VSO + Runbook +} + +enum FeatureGroup +{ + Unknown + Subscription + Service +} + +enum ServiceScanKind +{ + Partial + ResourceGroup + Subscription +} + +enum SubscriptionScanKind +{ + Partial + Complete +} + +enum OMSInstallationOption +{ + All + Queries + Alerts + SampleView + GenericView +} + +enum GeneratePDF +{ + None + Landscape + Portrait +} + +enum CAReportsLocation +{ + CentralSub + IndividualSubs +} + +enum InfoType +{ + SubscriptionInfo + ControlInfo + HostInfo + AttestationInfo + ComplianceInfo +} + +enum AutoUpdate +{ + On + Off + NotSet +} + +enum StorageContainerType +{ + AttestationDataContainer + CAMultiSubScanConfigContainer + ScanProgressSnapshotsContainer + CAScanOutputLogsContainer +} + +enum TertiaryBool +{ + False + True + NotSet +} + +enum ComparisionType +{ + NumLesserOrEqual +} + +enum OverrideConfigurationType +{ + Installer + CARunbooks + AzSKRootConfig + MonitoringDashboard + OrgAzSKVersion + All + None +} + +enum RemoveConfiguredCASetting +{ + OMSSettings + AltOMSSettings + WebhookSettings +} + +enum CommandType +{ + Azure + AzureDevOps + AAD +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/Exception/SuppressedException.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/Exception/SuppressedException.ps1 new file mode 100644 index 000000000..f3fc7c89c --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/Exception/SuppressedException.ps1 @@ -0,0 +1,36 @@ +Set-StrictMode -Version Latest + +enum SuppressedExceptionType +{ + InvalidArgument + NullArgument + Generic + InvalidOperation + AccessDenied +} + +class SuppressedException : System.Exception +{ + [SuppressedExceptionType] $ExceptionType = [SuppressedExceptionType]::InvalidArgument + SuppressedException($message): + Base($message) + { } + + SuppressedException($message, [SuppressedExceptionType] $exceptionType): + Base($message) + { + $this.ExceptionType = $exceptionType; + } + + [string] ConvertToString() + { + $result = ""; + if($this.ExceptionType -ne [SuppressedExceptionType]::Generic) + { + $result = $this.ExceptionType.ToString() + ": " ; + } + $result = $result + $this.Message; + + return $result; + } +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/FeatureFlight.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/FeatureFlight.ps1 new file mode 100644 index 000000000..d9509d2a0 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/FeatureFlight.ps1 @@ -0,0 +1,17 @@ +Set-StrictMode -Version Latest +class FeatureFlight +{ + [string] $Version + [Feature[]] $Features +} + +class Feature +{ + [string] $Name; + [string] $Description; + [string[]] $Sources; + [string[]] $EnabledForSubs; + [string[]] $DisabledForSubs; + [bool] $UnderPreview; + [bool] $IsEnabled; +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/FixControl/FixControlModel.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/FixControl/FixControlModel.ps1 new file mode 100644 index 000000000..724eeda33 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/FixControl/FixControlModel.ps1 @@ -0,0 +1,55 @@ +Set-StrictMode -Version Latest + +class FixControlConfig +{ + [TenantContext] $TenantContext; + [ResourceGroupConfig[]] $ResourceGroups = @(); + [ControlParam[]] $SubscriptionControls = @(); +} + +class ResourceGroupConfig +{ + [string] $ResourceGroupName = "" + [ResourceConfig[]] $Resources = @(); +} + +class ResourceConfig +{ + [string] $ResourceName = "" + [string] $ResourceType = "" + [string] $ResourceTypeName = "" + [ControlParam[]] $Controls = @(); + hidden [ResourceTypeMapping] $ResourceTypeMapping = $null; +} + +class ControlParam +{ + [string] $ControlID = "" + [string] $Id = "" + [string] $ControlSeverity = [ControlSeverity]::High + [FixControlImpact] $FixControlImpact = [FixControlImpact]::High; + [string] $Description = ""; + [bool] $Enabled = $true; + + [ChildResourceParam[]] $ChildResourceParams = @(); +} + +class ChildResourceParam +{ + [string] $ChildResourceName = "" + [PSObject] $Parameters = $null; +} + + +class ArrayWrapper +{ + [PSObject[]] $Values = @(); + ArrayWrapper([PSObject[]] $values) + { + $this.Values = @(); + if($values) + { + $this.Values += $values; + } + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/ComplianceStateModel.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/ComplianceStateModel.ps1 new file mode 100644 index 000000000..dff9a9842 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/ComplianceStateModel.ps1 @@ -0,0 +1,78 @@ +Set-StrictMode -Version Latest + +class ComplianceStateTableEntity +{ + #partition key = resourceid/tenantId + [string] $PartitionKey; + #row key = controlid + [string] $RowKey; + [string] $ResourceId = ""; + [string] $LastEventOn = [Constants]::AzSKDefaultDateTime; + [string] $ResourceGroupName = ""; + [string] $ResourceName = ""; + [string] $FeatureName = ""; + + #Default control values + [string] $ControlId = ""; + [string] $ControlIntId = ""; + [string] $ControlUpdatedOn = [Constants]::AzSKDefaultDateTime; + [string] $ControlSeverity = ([ControlSeverity]::High).ToString(); + [string] $ActualVerificationResult= ([VerificationResult]::Manual).ToString(); + [string] $AttestationStatus = ([AttestationStatus]::None).ToString(); + [string] $VerificationResult = ([VerificationResult]::Manual).ToString(); + [string] $AttestedBy = ""; + [string] $AttestedDate = [Constants]::AzSKDefaultDateTime; + [string] $Justification = ""; + [string] $PreviousVerificationResult = ([VerificationResult]::Manual).ToString(); + [bool] $IsBaselineControl; + [bool] $HasOwnerAccessTag; + + #Tracking information + [string] $LastResultTransitionOn = [Constants]::AzSKDefaultDateTime; + [string] $LastScannedOn = [Constants]::AzSKDefaultDateTime; + [string] $FirstScannedOn = [Constants]::AzSKDefaultDateTime; + [string] $FirstFailedOn = [Constants]::AzSKDefaultDateTime; + [string] $FirstAttestedOn = [Constants]::AzSKDefaultDateTime; + [bool] $IsControlInGrace; + [int] $AttestationCounter = 0; + + #Other information + [string] $ScannedBy = ""; + [string] $ScanSource; + [string] $ScannerModuleName = ""; + [string] $ScannerVersion = ""; + [bool] $IsLatestPSModule; + [bool] $HasRequiredPermissions; + [bool] $HasAttestationWritePermissions; + [bool] $HasAttestationReadPermissions; + [string] $UserComments = ""; + [string] $ChildResourceName = ""; + [bool] $IsActive = $true; + + [string] GetPartitionKey() + { + $HashId = [Helpers]::ComputeHash($this.ResourceId.ToLower()); + + return $HashId; + } + + [string] GetRowKey() + { + $partsToHash = $this.ControlIntId; + if(-not [string]::IsNullOrWhiteSpace($this.ChildResourceName)) + { + $partsToHash = $partsToHash + ":" + $this.ChildResourceName; + } + $HashId = [Helpers]::ComputeHash($partsToHash.ToLower()); + return $HashId; + } + + # static [ComplianceStateTableEntity] CreateEmptyResource([string] $resourceId, [string] $hashId) + # { + # [ComplianceStateTableEntity] $emptyResourceEntity = [ComplianceStateTableEntity]::new(); + # $emptyResourceEntity.PartitionKey = $hashId; + # $emptyResourceEntity.RowKey = "EmptyResource"; + # $emptyResourceEntity.ResourceId = $resourceId; + # return $emptyResourceEntity; + # } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/CsvOutputModel.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/CsvOutputModel.ps1 new file mode 100644 index 000000000..0ddb8aecc --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/CsvOutputModel.ps1 @@ -0,0 +1,26 @@ +Set-StrictMode -Version Latest + +class CsvOutputItem +{ + #Fields from JSON + [string] $ControlID = "" + [string] $Status = "" + [string] $FeatureName = "" + [string] $ResourceGroupName = "" + [string] $ResourceName = "" + [string] $ChildResourceName = "" + [string] $ControlSeverity = "" + [string] $IsBaselineControl = "" + [string] $IsControlInGrace="" + [string] $SupportsAutoFix = "" + [string] $Description = "" + [string] $ActualStatus = "" + [string] $AttestedSubStatus = "" + [string] $AttestationExpiryDate = "" + [string] $AttestedBy = "" + [string] $AttesterJustification = "" + [string] $Recommendation = "" + [string] $ResourceId = "" + [string] $DetailedLogFile = "" + [string] $UserComments = "" +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/LSRScanResultModel.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/LSRScanResultModel.ps1 new file mode 100644 index 000000000..de8b1d6fe --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/LSRScanResultModel.ps1 @@ -0,0 +1,93 @@ +Set-StrictMode -Version Latest +# LSR = LocalSubscriptionReport + +class LocalSubscriptionReport +{ + [LSRSubscription[]] $Subscriptions = @(); +} + +class LSRSubscription +{ + [string] $tenantId = ""; + [string] $TenantName = ""; + [LSRScanDetails] $ScanDetails = $null; + [string] $SubscriptionMetadata = ""; + [string] $SchemaVersion = ""; + + + LSRSubscription() { + $this.SchemaVersion = "1.0" + } +} + +class LSRScanDetails +{ + [LSRSubscriptionControlResult[]] $SubscriptionScanResult = @(); + [LSRResources[]] $Resources = @(); +} + +class LSRResources +{ + [string] $HashId = ""; + [string] $ResourceId = ""; + [DateTime] $LastEventOn = [Constants]::AzSKDefaultDateTime; + [DateTime] $FirstScannedOn = [Constants]::AzSKDefaultDateTime; + + [string] $ResourceGroupName = ""; + [string] $ResourceName = ""; + [string] $ResourceMetadata = ""; + [string] $FeatureName = ""; + + [LSRResourceScanResult[]] $ResourceScanResult = @(); +} + +class LSRControlResultBase +{ + #Default control values + [string] $ControlId = ""; + [string] $ControlIntId = ""; + [DateTime] $ControlUpdatedOn = [Constants]::AzSKDefaultDateTime; + [string] $ControlSeverity = [ControlSeverity]::High + [VerificationResult] $ActualVerificationResult= [VerificationResult]::Manual; + [AttestationStatus] $AttestationStatus = [AttestationStatus]::None; + [VerificationResult] $VerificationResult = [VerificationResult]::Manual; + [string] $AttestedBy = ""; + [DateTime] $AttestedDate = [Constants]::AzSKDefaultDateTime; + [string] $Justification = ""; + [string] $PreviousVerificationResult = [VerificationResult]::Manual; + [PSObject] $AttestationData; + [bool] $IsBaselineControl; + [bool] $HasOwnerAccessTag; + + #Tracking information + [DateTime] $LastResultTransitionOn = [Constants]::AzSKDefaultDateTime; + [DateTime] $LastScannedOn = [Constants]::AzSKDefaultDateTime; + [DateTime] $FirstScannedOn = [Constants]::AzSKDefaultDateTime; + [DateTime] $FirstFailedOn = [Constants]::AzSKDefaultDateTime; + [DateTime] $FirstAttestedOn = [Constants]::AzSKDefaultDateTime; + [int] $AttestationCounter = 0; + + #Other information + [string] $ScannedBy = ""; + [ScanSource] $ScanSource; + [string] $ScannerModuleName = ""; + [string] $ScannerVersion = ""; + [string] $ControlVersion = ""; + [bool] $IsLatestPSModule; + [bool] $HasRequiredPermissions; + [bool] $HasAttestationWritePermissions; + [bool] $HasAttestationReadPermissions; + + + [string] $UserComments = ""; + [string] $Metadata = ""; +} + +class LSRSubscriptionControlResult : LSRControlResultBase { + [SubscriptionScanKind] $ScanKind = [SubscriptionScanKind]::Partial; +} + +class LSRResourceScanResult : LSRControlResultBase { + [ServiceScanKind] $ScanKind = [ServiceScanKind]::Partial; + [string] $ChildResourceName = ""; +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/RecommendationReportModel.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/RecommendationReportModel.ps1 new file mode 100644 index 000000000..89c2aa460 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/RecommendationReportModel.ps1 @@ -0,0 +1,32 @@ +Set-StrictMode -Version Latest + +class SecurityReportInput +{ + [string[]] $Categories = @(); + [string[]] $Features = @(); +} + +class RecommendedSecureCombination +{ + [RecommendedFeatureGroup[]] $RecommendedFeatureGroups; + [RecommendedFeatureGroup] $CurrentFeatureGroup; +} + +class RecommendedFeatureGroup{ + [string[]] $Features; + [string[]] $Categories; + [int] $Ranking; + [int] $TotalSuccessCount; + [int] $TotalFailCount; + [float] $UsagePercentage; + [int] $TotalOccurances; + [float] $FailureRate; + [string] $OtherMostUsed; +} + +class RecommendedSecurityReport +{ + [SecurityReportInput] $Input; + [string] $ResourceGroupName; + [RecommendedSecureCombination] $Recommendations; +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/ScanResultModels.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/ScanResultModels.ps1 new file mode 100644 index 000000000..569573100 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/RemoteReports/ScanResultModels.ps1 @@ -0,0 +1,63 @@ +Set-StrictMode -Version Latest + +class ScanInfoBase { + [ScanInfoVersion] $ScanInfoVersion; + [string] $tenantId; + [string] $TenantName; + [ScanSource] $Source; + [string] $ScannerVersion; + [string] $ControlVersion; + [string] $Metadata; + [bool] $HasAttestationWritePermissions = $false + [bool] $HasAttestationReadPermissions = $false + [bool] $IsLatestPSModule + + ScanInfoBase() { + $this.ScanInfoVersion = [ScanInfoVersion]::V1 + } +} + +class ControlResultBase { + [string] $ControlId; + [string] $ControlIntId; + [string] $ControlSeverity; + [VerificationResult] $ActualVerificationResult; + [AttestationStatus] $AttestationStatus; + [DateTime] $AttestedDate = [Constants]::AzSKDefaultDateTime; + [VerificationResult] $VerificationResult; + [bool] $HasRequiredAccess = $true; + [string] $AttestedBy; + [string] $Justification; + [string] $AttestedState; + [string] $CurrentState; + [bool] $IsBaselineControl; + [string] $UserComments; + [bool] $HasOwnerAccessTag; + [int] $MaximumAllowedGraceDays=0; +} + +class SubscriptionControlResult : ControlResultBase { +} + +class ServiceControlResult : ControlResultBase { + [bool] $IsNestedResource; + [string] $NestedResourceName; +} + +class SubscriptionScanInfo : ScanInfoBase { + [SubscriptionScanKind] $ScanKind; + [SubscriptionControlResult[]] $ControlResults; +} + +class ServiceScanInfo : ScanInfoBase { + [string] $Feature; + [ServiceScanKind] $ScanKind; + [string] $ResourceGroup; + [string] $ResourceName; + [string] $ResourceId; + [ServiceControlResult[]] $ControlResults; +} + +enum ScanInfoVersion { + V1 +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/SVT/AttestationOptions.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/SVT/AttestationOptions.ps1 new file mode 100644 index 000000000..eae7c4f5a --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/SVT/AttestationOptions.ps1 @@ -0,0 +1,9 @@ +Set-StrictMode -Version Latest + +class AttestationOptions +{ + [AttestControls] $AttestControls = [AttestControls]::None + [bool] $IsBulkClearModeOn = $false; + [string] $JustificationText; + [AttestationStatus] $AttestationStatus; +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/SVT/PartialScanResourceMap.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/SVT/PartialScanResourceMap.ps1 new file mode 100644 index 000000000..05c744cba --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/SVT/PartialScanResourceMap.ps1 @@ -0,0 +1,28 @@ +Set-StrictMode -Version Latest +class PartialScanResourceMap +{ + [string] $Id + [DateTime] $CreatedDate + [PSObject] $ResourceMapTable +} + +class PartialScanResource +{ + [string] $Id + [ScanState] $State + [int] $ScanRetryCount + [DateTime] $CreatedDate + [DateTime] $ModifiedDate +} + +enum ActiveStatus{ + NotStarted + Yes + No +} + +enum ScanState{ + INIT + COMP + ERR +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/SVT/SVTConfig.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/SVT/SVTConfig.ps1 new file mode 100644 index 000000000..ecf700a43 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/SVT/SVTConfig.ps1 @@ -0,0 +1,60 @@ +Set-StrictMode -Version Latest +class SVTConfig +{ + [string] $FeatureName = "" + [string] $Reference = "" + [bool] $IsMaintenanceMode + [ControlItem[]] $Controls = @(); + + static [SVTConfig] LoadServerConfigFile([string] $fileName, [bool] $useOnlinePolicyStore, [string] $onlineStoreUri, [bool] $enableAADAuthForOnlinePolicyStore) + { + return [SVTConfig]([ConfigurationHelper]::LoadServerConfigFile($fileName, $useOnlinePolicyStore, $onlineStoreUri, $enableAADAuthForOnlinePolicyStore)); + } + + static [SVTConfig] LoadServerFileRaw([string] $fileName, [bool] $useOnlinePolicyStore, [string] $onlineStoreUri, [bool] $enableAADAuthForOnlinePolicyStore) + { + return [SVTConfig]([ConfigurationHelper]::LoadServerFileRaw($fileName, $useOnlinePolicyStore, $onlineStoreUri, $enableAADAuthForOnlinePolicyStore)); + } +} + +class ControlItem +{ + #Fields from JSON + [string] $ControlID = "" + [string] $Id = "" + [string] $ControlSeverity = [ControlSeverity]::High + [string] $Description = "" + [string] $Automated = "" + [string[]] $Tags = @() + [bool] $Enabled + hidden [string] $MethodName = "" + [string] $Recommendation = "" + [string] $Rationale = "" + hidden [string[]] $DataObjectProperties = @() + hidden [string] $AttestComparisionType = "" + hidden [FixControl] $FixControl = $null; + [int] $AttestationExpiryPeriodInDays + [bool] $IsBaselineControl + [DateTime] $GraceExpiryDate + [int] $NewControlGracePeriodInDays + [int] $AttestationPeriodInDays + [string[]] $ValidAttestationStates + [string] $PolicyDefinitionGuid + [string] $PolicyDefnResourceIdSuffix + [string] $policyDefinitionId +} + +class FixControl +{ + [string] $FixMethodName = "" + [FixControlImpact] $FixControlImpact = [FixControlImpact]::High; + [PSObject] $Parameters = $null; +} + +enum FixControlImpact +{ + Critical + High + Medium + Low +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/SVT/SVTEvent.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/SVT/SVTEvent.ps1 new file mode 100644 index 000000000..37afb9792 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/SVT/SVTEvent.ps1 @@ -0,0 +1,182 @@ +Set-StrictMode -Version Latest + +class SVTEvent +{ + #First level event + + #Command level event + static [string] $CommandStarted = "AzSK.SVT.Command.Started"; #Initialize listeners #Function execution started + static [string] $CommandCompleted = "AzSK.SVT.Command.Completed"; #Cleanup listeners #Function execution completed + static [string] $CommandError = "AzSK.SVT.Command.Error"; + + #Second level event for every resource + static [string] $EvaluationStarted = "AzSK.SVT.Evaluation.Started"; #Individual Resource execution started + static [string] $EvaluationCompleted = "AzSK.SVT.Evaluation.Completed"; #Individual Resource execution completed + static [string] $EvaluationError = "AzSK.SVT.Evaluation.Error"; + + #Control level events + static [string] $ControlStarted = "AzSK.SVT.Control.Started"; #Individual control execution started + static [string] $ControlCompleted = "AzSK.SVT.Control.Completed"; #Individual control execution completed + static [string] $ControlError = "AzSK.SVT.Control.Error"; #Error while control execution + static [string] $ControlDisabled = "AzSK.SVT.Control.Disabled"; #Event if control is in disabled mode + + #Resource and Control Level event + static [string] $WriteInventory = "AzSK.SVT.WriteInventory"; #Custom event to write resource inventory +} + +class ResourceContext +{ + [string] $ResourceId ="" + [string] $ResourceGroupName = "" + [string] $ResourceName = "" + [string] $ResourceType = "" + [hashtable] $ResourceMetadata = @{} + [string] $ResourceTypeName = "" +} + +class ControlResult +{ + [string] $ChildResourceName = ""; + + [VerificationResult] $VerificationResult = [VerificationResult]::Manual; + [VerificationResult] $ActualVerificationResult = [VerificationResult]::Manual; + [SessionContext] $CurrentSessionContext = [SessionContext]::new(); + [AttestationStatus] $AttestationStatus = [AttestationStatus]::None; + + [StateManagement] $StateManagement = [StateManagement]::new(); + hidden [PSObject] $FixControlParameters = $null; + hidden [bool] $EnableFixControl = $false; + [bool] $IsControlInGrace; + [DateTime] $FirstFailedOn = [Constants]::AzSKDefaultDateTime; + [DateTime] $FirstScannedOn = [Constants]::AzSKDefaultDateTime; + [int] $MaximumAllowedGraceDays=0; + [String] $UserComments + [MessageData[]] $Messages = @(); + + [void] AddMessage([MessageData] $messageData) + { + if((-not [string]::IsNullOrEmpty($messageData.Message)) -or ($null -ne $messageData.DataObject)) + { + $this.Messages += $messageData; + } + } + + [void] AddMessage([VerificationResult] $result, [MessageData] $messageData) + { + $this.VerificationResult = $result; + $this.AddMessage($messageData); + } + + [void] AddMessage([VerificationResult] $result, [string] $message, [PSObject] $dataObject) + { + $this.VerificationResult = $result; + $this.AddMessage([MessageData]::new($message, $dataObject)); + } + + [void] AddMessage([string] $message, [PSObject] $dataObject) + { + $this.AddMessage([MessageData]::new($message, $dataObject)); + } + + [void] AddMessage([PSObject] $dataObject) + { + $this.AddMessage([MessageData]::new($dataObject)); + } + [void] AddMessage([string] $message) + { + $this.AddMessage([MessageData]::new($message)); + } + + [void] AddError([System.Management.Automation.ErrorRecord] $exception) + { + $this.AddMessage([MessageData]::new($exception, [MessageType]::Error)); + } + + [void] SetStateData([string] $message, [PSObject] $dataObject) + { + $this.StateManagement.CurrentStateData = [StateData]::new($message, $dataObject); + } +} + +class SessionContext +{ + [UserPermissions] $Permissions = [UserPermissions]::new(); + [bool] $IsLatestPSModule +} + +class UserPermissions +{ + [bool] $HasAttestationWritePermissions = $false + [bool] $HasAttestationReadPermissions = $false + [bool] $HasRequiredAccess = $true; +} + +class StateManagement +{ + [StateData] $AttestedStateData; + [StateData] $CurrentStateData; +} + +class Metadata +{ + [string] $Reference = "" +} + +class StateData: MessageDataBase +{ + [string] $Justification = ""; + [string] $AttestedBy ="" + [DateTime] $AttestedDate + [string] $ExpiryDate ="" + StateData() + { + } + + StateData([string] $message, [PSObject] $dataObject) : + Base($message, $dataObject) + { + } +} + +class SVTEventContext: AzSKRootEventArgument +{ + [string] $FeatureName = "" + [Metadata] $Metadata + [string] $PartialScanIdentifier; + [ResourceContext] $ResourceContext; + [ControlItem] $ControlItem; + [ControlResult[]] $ControlResults = @(); + + [bool] IsResource() + { + if($this.ResourceContext) + { + return $true; + } + else + { + return $false; + } + } + + [string] GetUniqueId() + { + $uniqueId = ""; + if($this.IsResource()) + { + $uniqueId = $this.ResourceContext.ResourceId; + } + else + { + $uniqueId = $this.TenantContext.Scope; + } + + # Unique Id validation + if([string]::IsNullOrWhiteSpace($uniqueId)) + { + throw "Error while evaluating Unique Id. The parameter 'ResourceContext.ResourceId' OR 'TenantContext.Scope' is null or empty." + } + + return $uniqueId; + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/SVT/SVTResource.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/SVT/SVTResource.ps1 new file mode 100644 index 000000000..9b949d230 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/SVT/SVTResource.ps1 @@ -0,0 +1,25 @@ +Set-StrictMode -Version Latest + +class SubscriptionMapping +{ + [string] $JsonFileName + [string] $ClassName + [string] $FixClassName = ""; + [string] $FixFileName = ""; +} + +class ResourceTypeMapping: SubscriptionMapping +{ + [string] $ResourceTypeName + [string] $ResourceType +} + +class SVTResource +{ + [string] $ResourceId = ""; + [string] $ResourceGroupName = ""; + [string] $ResourceName = ""; + [string] $Location = ""; + [string] $ResourceType = ""; + hidden [ResourceTypeMapping] $ResourceTypeMapping = $null; +} diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/SubscriptionCore/AzureSecurityCenter.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/SubscriptionCore/AzureSecurityCenter.ps1 new file mode 100644 index 000000000..129f98807 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/SubscriptionCore/AzureSecurityCenter.ps1 @@ -0,0 +1,59 @@ +Set-StrictMode -Version Latest + +class AzureSecurityCenter +{ + [PSObject] $Policies + [PSObject] $Alerts + [PSObject] $Tasks + + hidden static [PSObject] GetASCAlerts([PSObject] $alertObjects) + { + $activeAlerts =@() + + if($null -ne $alertObjects -and ($alertObjects | Measure-Object).Count -gt 0) + { + $alertObjects | ForEach-Object { + $out = "" | Select-Object AlertDisplayName, AlertName, Description, State, ReportedTimeUTC, RemediationSteps + Set-Variable -Name AlertDisplayName -Value $_.properties.alertDisplayName + Set-Variable -Name AlertName -Value $_.properties.alertName + Set-Variable -Name Description -Value $_.properties.description + Set-Variable -Name State -Value $_.properties.state + Set-Variable -Name ReportedTimeUTC -Value $_.properties.reportedTimeUtc + Set-Variable -Name RemediationSteps -Value $_.properties.remediationSteps + + $out.AlertDisplayName = $AlertDisplayName + $out.AlertName = $AlertName + $out.Description = $Description + $out.State = $State + $out.ReportedTimeUTC = $ReportedTimeUTC + $out.RemediationSteps = $RemediationSteps + $activeAlerts += $out + } + } + return $activeAlerts; + } + + hidden static [PSObject] GetASCTasks([PSObject] $taskObjects) + { + $activeTasks =@() + + if($null -ne $taskObjects -and ($taskObjects | Measure-Object).Count -gt 0) + { + $taskObjects | ForEach-Object { + if([Helpers]::CheckMember($_, "Id")){ + $out = "" | Select-Object Name, State, ResourceId, Id + Set-Variable -Name Name -Value $_.properties.securityTaskParameters.name + Set-Variable -Name State -Value $_.properties.state + Set-Variable -Name ResourceId -Value $_.properties.securityTaskParameters.resourceId + + $out.Name = $Name + $out.State = $State + $out.ResourceId = $ResourceId + $out.Id = $_.Id + $activeTasks += $out + } + } + } + return $activeTasks + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/SubscriptionCore/ManagementCertificate.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/SubscriptionCore/ManagementCertificate.ps1 new file mode 100644 index 000000000..a59435a9c --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/SubscriptionCore/ManagementCertificate.ps1 @@ -0,0 +1,39 @@ +Set-StrictMode -Version Latest + +class ManagementCertificate +{ + [string] $CertThumbprint + [string] $SubjectName + [string] $Issuer + [PSObject] $Created + [PSObject] $ExpiryDate + [string] $IsExpired + [PSObject] $Difference + [bool] $Whitelisted + + hidden static [ManagementCertificate[]] ListManagementCertificates([PSObject] $certObjects) + { + [ManagementCertificate[]] $certs = @() + $certObjects | ForEach-Object{ + [ManagementCertificate] $certObject = [ManagementCertificate]::new(); + $b64cert = $_.SubscriptionCertificateData + $certData = [System.Convert]::FromBase64String($b64Cert) + $certX = [System.Security.Cryptography.X509Certificates.X509Certificate2]($certData) + $certObject.ExpiryDate = $certX.NotAfter.ToString("yyyy-MM-dd HH:mm:ss") + $certObject.CertThumbprint = $_.SubscriptionCertificateThumbprint + $certObject.SubjectName = $certX.Subject + $certObject.Issuer = $certX.Issuer + $certObject.Created = $_.Created + $certObject.IsExpired = "False" + $certObject.Difference = New-TimeSpan -Start ([datetime]$certX.NotBefore) -End ([datetime]$certX.NotAfter) + if([System.DateTime]::UtcNow -ge $certX.NotAfter) + { + $certObject.IsExpired = "True" + } + #Has to be moved to new configuration model + $certObject.Whitelisted = $false + $certs += $certObject + } + return $certs; + } +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/Framework/Models/SubscriptionSecurity/SubscriptionRBAC.ps1 b/src/AzSK.AAD/0.9.0/Framework/Models/SubscriptionSecurity/SubscriptionRBAC.ps1 new file mode 100644 index 000000000..1d69a52ef --- /dev/null +++ b/src/AzSK.AAD/0.9.0/Framework/Models/SubscriptionSecurity/SubscriptionRBAC.ps1 @@ -0,0 +1,47 @@ +Set-StrictMode -Version Latest + +# Defines data structure for subscription RBAC json +class SubscriptionRBAC +{ + [string] $ActiveCentralAccountsVersion; + [string] $DeprecatedAccountsVersion; + [ActiveRBACAccount[]] $ValidActiveAccounts = @(); + [RBACAccount[]] $DeprecatedAccounts = @(); +} + +class RBACAccount +{ + #Fields from JSON + [string] $Name = ""; + [string] $Description = ""; + [bool] $Enabled = $false; + [string] $ObjectId = ""; + [string] $ObjectType = ""; + [RBACAccountType] $Type = [RBACAccountType]::Validate; +} + +class ActiveRBACAccount: RBACAccount +{ + [string[]] $Tags = @(); + [string] $RoleDefinitionName = ""; + [string] $Scope = ""; +} +class TelemetryRBAC +{ + [string] $tenantId=""; + [string] $Scope=""; + [string] $DisplayName=""; + [string] $ObjectId=""; + [string] $ObjectType=""; + [string] $RoleAssignmentId=""; + [string] $RoleDefinitionId=""; + [string] $RoleDefinitionName=""; + [bool] $IsPIMEnabled; +} +enum RBACAccountType +{ + # AzSK should not Add/Delete the account + Validate + # The account can be fully managed (Add/Delete) from AzSK + Provision +} \ No newline at end of file diff --git a/src/AzSK.AAD/0.9.0/PSGetModuleInfo.xml b/src/AzSK.AAD/0.9.0/PSGetModuleInfo.xml new file mode 100644 index 000000000..f914efea3 Binary files /dev/null and b/src/AzSK.AAD/0.9.0/PSGetModuleInfo.xml differ diff --git a/src/AzSK.AAD/0.9.0/SVT/SVT.ps1 b/src/AzSK.AAD/0.9.0/SVT/SVT.ps1 new file mode 100644 index 000000000..ff2cfac89 --- /dev/null +++ b/src/AzSK.AAD/0.9.0/SVT/SVT.ps1 @@ -0,0 +1,160 @@ +Set-StrictMode -Version Latest +function Get-AzSKAADSecurityStatusTenant +{ + <# + .SYNOPSIS + This command scans an Azure Active Directory (AAD) for tenant wide security issues and best practices. + .DESCRIPTION + This command scans various artifacts in an AAD tenant for security settings and best practices. It generates a report containing evaluation results and fix recommendations. + Refer AAD module section at https://aka.ms/devopskit/docs for more information. + + .PARAMETER TenantId + (Optional) TenantId of the AAD tenant for which security checks need to be performed. + + + + .NOTES + This command scans various artifacts in an AAD tenant for security settings and best practices. + + .LINK + https://aka.ms/devopskit/docs + + #> + + [OutputType([String])] + Param + ( + [string] + [Parameter(Position = 0, Mandatory = $false, HelpMessage="AAD tenant for which security evaluation has to be performed.")] + [ValidateNotNullOrEmpty()] + [Alias("tid")] + $TenantId, + + [String[]] + [Parameter(Position = 1, Mandatory = $false, HelpMessage="Comma separated list of object types to scan [Application, ServicePrincipal, Group, User, Device, All (default)].")] + [ValidateNotNullOrEmpty()] + [Alias("otp")] + [ValidateSet("All","Application", "Device", "Group", "ServicePrincipal", "User", "None")] + $ObjectTypes = @("All"), + + [int] + [Parameter(Position = 2, Mandatory = $false, HelpMessage="Max # of objects to check. Default is 3 (for preview release).")] + [Alias("mo")] + $MaxObj = 3 + ) + Begin + { + [CommandHelper]::BeginCommand($PSCmdlet.MyInvocation); + [ListenerHelper]::RegisterListeners(); + } + + Process + { + try + { + $resolver = [AADResourceResolver]::new($TenantId, $true); #pass $true to scan tenant + $resolver.SetScanParameters($ObjectTypes, $MaxObj) + + #If user didn't pass the tenantId, we set it after getting the login ctx (in the resolver). + if ([string]::IsNullOrEmpty($TenantId)) + { + $TenantId = $resolver.TenantId + } + + $secStatus = [ServicesSecurityStatus]::new($TenantId, $PSCmdlet.MyInvocation, $resolver); + if ($secStatus) + { + return $secStatus.EvaluateControlStatus(); + } + } + catch + { + [EventBase]::PublishGenericException($_); + } + } + + End + { + [ListenerHelper]::UnregisterListeners(); + } +} + + + +function Get-AzSKAADSecurityStatusUser +{ + <# + .SYNOPSIS + This command scans various user-created or user-owned objects in an Azure Active Directory (AAD) tenant for security issues and best practices. + .DESCRIPTION + This command scans various user-created or user-owned objects in an AAD tenant for security settings and best practices. + It generates a report containing evaluation results and fix recommendations. + Refer AAD module section at https://aka.ms/devopskit/docs for more information. + + .PARAMETER TenantId + (Optional) TenantId of the AAD tenant for which security checks need to be performed. + + + .NOTES + This command scans various user-created or user-owned objects in an AAD tenant for security settings and best practices. + + .LINK + https://aka.ms/devopskit/docs + + #> + [OutputType([String])] + Param + ( + [string] + [Parameter(Position = 0, Mandatory = $false, HelpMessage="AAD tenant for which security evaluation has to be performed.")] + [ValidateNotNullOrEmpty()] + [Alias("tid")] + $TenantId, + + [String[]] + [Parameter(Position = 1, Mandatory = $false, HelpMessage="Comma separated list of object types to scan [Application, ServicePrincipal, Group, User, Device, All (default)].")] + [ValidateNotNullOrEmpty()] + [Alias("otp")] + [ValidateSet("All","Application", "Device", "Group", "ServicePrincipal", "User", "None")] + $ObjectTypes = @("All"), + + [int] + [Parameter(Position = 1, Mandatory = $false, HelpMessage="Max # of objects to check. Default is 3 (for preview release).")] + [Alias("mo")] + $MaxObj = 3 + ) + Begin + { + [CommandHelper]::BeginCommand($PSCmdlet.MyInvocation); + [ListenerHelper]::RegisterListeners(); + } + + Process + { + try + { + $resolver = [AADResourceResolver]::new($TenantId, $false); #pass $false to indicate that the scan is for indiv. user + $resolver.SetScanParameters($ObjectTypes, $MaxObj) + + #If user didn't pass the tenantId, we set it after getting the login ctx (in the resolver). + if ([string]::IsNullOrEmpty($TenantId)) + { + $TenantId = $resolver.TenantId + } + $secStatus = [ServicesSecurityStatus]::new($TenantId, $PSCmdlet.MyInvocation, $resolver); + if ($secStatus) + { + return $secStatus.EvaluateControlStatus(); + } + } + catch + { + [EventBase]::PublishGenericException($_); + } + } + + End + { + [ListenerHelper]::UnregisterListeners(); + } +} \ No newline at end of file