posture-overview.tcl

Security and Privacy Posture Overview

Location:
/packages/acs-admin/www/posture-overview.tcl
Author:
Gustaf Neumann
Created:
2024-07-20

Related Files

[ hide source ] | [ make this the default ]

File Contents

ad_page_contract {
    Security and Privacy Posture Overview

    @author Gustaf Neumann
    @creation-date 2024-07-20
} {
    {current_location ""}
}

set doc(title) "Security and Privacy Posture Overview"
set context [list $doc(title)]

set ns_info_config [ns_info config]
set system_locale [lang::system::locale]
set installed_locales [lang::system::get_locales]

set packages [apm_enabled_packages]
set number_of_packages [llength $packages]
set version_numbers_on_result_pages [ns_config ns/server/[ns_info server] noticedetail]

if {$current_location eq ""} {
    set current_location [ns_conn location]
}
set behind_reverse_proxy_p [ad_conn behind_proxy_p]
set behind_secure_reverse_proxy_p [ad_conn behind_secure_proxy_p]

set secure_connection_p [expr {$behind_secure_reverse_proxy_p || (!$behind_reverse_proxy_p && [ns_conn proto] eq "https")}]
set reverse_proxy_setup [ad_decode $behind_reverse_proxy_p-$behind_secure_reverse_proxy_p \
                             0-0 "This server is not configured to be running behind a reverse proxy server" \
                             1-0 "This server is configured to be running behind a reverse proxy server" \
                             1-1 "This server is running behind a reverse proxy server over HTTPs" \
                            ]


set custom_error_pages_set [ns_configsection ns/server/[ns_info server]/redirects]
set custom_error_pages [expr {$custom_error_pages_set ne ""
                              ? [ns_set array $custom_error_pages_set]
                              : "It is recommended to define custom error pages for the most common HTTP errors"}]

if {[ns_info version] < 5.0} {
    set custom_server_reply_pages [string cat "You running a NaviServer < 5.0. " \
                                    "With new versions, one can define custom pages for all server replies " \
                                    "by configuring the parameter 'noticeadp' in the server configuration file."]
} else {
    set custom_server_reply_adp [ns_config ns/server/[ns_info server] noticeadp]
    if {$custom_server_reply_adp eq ""} {
        set custom_server_reply_pages "'ns/server/[ns_info server] noticeadp' is not configured"
    } else {
        if {![string match "/*" $custom_server_reply_adp]} {
            set custom_server_reply_adp [ns_info home]/conf/$custom_server_reply_adp
        }
        set custom_server_reply_pages "Using ADP file <i>$custom_server_reply_adp</i>"
    }
    # https://naviserver.sourceforge.io/5.0/manual/files/admin-config.html
}


set numSiteNodesEntries [::acs::dc list countSiteNodes {select count(*) from site_nodes}]
set dbPostgresql_p [string equal [::acs::dc cget -backend] postgresql]
set sitenodeBoundaries {
    500000 huge
    5000 large
    0 small
}
if {$dbPostgresql_p} {
    foreach {sitenodeBoundary model} [lsort -stride 2 -decreasing $sitenodeBoundaries] {
        if  {$numSiteNodesEntries > $sitenodeBoundary} {
            set sitenodeModel $model
            break
        }
    }

    if {$sitenodeModel eq "small"} {
        set numPublicReadableSiteNodes [xo::dc list countPublicSiteNodes {
            select count(orig_object_id) from acs_permission.permission_p_recursive_array(array(
                   select s.object_id from apm_packages ap, site_nodes s where s.object_id = ap.package_id
            ), -1, 'read');
        }]
    }
    set checkPublicURL [export_vars -base widely-accessible-packages {
        numSiteNodesEntries numPublicReadableSiteNodes sitenodeModel
    }]
}

#
# Collect information about certain security relevant package parameters parameters
#
set parameter_info {}
if {$secure_connection_p} {
    lappend parameter_info \
        SecureSessionCookie $::acs::kernel_id
}
lappend parameter_info \
    CSPEnabledP             $::acs::kernel_id \
    ShowMembersListTo       [ad_conn subsite_id] \
    UseHtmlAreaForRichtextP [apm_package_id_from_key acs-templating] \
    RichTextEditor          [apm_package_id_from_key acs-templating]

