Microsoft-Secure-Score-Assessment-Toolkit.psm1

#
# Microsoft Secure Score Assessment Toolkit
# PowerShell Module - Refactored Architecture
#

# Module base path
$script:ModuleRoot = $PSScriptRoot

# Import all required modules
$CoreModules = @(
    'Core\GraphApiClient.ps1',
    'Core\Models.ps1',
    'Core\Logger.ps1'
)

$ProcessorModules = @(
    'Processors\ComplianceProcessor.ps1',
    'Processors\UrlProcessor.ps1'
)

$ReportModules = @(
    'Reports\HtmlReportGenerator.ps1'
)

# Import all modules - fail fast if any are missing
$AllModules = $CoreModules + $ProcessorModules + $ReportModules
foreach ($module in $AllModules) {
    $modulePath = Join-Path $script:ModuleRoot $module
    if (Test-Path $modulePath) {
        . $modulePath
    }
    else {
        throw "Required module file not found: $modulePath. Please reinstall the module."
    }
}

# Module-level variables
$script:CurrentContext = $null

function Connect-MicrosoftSecureScore {
    <#
    .SYNOPSIS
        Authenticate to Microsoft Graph for Secure Score API access.

    .DESCRIPTION
        Establishes a connection to Microsoft Graph with the required permissions to access
        Microsoft Secure Score data. This function must be run before using Invoke-MicrosoftSecureScore.

    .PARAMETER UseDeviceCode
        Use device code authentication instead of interactive browser authentication.
        Useful for headless environments or remote sessions.

    .EXAMPLE
        Connect-MicrosoftSecureScore
        Connects using interactive browser authentication.

    .EXAMPLE
        Connect-MicrosoftSecureScore -UseDeviceCode
        Connects using device code authentication.

    .NOTES
        Requires SecurityEvents.Read.All permission in Microsoft Graph.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [switch]$UseDeviceCode
    )

    try {
        Write-Host "`n=== Microsoft Secure Score Authentication ===" -ForegroundColor Cyan
        Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Yellow

        # Check if Microsoft.Graph modules are installed
        $requiredModules = @(
            'Microsoft.Graph.Authentication',
            'Microsoft.Graph.Security'
        )

        foreach ($moduleName in $requiredModules) {
            if (-not (Get-Module -ListAvailable -Name $moduleName)) {
                Write-Host "$moduleName module not found. Installing..." -ForegroundColor Yellow
                try {
                    Install-Module -Name $moduleName -Scope CurrentUser -Force -AllowClobber
                    Write-Host "$moduleName module installed successfully." -ForegroundColor Green
                }
                catch {
                    throw "Failed to install $moduleName module: $_"
                }
            }
        }

        # Import required modules
        Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
        Import-Module Microsoft.Graph.Security -ErrorAction Stop

        # Connect using the GraphApiClient
        if ($UseDeviceCode) {
            $script:CurrentContext = Connect-SecureScoreGraph -UseDeviceCode
        }
        else {
            $script:CurrentContext = Connect-SecureScoreGraph
        }

        Write-Host "`nAuthentication successful!" -ForegroundColor Green
        Write-Host "Tenant ID: $($script:CurrentContext.TenantId)" -ForegroundColor Cyan
        Write-Host "Account: $($script:CurrentContext.Account)" -ForegroundColor Cyan
        Write-Host "`nYou can now run: Invoke-MicrosoftSecureScore" -ForegroundColor Yellow
    }
    catch {
        Write-Error "Authentication failed: $_"
        Write-Host "`nTroubleshooting:" -ForegroundColor Yellow
        Write-Host "1. Ensure you have Security Reader or Global Reader role" -ForegroundColor White
        Write-Host "2. Check your internet connection" -ForegroundColor White
        Write-Host "3. Try using -UseDeviceCode parameter for alternative authentication" -ForegroundColor White
        throw
    }
}

function Disconnect-MicrosoftSecureScore {
    <#
    .SYNOPSIS
        Disconnect from Microsoft Graph and clean up session.

    .DESCRIPTION
        Disconnects the current Microsoft Graph session and clears the module's connection context.

    .EXAMPLE
        Disconnect-MicrosoftSecureScore
        Disconnects from Microsoft Graph.
    #>

    [CmdletBinding()]
    param()

    try {
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
        $script:CurrentContext = $null
        Write-Host "Disconnected from Microsoft Graph successfully." -ForegroundColor Green
    }
    catch {
        Write-Warning "Error during disconnect: $_"
        $script:CurrentContext = $null
    }
}

