ARGUS.psm1

#Requires -Version 7.2
Set-StrictMode -Version Latest

# ---------------------------------------------------------------------------
# Module constants
# ---------------------------------------------------------------------------
$script:ArgusModuleRoot = $PSScriptRoot

$script:ArgusManifest = Import-PowerShellDataFile -Path (Join-Path $PSScriptRoot 'ARGUS.psd1')

$script:ArgusVersion = & {
    $prerelease = $script:ArgusManifest.PrivateData.PSData.Prerelease
    if ($prerelease) { "$($script:ArgusManifest.ModuleVersion)-$prerelease" } else { "$($script:ArgusManifest.ModuleVersion)" }
}

$script:ArgusSchemaVersion = '1.0'

# Area name -> Id prefix and display name, in report order.
$script:ArgusAreas = [ordered]@{
    EntraID      = @{ Code = 'ENT'; DisplayName = 'Entra ID' }
    Applications = @{ Code = 'APP'; DisplayName = 'Applications' }
    Licensing    = @{ Code = 'LIC'; DisplayName = 'Licensing' }
    GroupsTeams  = @{ Code = 'GRP'; DisplayName = 'Groups & Teams' }
    SharePoint   = @{ Code = 'SPO'; DisplayName = 'SharePoint & OneDrive' }
    Exchange     = @{ Code = 'EXO'; DisplayName = 'Exchange Online' }
    Copilot      = @{ Code = 'COP'; DisplayName = 'Copilot & Agents' }
}

$script:ArgusSeverities = @('Critical', 'High', 'Medium', 'Low', 'Info')
$script:ArgusStatuses = @('Passed', 'Failed', 'Investigate', 'Skipped', 'Error', 'NotRun')

$script:ArgusServices = [ordered]@{
    Graph      = 'Microsoft Graph'
    Exchange   = 'Exchange Online'
    SharePoint = 'SharePoint Online (PnP.PowerShell)'
    Commerce   = 'Microsoft 365 commerce policies (MSCommerce)'
}

# Licence (service plan) friendly names and implications (P2 includes P1).
$script:ArgusLicenses = @{
    AAD_PREMIUM    = @{ DisplayName = 'Entra ID P1'; ImpliedBy = @('AAD_PREMIUM_P2') }
    AAD_PREMIUM_P2 = @{ DisplayName = 'Entra ID P2'; ImpliedBy = @() }
    # Copilot and Agent 365 service plans ("Product names and service plan identifiers for licensing", 2026-10-19).
    M365_COPILOT_APPS = @{ DisplayName = 'Microsoft 365 Copilot'; ImpliedBy = @() }
    AGENT_365         = @{ DisplayName = 'Agent 365'; ImpliedBy = @() }
}

# Free, viral and developer SKUs (skuPartNumber) that are not paid seats: default IgnoredSkus of the Licensing
# checks. Part numbers from "Product names and service plan identifiers for licensing" (Microsoft Learn, 2026-10-19).
# Trial subscriptions are detected separately from /directory/subscriptions (isTrial).
$script:ArgusFreeSkus = @(
    'FLOW_FREE', 'POWER_BI_STANDARD', 'POWER_BI_STANDARD_FACULTY', 'POWER_BI_STANDARD_STUDENT', 'POWER_BI_INDIVIDUAL_USER'
    'TEAMS_EXPLORATORY', 'Microsoft_Teams_Exploratory_Dept', 'TEAMS_FREE', 'TEAMS_COMMERCIAL_TRIAL', 'MS_TEAMS_IW'
    'POWERAPPS_VIRAL', 'POWERAPPS_DEV', 'POWERAPPS_INDIVIDUAL_USER', 'CCIBOTS_PRIVPREV_VIRAL', 'Power_Pages_vTrial_for_Makers'
    'DYN365_CUSTOMER_INSIGHTS_VIRAL', 'Dynamics_365_Customer_Service_Enterprise_viral_trial', 'Dynamics_365_Field_Service_Enterprise_viral_trial'
    'Dynamics_365_Sales_Premium_Viral_Trial', 'Dynamics_365_Guides_vTrial', 'Microsoft_Cloud_for_Sustainability_vTrial', 'FORMS_PRO'
    'WINDOWS_STORE', 'RIGHTSMANAGEMENT_ADHOC', 'STREAM', 'MCOPSTNC', 'MICROSOFT_BUSINESS_CENTER', 'SMB_APPS', 'DEVELOPERPACK', 'DEVELOPERPACK_E5'
)

