Private/Config.ps1

# CIS Azure Benchmark PS - Configuration
# CIS Microsoft Azure Foundations Benchmark v6.0.0

Set-StrictMode -Version Latest

# Tool version shown in report headers and stamped into checkpoints/history —
# single-sourced from the module manifest so it cannot drift from the released
# version (it sat frozen at "2.0.0" through the 2.1–2.4 releases, which also
# silently disabled the version-mismatch checkpoint invalidation).
$script:CIS_VERSION = try {
    [string](Import-PowerShellDataFile (Join-Path (Split-Path $PSScriptRoot -Parent) 'CISAzureFoundationsBenchmark.psd1')).ModuleVersion
} catch { '0.0.0' }
$script:BENCHMARK_VER = "6.0.0"

# Status constants — avoid $Error which is a PS reserved variable
$script:PASS       = "PASS"
$script:FAIL       = "FAIL"
$script:ERR        = "ERROR"
$script:INFO       = "INFO"
$script:MANUAL     = "MANUAL"
$script:SUPPRESSED = "SUPPRESSED"

# Azure RBAC role GUIDs
$script:ROLE_OWNER = "8e3af657-a8ff-443c-a75c-2fe8c4bcb635"
$script:ROLE_UAA   = "18d7d88d-d35e-4fb5-a5c3-7773c20a72d9"

# Internet source addresses for NSG inbound rule evaluation.
# Azure uses "Internet" (service tag) and "<internet>" (legacy portal form) in addition
# to wildcard/CIDR representations. All are treated as equivalent in check logic.
$script:INTERNET_SRCS = @("*", "0.0.0.0", "internet", "any", "0.0.0.0/0", "<internet>")

# Subnets exempt from NSG requirement (Azure platform subnets).
# Azure manages security for these platform-reserved subnets — attaching an NSG
# is either unsupported or overridden by the platform. Checked case-insensitively.
$script:EXEMPT_SUBNETS = @(
    "gatewaysubnet",
    "azurebastionsubnet",
    "azurefirewallsubnet",
    "azurefirewallmanagementsubnet",
    "routeserversubnet"
)

# CLI timeouts (seconds). Graph and activity_log use longer values because those
# calls aggregate data across many resources and can span multiple API pages.
$script:TIMEOUTS = @{
    default      = 60
    storage_list = 90
    storage_svc  = 45
    activity_log = 75
    graph        = 180
}

# Checkpoint directory (relative to working directory)
$script:CHECKPOINT_DIR = "cis_checkpoints"

# Module-level flags (set by entry point)
$script:DEBUG_MODE   = $false
$script:VERBOSE_MODE = $false
$script:LOG_FILE     = $null

# ── Config file loader ───────────────────────────────────────────────────────

function Read-ConfigFile {
    <#
    .SYNOPSIS
    Loads cis_audit.json (or CIS_AUDIT_CONFIG env var path) and applies overrides.
    Returns a hashtable of values that were set, for merging with CLI params.
    #>

    param([string]$ScriptRoot)

    $configPath = $env:CIS_AUDIT_CONFIG
    if (-not $configPath) {
        $configPath = Join-Path $ScriptRoot "cis_audit.json"
    } elseif (-not [System.IO.Path]::IsPathRooted($configPath)) {
        $configPath = Join-Path $ScriptRoot $configPath
    }

    if (-not (Test-Path $configPath)) { return @{} }

    try {
        $raw = Get-Content $configPath -Raw -ErrorAction Stop
        $cfg = $raw | ConvertFrom-Json -ErrorAction Stop
    } catch {
        Write-Host " [WARN] Failed to parse config file: $configPath — $_" -ForegroundColor Yellow
        return @{}
    }

    Write-Host " ✅ Config: $configPath" -ForegroundColor DarkGray
    $overrides = @{}

    # [audit] section
    if ($cfg.PSObject.Properties['audit']) {
        $a = $cfg.audit
        if ($a.PSObject.Properties['parallel']) {
            $v = $a.parallel -as [int]
            if ($v -and $v -ge 1 -and $v -le 20) { $overrides['Parallel'] = $v }
            else { Write-Host " [WARN] Config audit.parallel must be 1–20 (got $($a.parallel)) — ignored" -ForegroundColor Yellow }
        }
        if ($a.PSObject.Properties['level']) {
            $v = [string]$a.level
            if ($v -in @("1","2","both")) { $overrides['Level'] = $v }
            else { Write-Host " [WARN] Config audit.level must be 1, 2, or both (got $($a.level)) — ignored" -ForegroundColor Yellow }
        }
        if ($a.PSObject.Properties['checkpoint_dir']) {
            $v = [string]$a.checkpoint_dir
            if ($v) { $script:CHECKPOINT_DIR = $v }
        }
        if ($a.PSObject.Properties['suppressions_file']) {
            $v = [string]$a.suppressions_file
            if ($v) { $overrides['SuppressionsFile'] = $v }
        }
        if ($a.PSObject.Properties['exit_code']) {
            if ($a.exit_code -eq $true) { $overrides['ExitCode'] = $true }
        }
        if ($a.PSObject.Properties['no_open']) {
            if ($a.no_open -eq $true) { $overrides['NoOpen'] = $true }
        }
    }

    # [timeouts] section
    if ($cfg.PSObject.Properties['timeouts']) {
        $known = @("default", "storage_list", "storage_svc", "activity_log", "graph")
        foreach ($prop in $cfg.timeouts.PSObject.Properties) {
            if ($prop.Name -notin $known) {
                Write-Host " [WARN] Unknown timeouts key '$($prop.Name)' in config — ignored" -ForegroundColor Yellow
                continue
            }
            $v = $prop.Value -as [int]
            if ($v -and $v -gt 0) { $script:TIMEOUTS[$prop.Name] = $v }
            else { Write-Host " [WARN] timeouts.$($prop.Name) must be a positive integer (got $($prop.Value)) — ignored" -ForegroundColor Yellow }
        }
    }

    return $overrides
}