function Invoke-MicrosoftSecureScore {
    <#
    .SYNOPSIS
        Generate Microsoft Secure Score assessment report.

    .DESCRIPTION
        Fetches 411+ security controls from Microsoft Graph Secure Score API and generates
        a comprehensive HTML report with interactive filtering and assessment guidance.

    .PARAMETER TenantName
        Display name for your organization in the report. Defaults to "Your Organization".

    .PARAMETER ApplicableOnly
        Generate report showing only controls applicable to your tenant (typically ~70 controls).
        By default, shows all 411+ available controls.

    .PARAMETER ReportPath
        Path where the HTML report will be saved. Defaults to current directory with timestamp.

    .PARAMETER LogPath
        Path where the log file will be saved. If not specified, logging to file is disabled.

    .PARAMETER CsvPath
        Path where the CSV export will be saved. If not specified, CSV export is skipped.

    .PARAMETER NoOpen
        Do not automatically open the report in the default browser after generation.

    .PARAMETER ExcludeCategories
        Array of category names to exclude from the report.
        Valid categories: Identity, Defender, Exchange, SharePoint, Groups, Teams, Compliance, Intune.

    .EXAMPLE
        Invoke-MicrosoftSecureScore
        Generates a full report with all 411+ controls.

    .EXAMPLE
        Invoke-MicrosoftSecureScore -ApplicableOnly
        Generates a report showing only applicable controls for your tenant.

    .EXAMPLE
        Invoke-MicrosoftSecureScore -TenantName "Contoso Corporation"
        Generates a report with custom organization name.

    .EXAMPLE
        Invoke-MicrosoftSecureScore -ReportPath "C:\Reports\SecureScore.html" -LogPath "C:\Logs\SecureScore.log"
        Generates a report and log at specified locations.

    .EXAMPLE
        Invoke-MicrosoftSecureScore -ExcludeCategories @("Exchange", "SharePoint")
        Generates a report excluding Exchange and SharePoint controls.

    .EXAMPLE
        Invoke-MicrosoftSecureScore -CsvPath "C:\Reports\SecureScore.csv"
        Generates an HTML report and exports results to CSV.

    .EXAMPLE
        Invoke-MicrosoftSecureScore -NoOpen
        Generates a report without opening it in the browser.

    .NOTES
        You must run Connect-MicrosoftSecureScore before using this function.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string]$TenantName = "Your Organization",

        [Parameter(Mandatory = $false)]
        [switch]$ApplicableOnly,

        [Parameter(Mandatory = $false)]
        [string]$ReportPath,

        [Parameter(Mandatory = $false)]
        [string]$LogPath,

        [Parameter(Mandatory = $false)]
        [string]$CsvPath,

        [Parameter(Mandatory = $false)]
        [switch]$NoOpen,

        [Parameter(Mandatory = $false)]
        [ValidateSet("Identity", "Defender", "Exchange", "SharePoint", "Groups", "Teams", "Compliance", "Intune")]
        [string[]]$ExcludeCategories
    )

    try {
        # Validate output paths - ensure parent directories exist
        if ($ReportPath) {
            $reportDir = Split-Path -Path $ReportPath -Parent
            if ($reportDir -and -not (Test-Path $reportDir)) {
                throw "Report output directory does not exist: $reportDir"
            }
        }
        if ($LogPath) {
            $logDir = Split-Path -Path $LogPath -Parent
            if ($logDir -and -not (Test-Path $logDir)) {
                throw "Log output directory does not exist: $logDir"
            }
        }
        if ($CsvPath) {
            $csvDir = Split-Path -Path $CsvPath -Parent
            if ($csvDir -and -not (Test-Path $csvDir)) {
                throw "CSV output directory does not exist: $csvDir"
            }
        }

        # Restore context from Graph if we lost it (e.g., module reload)
        if (-not $script:CurrentContext) {
            $context = Get-MgContext
            if ($context) {
                $script:CurrentContext = @{
                    TenantId = $context.TenantId
                    Account = $context.Account
                }
                Write-Verbose "Restored context from active Graph connection"
            }
        }

        # Check if authenticated
        if (-not (Test-GraphConnection)) {
            throw "Not authenticated to Microsoft Graph. Please run Connect-MicrosoftSecureScore first."
        }

        # Set default report path if not provided
        if (-not $ReportPath) {
            $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
            $ReportPath = Join-Path (Get-Location) "SecureScore-Report-$timestamp.html"
        }

        # Initialize logger
        if ($LogPath) {
            Initialize-Logger -LogPath $LogPath -LogToConsole $true -LogToFile $true
        }
        else {
            Initialize-Logger -LogToConsole $true -LogToFile $false
        }

        Write-LogSection -Title "Microsoft Secure Score Assessment" -Level Info
        Write-Log "Tenant: $TenantName" -Level Info
        Write-Log "Mode: $(if ($ApplicableOnly) { 'Applicable Controls Only' } else { 'All Controls' })" -Level Info
        Write-Log "Report Path: $ReportPath" -Level Info
        if ($LogPath) {
            Write-Log "Log Path: $LogPath" -Level Info
        }
        if ($CsvPath) {
            Write-Log "CSV Export Path: $CsvPath" -Level Info
        }
        if ($ExcludeCategories -and $ExcludeCategories.Count -gt 0) {
            Write-Log "Excluded Categories: $($ExcludeCategories -join ', ')" -Level Info
        }

        # Initialize URL processor with config
        $configPath = Join-Path $script:ModuleRoot "Config\control-mappings.json"
        Initialize-UrlProcessor -ConfigPath $configPath
        Write-Log "URL processor initialized with mappings from config" -Level Success

        # Fetch organization information
        Write-LogSection -Title "Fetching Organization Information" -Level Info
        $orgInfo = Get-OrganizationInfo
        $actualTenantName = if ($TenantName -ne "Your Organization") {
            $TenantName
        }
        elseif ($orgInfo.DisplayName) {
            $orgInfo.DisplayName
        }
        else {
            "Organization"
        }
        Write-Log "Organization Name: $actualTenantName" -Level Success

        # Fetch secure score data
        Write-LogSection -Title "Fetching Secure Score Data" -Level Info
        $scoreData = Get-SecureScoreData
        Write-Log "Current Score: $($scoreData.CurrentScore) / $($scoreData.MaxScore)" -Level Success
        $scorePercentage = if ($scoreData.MaxScore -gt 0) {
            [math]::Round(($scoreData.CurrentScore / $scoreData.MaxScore) * 100, 1)
        } else { 0 }
        Write-Log "Percentage: $scorePercentage%" -Level Success

        # Fetch control profiles
        Write-LogSection -Title "Fetching Control Profiles" -Level Info
        $scoredControlsList = $scoreData.ControlScores.Keys

        $controlParams = @{
            FilterApplicableOnly = $ApplicableOnly
        }
        if ($ApplicableOnly) {
            $controlParams['ScoredControlsList'] = $scoredControlsList
        }

        $controls = Get-SecureScoreControlProfiles @controlParams
        Write-Log "Retrieved $($controls.Count) control profiles" -Level Success

        # Group by category for summary
        $categories = $controls | Group-Object -Property ControlCategory | Sort-Object Count -Descending
        Write-Log "Control Summary by Category:" -Level Info
        foreach ($category in $categories) {
            Write-Log " $($category.Name): $($category.Count) controls" -Level Info
        }

        # Initialize report data
        Write-LogSection -Title "Processing Controls" -Level Info
        $reportData = New-ReportData

        # Update report metadata
        Update-ReportMetadata -ReportData $reportData `
            -TenantId $script:CurrentContext.TenantId `
            -TenantName $actualTenantName `
            -GeneratedBy $script:CurrentContext.Account `
            -GeneratedDate (Get-Date -Format "MMMM dd, yyyy HH:mm:ss") `
            -CurrentScore $scoreData.CurrentScore `
            -MaxScore $scoreData.MaxScore

        # Process each control
        $totalControls = $controls.Count
        $processedCount = 0
        $skippedCount = 0
        $excludedCount = 0

        foreach ($control in $controls) {
            # Check if category should be excluded (before incrementing processed count)
            if ($ExcludeCategories -and $ExcludeCategories.Count -gt 0) {
                if ($control.ControlCategory -in $ExcludeCategories) {
                    $excludedCount++
                    Write-Log "Excluded control from category '$($control.ControlCategory)': $($control.Title)" -Level Info -NoConsole
                    continue
                }
            }

            $processedCount++

            # Log progress (based on non-excluded controls)
            Write-LogProgress -Activity "Processing Secure Score Controls" `
                -Current $processedCount `
                -Total ($totalControls - $excludedCount) `
                -FileLogInterval 50

            # Validate control data
            if (-not (Test-ControlDataValid -Control $control)) {
                $skippedCount++
                Write-Log "Skipped invalid control: $($control.Id)" -Level Warning -NoConsole
                continue
            }

            # Extract control properties
            $controlId = $control.Id
            $title = $control.Title
            $category = $control.ControlCategory
            $maxScore = $control.MaxScore
            $implementationCost = $control.ImplementationCost
            $userImpact = $control.UserImpact
            $threats = $control.Threats -join ", "
            $remediation = ConvertFrom-HtmlString -HtmlText $control.Remediation

            # Optimize action URL
            $actionUrl = Optimize-ControlUrl -Url $control.ActionUrl `
                -ControlName $title `
                -TenantId $script:CurrentContext.TenantId

            # Determine compliance status and risk
            $complianceStatus = Get-ComplianceStatus -ControlId $controlId `
                -ControlScores $scoreData.ControlScores `
                -MaxScore $maxScore

            $riskLevel = Get-RiskLevel -MaxScore $maxScore `
                -UserImpact $userImpact `
                -Threats $threats

            # Build values
            $currentValue = Get-ControlCurrentValue -ControlId $controlId `
                -ControlScores $scoreData.ControlScores `
                -MaxScore $maxScore

            $proposedValue = Get-ControlProposedValue -MaxScore $maxScore `
                -ImplementationCost $implementationCost `
                -UserImpact $userImpact

            $justification = Get-ControlJustification -Threats $threats `
                -Remediation $remediation

            $scoreImpact = Get-ScoreImpact -ControlMaxScore $maxScore `
                -TotalMaxScore $scoreData.MaxScore

            # Add to report
            Add-ReportItem -ReportData $reportData `
                -Category $category `
                -SettingName $title `
                -CurrentValue $currentValue `
                -ProposedValue $proposedValue `
                -Justification $justification `
                -Risk $riskLevel `
                -Status $complianceStatus `
                -SecureScoreImpact $scoreImpact `
                -Reference $actionUrl `
                -ActionUrl $actionUrl
        }

        # Clear progress bar
        Write-Progress -Activity "Processing Secure Score Controls" -Completed

        Write-Log -Level Info
        Write-Log "Processed $processedCount controls" -Level Success
        if ($excludedCount -gt 0) {
            Write-Log "Excluded $excludedCount controls based on category filter" -Level Info
        }
        if ($skippedCount -gt 0) {
            Write-Log "Skipped $skippedCount invalid controls" -Level Warning
        }
        Write-Log "Collected $($reportData.ProposedChanges.Count) configuration items" -Level Success

        # Export to CSV if requested
        if ($CsvPath) {
            Write-LogSection -Title "Exporting CSV Report" -Level Info
            try {
                $csvData = $reportData.ProposedChanges | ForEach-Object {
                    [PSCustomObject]@{
                        Category          = $_.Category
                        SettingName       = $_.SettingName
                        Status            = $_.Status
                        Risk              = $_.Risk
                        CurrentValue      = $_.CurrentValue
                        ProposedValue     = $_.ProposedValue
                        Justification     = $_.Justification
                        SecureScoreImpact = $_.SecureScoreImpact
                        ActionUrl         = $_.ActionUrl
                    }
                }
                $csvData | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8 -Force
                Write-Log "CSV exported successfully: $CsvPath" -Level Success
            }
            catch {
                Write-Log "Failed to export CSV: $_" -Level Error
                Write-Warning "CSV export failed: $_"
            }
        }

        # Generate HTML report
        Write-LogSection -Title "Generating HTML Report" -Level Info
        $templatePath = Join-Path $script:ModuleRoot "Templates"

        $reportParams = @{
            ReportData   = $reportData
            TemplatePath = $templatePath
            OutputPath   = $ReportPath
        }

        $generatedReport = New-HtmlReport @reportParams
        Write-Log "Report generated successfully!" -Level Success
        Write-Log "Report location: $generatedReport" -Level Info

        # Close logger
        Close-Logger

        # Open report in browser unless suppressed
        if (-not $NoOpen) {
            Write-Host "`nOpening report in default browser..." -ForegroundColor Cyan
            Start-Process $generatedReport
        }

        Write-Host "`n=== Assessment Complete ===" -ForegroundColor Green
        Write-Host "Report: $generatedReport" -ForegroundColor Cyan
        if ($CsvPath) {
            Write-Host "CSV: $CsvPath" -ForegroundColor Cyan
        }
        if ($LogPath) {
            Write-Host "Log: $LogPath" -ForegroundColor Cyan
        }

        return $generatedReport
    }
    catch {
        Write-Error "Failed to generate secure score assessment: $_"
        Write-Log "Error: $_" -Level Error
        Close-Logger
        throw
    }
}

