Public/Invoke-Infiltration.ps1
|
# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Invoke-Infiltration { <# .SYNOPSIS Performs a comprehensive Entra ID, Azure, Intune, and M365 security assessment. .DESCRIPTION Invoke-Infiltration executes a thorough audit of Microsoft cloud identity and device management configuration. It evaluates Conditional Access policies, authentication methods, privileged identity management, application security, federation configuration, tenant settings, Azure IAM, Intune endpoint management, and M365 service configurations. Emulates: ScubaGear, Maester (EIDSCA), EntraFalcon, Mandiant Azure AD Investigator, M365SAT, ROADtools, AADInternals (defensive), Azure AD Assessment, and others. .PARAMETER Categories Specifies which audit categories to run. Default is 'All'. Valid values: All, ConditionalAccess, AuthenticationMethods, PIM, Applications, Federation, TenantConfig, AzureIAM, Intune, M365Services .PARAMETER TenantId The Azure AD / Entra ID tenant ID. .PARAMETER ClientId The application (client) ID for authentication. .PARAMETER CertificateThumbprint Certificate thumbprint for app-only authentication. .PARAMETER ClientSecret Client secret for app-only authentication. .PARAMETER DeviceCode Use device code flow for interactive authentication. .PARAMETER OutputDirectory Directory for report output. Default: per-user data dir + /PSGuerrilla/Reports (Windows: $env:APPDATA; macOS: ~/Library/Application Support; Linux: $XDG_CONFIG_HOME or ~/.config) .PARAMETER NoReports Skip report generation. .PARAMETER NoDelta Skip delta comparison with previous scan. .PARAMETER Quiet Suppress console output. .PARAMETER ConfigPath Path to PSGuerrilla configuration file. .PARAMETER ConfigFile Path to a guerrilla-config.json mission config generated by the PSGuerrilla website. When provided, resolves credentials from the SecretManagement vault and applies category filtering from the mission config. .EXAMPLE Invoke-Infiltration -TenantId 'contoso.onmicrosoft.com' -ClientId $appId -ClientSecret $secret .EXAMPLE Invoke-Infiltration -TenantId $tenantId -ClientId $appId -DeviceCode -Categories ConditionalAccess, PIM #> [CmdletBinding()] param( [ValidateSet('All', 'ConditionalAccess', 'AuthenticationMethods', 'PIM', 'Applications', 'Federation', 'TenantConfig', 'AzureIAM', 'Intune', 'M365Services')] [string[]]$Categories = @('All'), [string]$TenantId, [string]$ClientId, [string]$CertificateThumbprint, [securestring]$ClientSecret, [switch]$DeviceCode, [string]$OutputDirectory, [switch]$NoReports, [switch]$NoDelta, [switch]$Quiet, [Alias('RuntimeConfig')] [string]$ConfigPath, [Alias('MissionConfig')] [string]$ConfigFile ) # --- Resolve mission config (guerrilla-config.json) --- if ($ConfigFile) { $missionCfg = Read-MissionConfig -Path $ConfigFile $vaultName = $missionCfg.VaultName # Resolve Microsoft Graph credentials from vault $graphRef = $missionCfg.Config.credentials.references.microsoftGraph if ($graphRef) { if ($graphRef.tenantIdVaultKey -and -not $PSBoundParameters.ContainsKey('TenantId')) { try { $TenantId = Get-GuerrillaCredential -VaultKey $graphRef.tenantIdVaultKey -VaultName $vaultName } catch { Write-Warning "Failed to resolve TenantId from vault: $_" } } if ($graphRef.clientIdVaultKey -and -not $PSBoundParameters.ContainsKey('ClientId')) { try { $ClientId = Get-GuerrillaCredential -VaultKey $graphRef.clientIdVaultKey -VaultName $vaultName } catch { Write-Warning "Failed to resolve ClientId from vault: $_" } } if ($graphRef.vaultKey -and -not $PSBoundParameters.ContainsKey('CertificateThumbprint') -and -not $PSBoundParameters.ContainsKey('ClientSecret')) { try { $secretVal = Get-GuerrillaCredential -VaultKey $graphRef.vaultKey -VaultName $vaultName if ($graphRef.authMethod -eq 'certificate') { $CertificateThumbprint = $secretVal } else { $ClientSecret = $secretVal | ConvertTo-SecureString -AsPlainText -Force } } catch { Write-Warning "Failed to resolve Graph auth credential from vault: $_" } } } # Apply categories from mission config (combine entraAzure + m365 + intune) if (-not $PSBoundParameters.ContainsKey('Categories')) { $missionCats = [System.Collections.Generic.List[string]]::new() foreach ($envKey in @('entraAzure', 'm365', 'intune')) { $envCfg = $missionCfg.EnabledEnvironments[$envKey] if ($envCfg -and $envCfg.audit -and $envCfg.audit.categories) { foreach ($entry in $envCfg.audit.categories.GetEnumerator()) { if ($entry.Value -and $entry.Key -notin $missionCats) { $missionCats.Add($entry.Key) } } } } if ($missionCats.Count -gt 0) { $Categories = @($missionCats) } } } $scanId = [guid]::NewGuid().ToString() $scanStart = [datetime]::UtcNow # --- Load config --- $cfgPath = if ($ConfigPath) { $ConfigPath } else { $script:ConfigPath } $config = $null if (Test-Path $cfgPath) { $config = Get-Content -Path $cfgPath -Raw | ConvertFrom-Json -AsHashtable } $outDir = if ($OutputDirectory) { $OutputDirectory } elseif ($config -and $config.output.directory) { $config.output.directory } else { Join-Path (Get-PSGuerrillaDataRoot) 'Reports' } # Merge TenantId / ClientId from config.json if not already set if (-not $TenantId -and $config -and $config.entra.tenantId) { $TenantId = $config.entra.tenantId } if (-not $ClientId -and $config -and $config.entra.clientId) { $ClientId = $config.entra.clientId } # Validate required parameters if (-not $TenantId) { throw 'TenantId is required. Provide -TenantId, -ConfigFile, or set entra.tenantId in config.' } if (-not $ClientId) { throw 'ClientId is required. Provide -ClientId, -ConfigFile, or set entra.clientId in config.' } # --- Operation header --- if (-not $Quiet) { Write-OperationHeader -Operation 'INFILTRATION AUDIT' -Mode 'Entra / Azure / M365' ` -Target $TenantId -DaysBack 0 } # --- Authenticate to Microsoft Graph --- if (-not $Quiet) { Write-ProgressLine -Phase INFILTRATE -Message 'Authenticating to Microsoft Graph' } $authParams = @{ TenantId = $TenantId ClientId = $ClientId } if ($CertificateThumbprint) { $authParams['CertificateThumbprint'] = $CertificateThumbprint } if ($ClientSecret) { $authParams['ClientSecret'] = $ClientSecret } if ($DeviceCode) { $authParams['DeviceCode'] = $true } try { $graphToken = Get-GraphAccessToken @authParams ` -Scopes @('https://graph.microsoft.com/.default') } catch { throw "Failed to authenticate to Microsoft Graph: $_" } if (-not $Quiet) { Write-ProgressLine -Phase INFILTRATE -Message 'Authenticated to Microsoft Graph' } # --- Authenticate to Azure Resource Manager (if Azure IAM checks needed) --- $armToken = $null $categoriesToRun = if ($Categories -contains 'All') { @('ConditionalAccess', 'AuthenticationMethods', 'PIM', 'Applications', 'Federation', 'TenantConfig', 'AzureIAM', 'Intune', 'M365Services') } else { $Categories } if ('AzureIAM' -in $categoriesToRun) { if (-not $Quiet) { Write-ProgressLine -Phase INFILTRATE -Message 'Authenticating to Azure Resource Manager' } try { $armToken = Get-GraphAccessToken @authParams ` -Scopes @('https://management.azure.com/.default') ` -ResourceUrl 'https://management.azure.com' } catch { Write-Warning "ARM authentication failed — Azure IAM checks will be skipped: $_" } } # --- Collect data --- if (-not $Quiet) { Write-ProgressLine -Phase INFILTRATE -Message 'Beginning data collection' } $auditData = Get-InfiltrationData ` -AccessToken $graphToken ` -ArmAccessToken $armToken ` -Categories $Categories ` -Quiet:$Quiet # Report collection errors if ($auditData.Errors.Count -gt 0 -and -not $Quiet) { Write-ProgressLine -Phase INFO -Message "Data collection had $($auditData.Errors.Count) error(s)" foreach ($errKey in $auditData.Errors.Keys) { Write-ProgressLine -Phase INFO -Message " $errKey" -Detail $auditData.Errors[$errKey] } } # --- Run checks --- if (-not $Quiet) { Write-ProgressLine -Phase INFILTRATE -Message 'Evaluating security checks' } $allFindings = [System.Collections.Generic.List[PSCustomObject]]::new() $categoryMap = @{ ConditionalAccess = 'Invoke-EntraCAChecks' AuthenticationMethods = 'Invoke-EntraAuthChecks' PIM = 'Invoke-EntraPIMChecks' Applications = 'Invoke-EntraAppChecks' Federation = 'Invoke-EntraFedChecks' TenantConfig = 'Invoke-EntraTenantChecks' AzureIAM = 'Invoke-AzureIAMChecks' Intune = 'Invoke-IntuneChecks' M365Services = @( 'Invoke-M365ExchangeChecks' 'Invoke-M365SharePointChecks' 'Invoke-M365TeamsChecks' 'Invoke-M365DefenderChecks' 'Invoke-M365AuditChecks' 'Invoke-M365PowerPlatformChecks' ) } foreach ($cat in $categoriesToRun) { $funcNames = $categoryMap[$cat] if (-not $funcNames) { continue } # Handle M365Services which maps to multiple check functions foreach ($funcName in @($funcNames)) { if (-not $Quiet) { Write-ProgressLine -Phase INFILTRATE -Message "Running $funcName" } if (Get-Command $funcName -ErrorAction SilentlyContinue) { try { $findings = & $funcName -AuditData $auditData foreach ($f in $findings) { $allFindings.Add($f) } } catch { Write-Warning "Check category '$funcName' failed: $_" } } else { if (-not $Quiet) { Write-ProgressLine -Phase INFO -Message "$funcName not available (module not loaded)" } } } } # --- Score --- $score = Get-AuditPostureScore -Findings @($allFindings) # --- Summary --- $scanEnd = [datetime]::UtcNow $scanDuration = $scanEnd - $scanStart $result = [PSCustomObject]@{ PSTypeName = 'PSGuerrilla.InfiltrationResult' ScanId = $scanId TenantId = $TenantId ScanStart = $scanStart ScanEnd = $scanEnd Duration = $scanDuration Categories = $categoriesToRun Findings = @($allFindings) Score = $score DataErrors = $auditData.Errors AuditData = $auditData } # --- Console output --- if (-not $Quiet) { Write-InfiltrationReport -Result $result } # --- Reports --- if (-not $NoReports) { if (-not (Test-Path $outDir)) { New-Item -Path $outDir -ItemType Directory -Force | Out-Null } $timestamp = $scanStart.ToString('yyyyMMdd-HHmmss') $tenantLabel = $TenantId -replace '[^a-zA-Z0-9]', '_' # Delta comparison $previousFindings = $null if (-not $NoDelta) { $previousFile = Get-ChildItem -Path $outDir -Filter "infiltration-$tenantLabel-*.json" ` -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($previousFile) { try { $previousData = Get-Content -Path $previousFile.FullName -Raw | ConvertFrom-Json $previousFindings = $previousData.Findings if (-not $Quiet) { Write-ProgressLine -Phase INFILTRATE -Message "Delta comparison with $($previousFile.Name)" } } catch { Write-Verbose "Could not load previous scan for delta: $_" } } } # Export reports $baseName = "infiltration-$tenantLabel-$timestamp" if (-not $Quiet) { Write-ProgressLine -Phase INFILTRATE -Message 'Generating reports' } try { Export-InfiltrationReportHtml -Result $result -OutputPath (Join-Path $outDir "$baseName.html") ` -PreviousFindings $previousFindings } catch { Write-Warning "HTML report generation failed: $_" } try { Export-InfiltrationReportCsv -Result $result -OutputPath (Join-Path $outDir "$baseName.csv") } catch { Write-Warning "CSV report generation failed: $_" } try { Export-InfiltrationReportJson -Result $result -OutputPath (Join-Path $outDir "$baseName.json") } catch { Write-Warning "JSON report generation failed: $_" } if (-not $Quiet) { Write-ProgressLine -Phase INFILTRATE -Message "Reports saved to $outDir" } } # --- Complete --- if (-not $Quiet) { Write-ProgressLine -Phase INFILTRATE -Message "Infiltration audit complete in $([Math]::Round($scanDuration.TotalSeconds, 1))s" } return $result } |