# Resource Graph Kusto queries (one per resource type)
$script:GRAPH_QUERIES = @{

    nsgs = @"
resources | where type =~ 'microsoft.network/networksecuritygroups'
| project id, name, resourceGroup, subscriptionId, rules = properties.securityRules
"@


    storage = @"
resources | where type =~ 'microsoft.storage/storageaccounts'
| project id, name, resourceGroup, subscriptionId,
    httpsOnly = properties.supportsHttpsTrafficOnly,
    publicAccess = properties.publicNetworkAccess,
    crossTenant = properties.allowCrossTenantReplication,
    blobAnon = properties.allowBlobPublicAccess,
    defaultAction= properties.networkAcls.defaultAction,
    bypass = properties.networkAcls.bypass,
    minTls = properties.minimumTlsVersion,
    keyAccess = properties.allowSharedKeyAccess,
    oauthDefault = properties.defaultToOAuthAuthentication,
    isHns = properties.isHnsEnabled,
    sku = sku.name,
    privateEps = array_length(properties.privateEndpointConnections)
"@


    keyvaults = @"
resources | where type =~ 'microsoft.keyvault/vaults'
| project id, name, resourceGroup, subscriptionId,
    purgeProtection = properties.enablePurgeProtection,
    rbac = properties.enableRbacAuthorization,
    publicAccess = properties.publicNetworkAccess,
    privateEps = array_length(properties.privateEndpointConnections)
"@


    vnets = @"
resources | where type =~ 'microsoft.network/virtualnetworks'
| project id, name, resourceGroup, subscriptionId, location,
    hasDdos = isnotnull(properties.ddosProtectionPlan.id)
"@


    subnets = @"
resources | where type =~ 'microsoft.network/virtualnetworks'
| project subscriptionId,
    vnetName = tostring(name),
    resourceGroup = tostring(resourceGroup),
    subnets = properties.subnets
| mv-expand subnet = subnets
| project subscriptionId, vnetName, resourceGroup,
    subnetName = tostring(subnet.name),
    hasNsg = isnotnull(subnet.properties.networkSecurityGroup.id)
"@


    bastion = @"
resources | where type =~ 'microsoft.network/bastionhosts'
| project id, name, resourceGroup, subscriptionId, sku
"@


    vms = @"
resources | where type =~ 'microsoft.compute/virtualmachines'
| project id, name, subscriptionId
"@


    watchers = @"
resources | where type =~ 'microsoft.network/networkwatchers'
| project id, name, subscriptionId, location, state = properties.provisioningState
"@


    locations = @"
resources | project subscriptionId, location | distinct subscriptionId, location
"@


    roles = @"
authorizationresources
| where type =~ 'microsoft.authorization/roleassignments'
| where properties.roleDefinitionId endswith '8e3af657-a8ff-443c-a75c-2fe8c4bcb635'
    or properties.roleDefinitionId endswith '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9'
| project subscriptionId = tostring(subscriptionId),
    principalId = tostring(properties.principalId),
    principalName = tostring(properties.principalName),
    principalType = tostring(properties.principalType),
    roleDefinitionId = tostring(properties.roleDefinitionId),
    scope = tostring(properties.scope)
"@


    app_gateways = @"
resources | where type =~ 'microsoft.network/applicationgateways'
| project id, name, resourceGroup, subscriptionId,
    enableHttp2 = properties.enableHttp2,
    wafEnabled = properties.webApplicationFirewallConfiguration.enabled,
    wafMode = properties.webApplicationFirewallConfiguration.firewallMode,
    wafReqBody = properties.webApplicationFirewallConfiguration.requestBodyCheck,
    sslMinProto = properties.sslPolicy.minProtocolVersion,
    wafPolicyId = tostring(properties.firewallPolicy.id)
"@


    databricks = @"
resources | where type =~ 'microsoft.databricks/workspaces'
| project id, name, resourceGroup, subscriptionId,
    noPublicIp = properties.parameters.enableNoPublicIp.value,
    publicAccess = properties.publicNetworkAccess,
    vnetId = tostring(properties.parameters.customVirtualNetworkId.value),
    privateEps = array_length(properties.privateEndpointConnections)
"@


    waf_policies = @"
resources | where type =~ 'microsoft.network/applicationgatewaywebapplicationfirewallpolicies'
| project id, name, subscriptionId,
    botMode = properties.policySettings.mode,
    requestBodyInspect = properties.policySettings.requestBodyInspect
"@


    app_services = @"
resources | where type =~ 'microsoft.web/sites'
| project id, name, resourceGroup, subscriptionId, kind
"@

}