function Get-MicrosoftSecureScoreInfo {
    <#
    .SYNOPSIS
        Display information about the Microsoft Secure Score Assessment Toolkit.

    .DESCRIPTION
        Shows version information, usage instructions, and helpful links for the toolkit.

    .EXAMPLE
        Get-MicrosoftSecureScoreInfo
        Displays toolkit information and usage guide.
    #>

    [CmdletBinding()]
    param()

    # Read version from module manifest
    $manifestPath = Join-Path $script:ModuleRoot "Microsoft-Secure-Score-Assessment-Toolkit.psd1"
    $version = "Unknown"
    if (Test-Path $manifestPath) {
        try {
            $manifestData = Import-PowerShellDataFile -Path $manifestPath
            $version = $manifestData.ModuleVersion
        }
        catch {
            $version = "Unknown"
        }
    }

    Write-Host "`n=====================================================================" -ForegroundColor Cyan
    Write-Host " Microsoft Secure Score Assessment Toolkit v$version" -ForegroundColor Cyan
    Write-Host " Modular Architecture - Enterprise Security Assessment" -ForegroundColor Cyan
    Write-Host "=====================================================================" -ForegroundColor Cyan

    Write-Host "`nDESCRIPTION:" -ForegroundColor Yellow
    Write-Host " Generate comprehensive security reports with over 400 Microsoft" -ForegroundColor White
    Write-Host " Secure Score controls fetched directly from Microsoft Graph API." -ForegroundColor White

    Write-Host "`nQUICK START:" -ForegroundColor Yellow
    Write-Host " 1. Connect-MicrosoftSecureScore" -ForegroundColor Green
    Write-Host " Authenticate to Microsoft Graph" -ForegroundColor Gray
    Write-Host ""
    Write-Host " 2. Invoke-MicrosoftSecureScore" -ForegroundColor Green
    Write-Host " Generate full assessment report with 411+ controls" -ForegroundColor Gray
    Write-Host ""
    Write-Host " 3. Disconnect-MicrosoftSecureScore" -ForegroundColor Green
    Write-Host " Clean up Microsoft Graph session" -ForegroundColor Gray

    Write-Host "`nCOMMON EXAMPLES:" -ForegroundColor Yellow
    Write-Host " # Full report with all controls" -ForegroundColor Gray
    Write-Host " Invoke-MicrosoftSecureScore" -ForegroundColor White
    Write-Host ""
    Write-Host " # Only applicable controls with logging" -ForegroundColor Gray
    Write-Host " Invoke-MicrosoftSecureScore -ApplicableOnly -LogPath 'C:\Logs\assessment.log'" -ForegroundColor White
    Write-Host ""
    Write-Host " # Export to CSV for analysis" -ForegroundColor Gray
    Write-Host " Invoke-MicrosoftSecureScore -CsvPath 'C:\Reports\SecureScore.csv'" -ForegroundColor White
    Write-Host ""
    Write-Host " # Exclude categories and suppress browser" -ForegroundColor Gray
    Write-Host " Invoke-MicrosoftSecureScore -ExcludeCategories @('Exchange','SharePoint') -NoOpen" -ForegroundColor White

    Write-Host "`nFEATURES:" -ForegroundColor Yellow
    Write-Host " - Modular architecture with separated concerns" -ForegroundColor Green
    Write-Host " - Category filtering with ExcludeCategories parameter" -ForegroundColor Green
    Write-Host " - CSV export for spreadsheet analysis" -ForegroundColor Green
    Write-Host " - File-based logging support" -ForegroundColor Green
    Write-Host " - Externalized configuration (JSON)" -ForegroundColor Green
    Write-Host " - Template-based HTML generation" -ForegroundColor Green

    Write-Host "`nREQUIREMENTS:" -ForegroundColor Yellow
    Write-Host " - Microsoft Graph PowerShell SDK" -ForegroundColor White
    Write-Host " - SecurityEvents.Read.All permission" -ForegroundColor White
    Write-Host " - Security Reader or Global Reader role" -ForegroundColor White

    Write-Host "`nLINKS:" -ForegroundColor Yellow
    Write-Host " GitHub: https://github.com/mohammedsiddiqui6872/Microsoft-Secure-Score-Assessment-Toolkit" -ForegroundColor Cyan
    Write-Host " Issues: https://github.com/mohammedsiddiqui6872/Microsoft-Secure-Score-Assessment-Toolkit/issues" -ForegroundColor Cyan

    Write-Host "`nSUPPORT:" -ForegroundColor Yellow
    Write-Host " Buy Me a Coffee: https://buymeacoffee.com/mohammedsiddiqui" -ForegroundColor Magenta
    Write-Host ""
}

# Export only public functions - internal functions remain private
Export-ModuleMember -Function @(
    'Connect-MicrosoftSecureScore',
    'Disconnect-MicrosoftSecureScore',
    'Invoke-MicrosoftSecureScore',
    'Get-MicrosoftSecureScoreInfo'
)