template::multirow create parameter_check \
    parameter_name description package value link diagnosis

foreach {parameter_name package_id} $parameter_info  {
    set value [parameter::get -package_id $package_id -parameter $parameter_name]
    set package_key [apm_package_key_from_id $package_id]
    set description [::acs::dc list get {
        select description from apm_parameters
        where package_key = :package_key
        and parameter_name = :parameter_name
    }]
    switch $parameter_name {
        SecureSessionCookie {
            set diagnosis [ad_decode $value 0 "Allow sessions over HTTP and HTTPS" 1 "Allow sessions only over HTTPS"]
        }
        CSPEnabledP {
            set intro "Context Security Policies (CSP) are"
            set diagnosis [ad_decode $value 0 "$intro not enabled" 1 "$intro enabled"]
        }
        ShowMembersListTo {
            set intro "Show member list to"
            set diagnosis [ad_decode $value \
                               0 "$intro everyone" \
                               1 "$intro members" \
                               2 "$intro administrators only" \
                               3 "$intro members, unless on subsite" \
                              ]
        }
        UseHtmlAreaForRichtextP {
            set diagnosis [ad_decode $value 0 "Rich text editors deactivated" 1 "Richtext editors activated"]
        }
        RichTextEditor {
            set diagnosis "Use this rich text editor when rich text editors are enabled"
        }
        default {
            set diagnosis ""
        }
    }
    template::multirow append parameter_check \
        $parameter_name \
        [lindex $description 0] \
        $package_key \
        $value \
        [string cat \
             /shared/parameters?package_id=$package_id \
             &return_url=/acs-admin/system-overview\
             &scroll_to=$parameter_name] \
        $diagnosis
}


set host_header [security::validated_host_header]
set public_ip_addr_p "?"
if {$host_header ne ""} {
    set host [dict get [ns_parsehostport $host_header] host]
    set current_ip_addr [ns_addrbyhost $host]
    if {[::acs::icanuse "ns_ip"]} {
        set public_ip_addr_p [ns_ip public $current_ip_addr]
    }
}
set public_ip_addr_p_label [ad_decode $public_ip_addr_p 0 no 1 yes $public_ip_addr_p]

#
# If we have a public IP address and we are on a TLS connection, can
# offer a check vs. ssllabs.
#
if {$public_ip_addr_p == 1 && $secure_connection_p} {
    #
    # Do not force a public listing of the results
    #
    set ssllabs_url https://www.ssllabs.com/ssltest/analyze.html?viaform=on&d=https://$host_header&hideResults=on
}

template::multirow create link_check \
    type url status package_id permission_info diagnosis

foreach {type url} [subst {
    internal /acs-service-contract/
    internal /api-doc/
    internal /doc/
    internal /ds/
    internal /request-monitor/
    internal /shared/
    internal /shared/parameters
    internal /test/
    internal /xotcl/
    internal /xotcl/version-numbers
    personal /members/
    personal /shared/community-member?user_id=[ad_conn user_id]
    personal /shared/portrait?user_id=[ad_conn user_id]
    personal /shared/whos-online
}] {

    set posture [::acs_admin::posture_status \
                     -current_location $current_location \
                     -url $url]
    dict with posture {
        template::multirow append link_check \
            $type \
            $url \
            $status \
            $package_id \
            [expr {$status == 404 ? "" : "$direct_permissions [llength $parties] parties"} ] \
            $diagnosis
    }

}

template::multirow create machine_readable url status diagnosis detailURL detailLabel

foreach url {
    /robots.txt
    /security.txt
} {
    try {
        ns_http run -timeout 300ms $current_location$url
    } on ok {result} {
        set status [dict get $result status]
        set diagnosis ""
        set detailURL ""
        set detailLabel ""
        switch $status {
            200 {set diagnosis "publicly accessible"}
            404 {
                set diagnosis "not provided"
                switch $url {
                    /robots.txt   {
                        set detailLabel "RFC 9309"
                        set detailURL https://datatracker.ietf.org/doc/html/rfc9309
                    }
                    /security.txt {
                        set detailLabel "RFC 9116"
                        set detailURL https://www.rfc-editor.org/rfc/rfc9116
                    }
                }
            }
        }
        #append diagnosis " $node_id $package_id ($parties) // [llength $parties] // $direct_permissions"
        #append report "status $status $diagnose\n<br>"
    } on error {errorMsg} {
        set diagnosis $errorMsg
        set status 0
    }

    template::multirow append machine_readable $url $status $diagnosis $detailURL $detailLabel
}