# Microsoft Graph endpoint (global cloud).
$script:ArgusGraphBaseUri = 'https://graph.microsoft.com'

# Read-only Graph permissions ARGUS always needs, whatever checks are selected:
# tenant name/domains, licence detection (Entra ID P1/P2), concealed report names,
# and the signed-in user's own role assignments (Test-ArgusPrerequisite).
$script:ArgusCorePermissions = @('Organization.Read.All', 'LicenseAssignment.Read.All', 'ReportSettings.Read.All', 'RoleManagement.Read.Directory')

# Roles recommended for the account running ARGUS with delegated (interactive) sign-in.
$script:ArgusRecommendedRoles = @('Global Reader', 'Reports Reader')

# Global exclusion list -> finding columns it is matched against (case-insensitive).
$script:ArgusExclusionColumns = [ordered]@{
    UserPrincipalNames = @('UserPrincipalName', 'UPN')
    ObjectIds          = @('ObjectId', 'Id', 'UserId')
    GroupIds           = @('GroupId')
    SiteUrls           = @('SiteUrl')
    AppIds             = @('AppId')
}

# ---------------------------------------------------------------------------
# Per-session state
# ---------------------------------------------------------------------------
$script:ArgusRegistrationTarget = $null   # List used by Register-ArgusCheck during Import-ArgusCheck
$script:ArgusConnection = $null           # set by Connect-Argus (Phase 4)
$script:ArgusDataCache = @{}              # per-run data set cache (Get-ArgusData)
$script:ArgusDataSetLoaders = @{}         # data set name -> scriptblock (registered below)
$script:ArgusDataSetNotes = @{}           # data set name -> notes recorded while loading (e.g. signInActivity omitted)
$script:ArgusCommercePolicies = $null     # MSCommerce product policies read at Connect-Argus (cleared by Disconnect-Argus)

# ---------------------------------------------------------------------------
# Load functions
# ---------------------------------------------------------------------------
foreach ($folder in 'Private', 'Public') {
    $path = Join-Path -Path $PSScriptRoot -ChildPath $folder
    if (-not (Test-Path -LiteralPath $path)) { continue }
    foreach ($file in Get-ChildItem -LiteralPath $path -Filter '*.ps1' -File | Sort-Object Name) {
        try {
            . $file.FullName
        }
        catch {
            throw "ARGUS: failed to load '$($file.FullName)': $_"
        }
    }
}

# ---------------------------------------------------------------------------
# Register data set loaders
# ---------------------------------------------------------------------------
foreach ($dataSet in Get-ArgusDataSetDefinition) {
    $script:ArgusDataSetLoaders[$dataSet.Name] = $dataSet.Loader
}

# ---------------------------------------------------------------------------
# Register built-in checks
# ---------------------------------------------------------------------------
$script:ArgusBuiltInChecks = @(Import-ArgusCheck -Path (Join-Path $PSScriptRoot 'Checks') -ErrorAction Continue)

Update-TypeData -TypeName 'Argus.CheckInfo' -DefaultDisplayPropertySet 'Id', 'Area', 'Severity', 'Title' -Force

# Exports are controlled by FunctionsToExport in ARGUS.psd1 (explicit list).
# Export-ModuleMember is intentionally not used here: PSScriptAnalyzer 1.25 crashes
# (ProvideCommentHelp/GetExportedFunction) on Export-ModuleMember with a variable argument.