template::multirow create hdr_check \
    field value
foreach url $current_location {
    set result [ns_http run $url]
    set hdrs [dict get $result headers]
    #set location [ns_set iget [dict get $result headers] location]
    ns_log notice "HDRS [ns_set array $hdrs]"
    foreach field {
        Content-Security-Policy
        Referrer-Policy
        Strict-Transport-Security
        X-Forwarded-For
        X-Content-Type-Options
        X-Frame-Options
        X-XSS-Protection
        X-SSL-Request
    } {
        template::multirow append hdr_check $field [ns_set iget $hdrs $field]
    }
}

template::multirow create library_check \
    library swa_link version_color \
    configured_version vulnerability vulnerabilityCheckURL \
    installed_locally available diagnosis

foreach proc_name [::util::resources::resource_info_procs] {
    set resource_info [::$proc_name]
    set libraryName [dict get $resource_info resourceName]

    if {[dict exists $resource_info configuredVersion]} {
        set configuredVersion [dict get $resource_info configuredVersion]
    } else {
        set configuredVersion ?
    }

    if {$configuredVersion ne "?"} {
        set is_installed [::util::resources::is_installed_locally \
                              -resource_info $resource_info]
    } else {
        set version_segment ?
        set is_installed ?
    }
    #
    # Get the package_key from the resourceDir form resource_info to provide
    # a link to the swa pages of the package.
    #
    set resourceDir [dict get $resource_info resourceDir]
    if {[regexp {/packages/([^/]+)/} $resourceDir . package_key]
        && [file exists [acs_package_root_dir $package_key]/www/sitewide-admin/]
    } {
        set swa_link /acs-admin/package/$package_key/
    } else {
        set swa_link ""
    }

    set reported 0
    set version_color "body"
    set availableVersion ""

    if {[dict exists $resource_info versionCheckAPI] && [dict exists $resource_info configuredVersion]} {
        set availableVersion [::util::resources::cdnjs_get_newest_version -resource_info $resource_info]
        if {$availableVersion ne "unknown"} {
            ns_log notice ... configured version $configuredVersion available version $availableVersion
            set new_version_available [expr {$configuredVersion == $availableVersion
                                             ? 0
                                             : [apm_version_names_compare $configuredVersion $availableVersion] < 0}]
            if {$new_version_available} {
                ns_log notice "NEW VERSION for $libraryName"
                switch $libraryName {
                    "CKEditor 4" {set diagnosis "Versions after 4.22.1 require a license"}
                    default      {set diagnosis "Newer version upstream available"}
                }
                set version_color warning
            } else {
                set diagnosis "Up-to-date"
                set version_color success
            }
            set reported 1
        } else {
            set availableVersion ""
            set diagnosis "Could not determine upstream version number"
            set reported 1
        }
    }
    if {!$reported} {
        set version_color "body"
        set diagnosis "Could not determine upstream version number (no version check API available)"
    }

    set vulnerability ""
    set vulnerabilityCheckVersionURL ""
    if {[dict exists $resource_info vulnerabilityCheck]} {
        set vulnerabilityCheck [dict get $resource_info vulnerabilityCheck]
        dict with vulnerabilityCheck {
            set result [::util::resources::check_vulnerability \
                            -service $service \
                            -library $library \
                            -version $configuredVersion]
            if {[dict get $result hasVulnerability] ne "?"} {
                set vulnerabilityCheckResult [dict get $result hasVulnerability]
                if {$vulnerabilityCheckResult ne "?"} {
                    set vulnerability $vulnerabilityCheckResult
                    set vulnerabilityCheckVersionURL [dict get $result versionURL]
                }
            }
        }
    }

    template::multirow append library_check \
        $libraryName $swa_link $version_color $configuredVersion \
        $vulnerability $vulnerabilityCheckVersionURL \
        $is_installed \
        $availableVersion \
        $diagnosis
}


if {0} {
    Report any mounted instance of file-storage that is publicly accessible.
}


# Local variables:
#    mode: tcl
#    tcl-indent-level: 4
#    indent-tabs-mode: nil
# End: