diff --git a/check_gitlab_tokens b/check_gitlab_tokens new file mode 100755 index 0000000000000000000000000000000000000000..c3ad8495a6ae11fcde45308a89d873923f17bf11 --- /dev/null +++ b/check_gitlab_tokens @@ -0,0 +1,294 @@ +#!/bin/bash +# ====================================================================== +# +# Check Gitlab tokens of groups and projects. +# It warns if tokens expire soon. +# +# requirements: +# - inc/rest-api-client.sh +# - curl +# +# ---------------------------------------------------------------------- +# 2024-10-29 v1.0 <axel.hahn@iml.unibe.ch> +# ====================================================================== + +cd "$( dirname "$0" )" || exit +. "$( dirname $0 )/inc_pluginfunctions" || exit 1 + +export self_APPVERSION=1.0 + + +GITLAB_API='https://gitlab.example.com/api/v4' +GITLAB_TOKEN='glpat-12345678' + +GITLAB_CONFIG=/etc/icinga2/gitlab.cfg +REST_CLIENT="$( dirname $0 )/../inc/rest-api-client.sh" + +projectUrls= + +OUT_TOKENS=/tmp/gitlab-tokens__$USER.json +OUT_USERS=/tmp/gitlab-users__$USER.json + +typeset -i iSince=395 +typeset -i iTokenCount +typeset -i iTokensFound +NL=" +" + +typeset -i iWarnLimit=30 +typeset -i iCriticalLimit=10 + +typeset -i iCountWarn=0 +typeset -i iCountCritical=0 + +output="" + +# ---------------------------------------------------------------------- +# functions +# ---------------------------------------------------------------------- + +# Show help +function showHelp(){ + local _self; _self=$(basename $0) +cat <<EOF +$( ph.showImlHelpHeader ) + +Check gitlab tokens and warn if tokens expire soon. + +This check fetches the gitlbab tokens created in the last $iSince days +from the Gitlab API. It skips + + - personal access tokens of users + - revoked tokens + +The script can run several seconds depending on count of tokens, projects +and users. Maybe you want to call it with a longer interval. + +SYNTAX: +$_self [OPTIONS] + +OPTIONS: + + -h or --help show this help. + + -w VALUE warning level (default: $iWarnLimit) + -c VALUE critical level (default: $iCriticalLimit) + + -g FILE path to GITLAB_CONFIG; default: $GITLAB_CONFIG + -r FILE path to REST_CLIENT; default: $REST_CLIENT + + -s DAYS Number of days for max age of token; default: $iSince + +PARAMETERS: + + None. + +EXAMPLES: + + $_self -w 28 -c 7 + Set other warning and critical level + + $_self -g ./gitlab.cfg + Set a custom gitlab config file + + $_self -r /opt/bash-api-client/bash-api-client.sh + Set a custom gitlab config file + +EOF +} + +# Fetch data from gitlab api with multiple page requests +# +# param string url +# param string output file +# param int optional: number of items per page; default: 100 +function _getPagesToFile(){ + local url="$1" + local outfile="$2" + local iPerPage=${3:-100} + local page=0 + + test -f "${outfile}" && rm "${outfile}" + + grep -q "?" <<< "$url" || url="${url}?" + while true; do + (( page++ )) + + pageUrl="$url&per_page=${iPerPage}&page=${page}" + # echo "Request: $pageUrl" + + http.makeRequest "$pageUrl" + if ! http.isOk > /dev/null; then + echo "ERROR: Request failed: $pageUrl" + http.getResponseHeader + http.getResponse + ph.abort + fi + + # if response is "[]" then we are done + if ! http.getResponse | grep -q "^\[\]$"; then + http.getResponse >> "${outfile}" + else + break + fi + + done +} + +# Get web link of a project to see its tokens +# +# global string projectUrls string to cache project name + link +# +# param string username eg. projects_14_bot_nnnnnn +# return string +function getWeblink(){ + local myusername="$1" + + sType=$( cut -f 1 -d "_" <<< "$myusername" )s + # sType is one of users|projects + + sId2=$( cut -f 2 -d "_" <<< "$myusername" ) + local data + local myurl + if ! grep -q "^/$sType/$sId2:" <<< "$projectUrls" ; then + http.makeRequest "/$sType/$sId2" + if ! http.isOk > /dev/null; then + echo "ERROR: Request /$sType/$sId2 failed: $pageUrl" + http.getResponseHeader + http.getResponse + ph.abort + fi + data="$( http.getResponse )" + myname=$( getKey "$data" "name" ) + myurl=$( getKey "$data" "web_url" ) + if [ -n "$myurl" ]; then + projectUrls+="/$sType/$sId2:$myname <$myurl/-/settings/access_tokens>$NL" + fi + fi + grep "^/$sType/$sId2:" <<< "$projectUrls" | cut -f 2- -d ":" +} + +# Get a single value from json +# param string json data +# param string key +# return string +function getKey(){ + jq -r ".$2" <<< "$1" | grep -v "null" +} + +# ---------------------------------------------------------------------- +# MAIN +# ---------------------------------------------------------------------- + +# --- check param -h +case "$1" in + "--help"|"-h") + showHelp + exit 0 + ;; + *) +esac + +REST_CLIENT=$( ph.getValueWithParam $REST_CLIENT r "$@") +GITLAB_CONFIG=$( ph.getValueWithParam $GITLAB_CONFIG g "$@") + +# --- check requirements +ph.require curl +. "${GITLAB_CONFIG}" || exit 1 +. "${REST_CLIENT}" || exit 1 +http.help >/dev/null || exit 1 + + +iWarnLimit=$( ph.getValueWithParam $iWarnLimit w "$@") +iCriticalLimit=$( ph.getValueWithParam $iCriticalLimit c "$@") +iSince=$( ph.getValueWithParam $iSince s "$@") + +http.init +http.addHeader "PRIVATE-TOKEN: $GITLAB_TOKEN" +http.setBaseUrl "$GITLAB_API" + +startdate="$( date +%Y-%m-%dT00:00:00Z --date "$iSince days ago")" +sDateWarn="$( date +%Y%m%d --date "${iWarnLimit} days" )" +sDateCritical="$( date +%Y%m%d --date "${iCriticalLimit} days" )" + +# get all tokens +# see https://docs.gitlab.com/ee/api/personal_access_tokens.html +_getPagesToFile "/personal_access_tokens/?revoked=false&created_after=${startdate}" "$OUT_TOKENS" + +# get all users +# see https://docs.gitlab.com/ee/api/users.html +_getPagesToFile "/users?exclude_humans=true&exclude_external=true&exclude_internal=true&created_after=${startdate}&search=_bot_" "$OUT_USERS" + +# IDs / Einträge zählen: +iTokenCount=$( jq ".[].id " < "$OUT_TOKENS" | wc -l ) + +iTokensFound=0 +for i in $( seq 1 $iTokenCount ) +do + + # get nth token + entry="$( jq ".[$i]" < "$OUT_TOKENS")" + + # hide non active tokens + if [ "$( getKey "$entry" "active" )" = "false" ]; then + continue + fi + + # hide tokens without name + sName=$( getKey "$entry" "name" ) + if [ -z "$sName" ]; then + continue + fi + + # hide tokens referencing a username that doesn't contain "_[number]_bot_" + sUserid=$( getKey "$entry" "user_id" ) + myusername="$( jq ".[] | select(.id == $sUserid)" < "$OUT_USERS" | jq ".username" | cut -f 1-3 -d "_" | tr -d '"') " + if ! grep -q "_[0-9]*_bot" <<< "$myusername" ; then + continue + fi + + iTokensFound+=1 + + # check expiration + sExpire=$( getKey "$entry" "expires_at" ) + + # remove "-" from date to get an integer + sExpire2=${sExpire//\-} + + + sStatus="OK " + if [ "$sExpire2" -le "$sDateWarn" ]; then + if [ "$sExpire2" -le "$sDateCritical" ]; then + iCountCritical+=1 + sStatus="Critical" + else + iCountWarn+=1 + sStatus="Warning " + fi + fi + + myproject="$( getWeblink "$myusername" )" + + output+="$sExpire $sStatus $sName - $myproject${NL}" + +done + +if [ $iCountCritical -gt 0 ]; then + ph.setStatus "critical" +elif [ $iCountWarn -gt 0 ]; then + ph.setStatus "warning" +else + ph.setStatus "ok" +fi + +ph.status "$iTokensFound Gitlab Tokens (max $iSince days old) .. critical: $iCountCritical ($iCriticalLimit days) .. warnings: $iCountWarn ($iWarnLimit days)" +echo + +echo "$output" + +# cleanup +rm -f "$OUT_TOKENS" "$OUT_USERS" + +ph.exit + +# ---------------------------------------------------------------------- diff --git a/docs/20_Checks/_index.md b/docs/20_Checks/_index.md index f8a7774e60f4e6ae2b0cc7ded88cdc4ac68d65c6..918714269cd546194b5af7abbff765962a9664a1 100644 --- a/docs/20_Checks/_index.md +++ b/docs/20_Checks/_index.md @@ -26,6 +26,7 @@ There is one include script used by all checks: * [check_eol](check_eol.md) * [check_fs_errors](check_fs_errors.md) * [check_fs_writable](check_fs_writable.md) +* [check_gitlab_tokens](check_gitlab_tokens.md) * [check_haproxy_health](check_haproxy_health.md) * [check_haproxy_status](check_haproxy_status.md) * [check_http](check_http.md) diff --git a/docs/20_Checks/check_gitlab_tokens.md b/docs/20_Checks/check_gitlab_tokens.md new file mode 100644 index 0000000000000000000000000000000000000000..50ac7121dab949f53847f0a21e4c652d4415b7ce --- /dev/null +++ b/docs/20_Checks/check_gitlab_tokens.md @@ -0,0 +1,115 @@ +# Check Gitlab tokens + +## Introduction + +**check_gitlab_tokens** checks all newer tokens of projects and groups if they expire soon. You can set a warning and a critical level in days. + +Gitlab has an api requrest `/personal_access_tokens` but it doesn't have the information about the project or usergroup where it is defined. +This check executes additional requests to show it and offers the url to the web linkinterface. + +The check returns + +* unknown - the http request to gitlab api failed +* critical - min. 1 token is expiring soon +* warning - min. 1 token reached the warning level (and no criritical token was found) +* ok - api request was successful; no critical or warning token was found. + +## Requirements + +* curl +* Bash REST API client<br>A set of class like functions with a http. prefix. <br>Docs: <https://os-docs.iml.unibe.ch/bash-rest-api-client/> + +Extract or Git pull the Bash REST API client somewhere in your filesystem. eg. /opt/bash-api-client/. With the parameter `-r <FILE>` you point to the file `rest-api-client.sh`. + +## Configuration + +The script needs to connect to the Gitlab API. +You need to create a token in a admin group to read all tokens of all projects. + +Put 2 bash variabbles into `/etc/icinga2/gitlab.cfg`: + +```shell +GITLAB_API='https://gitlab.example.com/api/v4' +GITLAB_TOKEN='glpat-1234567890' +``` + +You can use another filename for this configuration - but then you need the parameter `-g <FILE>`to reference it. + +## Syntax + +```txt +./check_gitlab_tokens -h +______________________________________________________________________ + +CHECK_GITLAB_TOKENS +v1.0 + +(c) Institute for Medical Education - University of Bern +Licence: GNU GPL 3 + +https://os-docs.iml.unibe.ch/icinga-checks/Checks/check_gitlab_tokens.html +______________________________________________________________________ + +Check gitlab tokens and warn if tokens expire soon. + +This check fetches the gitlbab tokens created in the last 395 days +from the Gitlab API. It skips + + - personal access tokens of users + - revoked tokens + +The script can run several seconds depending on count of tokens, projects +and users. Maybe you want to call it with a longer interval. + +SYNTAX: +check_gitlab_tokens [OPTIONS] + +OPTIONS: + + -h or --help show this help. + + -w VALUE warning level (default: 30) + -c VALUE critical level (default: 10) + + -g FILE path to GITLAB_CONFIG; default: /etc/icinga2/gitlab.cfg + -r FILE path to REST_CLIENT; default: ./../inc/rest-api-client.sh + + -s DAYS Number of days for max age of token; default: 395 + +PARAMETERS: + + None. + +EXAMPLES: + + check_gitlab_tokens -w 28 -c 7 + Set other warning and critical level + + check_gitlab_tokens -g ./gitlab.cfg + Set a custom gitlab config file + + check_gitlab_tokens -r /opt/bash-api-client/bash-api-client.sh + Set a custom gitlab config file + +``` + +## Example + +The execution of `check_gitlab_tokens` returns + +* a status line with found tokens total, count of warning and critical +* one line per token with + * date of expiration + * status; one of OK, warning, critical based on number of days before expiring + * name of thwe token + * name of the project or group + * web link to the token page of the project or group + +```text +OK: 16 Gitlab Tokens (max 395 days old) .. critical: 0 (10 days) .. warnings: 0 (30 days) + +2025-01-17 OK changelog - demoproject <https://gitlab.example.com/test/demoproject/-/settings/access_tokens> +2025-01-23 OK read_repo - demoproject <https://gitlab.example.com/test/demoproject/-/settings/access_tokens> +2025-03-14 OK api_token - admin <https://gitlab.example.com/admin/sysadminstuff/-/settings/access_tokens> +... +``` \ No newline at end of file diff --git a/docs/style.css b/docs/style.css index 4186d17f75909f2d9190cff5152d48366c829baf..b5afea45f18c3f10ab029eaca933088ee78d308d 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1,28 +1,30 @@ /* override css elements of daux.io blue theme - version 2023-10-09 + version 2024-10-24 */ :root { /* Axels Overrides */ --color-text: #234; - --link-color: #822; + --link-color: #228; --brand-color: var(--color-text); --brand-background: var(--body-background); + --code-tag-background-color: #f0f3f3; + --code-tag-border-color: #dee; + --code-tag-box-shadow: none; --hr-color: none; + --pager-background-color: #f8fafa; + --pager-border-color: none; --search-field-background: none; --search-field-border-color: none; --sidebar-background: var(--body-background); --sidebar-border-color: none; - --sidebar-link-active-background: #e8f4f6; - --sidebar-link-active-background: #eee; + --sidebar-link-active-background: #f0f4f6; /* Axels custom values */ --axel_bg-toc: var(--body-background); --axel_bg-toc-head: #f8f8f8; --axel_brand-background: none; --axel_brand-pre-background: rgb(255, 0, 51); - ; --axel_brand-pre-background-hover: rgb(255, 0, 51); - ; --axel_h1_header: none; --axel_h1: #111; --axel_h1-bg: none; @@ -33,12 +35,13 @@ --axel_h2-hero-bottom: 2px solid #912; --axel_h3: #333; --axel_h3-bottom: 0px solid #ddd; - --axel_h4: #444; - --axel_hero_bg: #f8f8f8; + --axel_h4: #478; + --axel_h5: #699; + --axel_hero_bg: #faf8f6; --axel_img-border: 2px dashed #ccc; --axel_nav-bg: #fcfcfc; --axel_nav-buttomborder: #ddd; - --axel_pre-background: #f8f8f8; + --axel_pre-background: #faf8f6; --axel-th-background: #e0e4e8; --axel-article-nav-border-top: 0px dotted #ddd; } @@ -46,9 +49,10 @@ .dark { /* Axels Overrides */ --color-text: #c0c0c0; - --link-color: #c66; + --link-color: #88e; --brand-color: var(--color-text); --brand-background: var(--body-background); + --body-background: #101418; --hr-color: none; --code-tag-background-color_: #bcc; --search-field-background: none; @@ -74,6 +78,8 @@ --axel_h2-hero-bottom: 2px solid #712; --axel_h3: #589; --axel_h3-bottom: 0px solid #333; + --axel_h4: #478; + --axel_h5: #278; --axel_hero_bg: #242424; --axel_img-border: 2px dashed #555; --axel_nav-bg: #242424; @@ -123,6 +129,17 @@ a.Brand { padding-top: 1em; } +.s-content h1::before{ + background: #fee; + border: 3px double #f00; + color: #f00; + content: 'FEHLER: Keine Überschrift 1 in einer Markdown-Datei für Daux verwenden! Mit H2 beginnen!'; + display: block; + font-size: 50%; + padding: 0.3em; + margin-bottom: 2em; +} + .s-content h1 { background: var(--axel_h1-bg); color: var(--axel_h1); @@ -168,6 +185,13 @@ img{ .s-content > h4 { color: var(--axel_h4); + font-size: 140%; + font-weight: bold; + margin: 2em 0; +} + +.s-content > h5 { + color: var(--axel_h5); font-size: 135%; font-weight: bold; margin: 2em 0; @@ -175,10 +199,12 @@ img{ .s-content .TableOfContentsContainer h4 { margin: 1em 0; - font-size: 100%; + font-size: 110%; text-align: center; - background-color: rgba(0, 0, 0, 0.05); + background-color: rgba(0, 0, 0, 0.1); padding: 0.3em; + font-weight: bold; + font-family: Arial; } ul.TableOfContents a{ color: var(--color-text); @@ -241,6 +267,7 @@ div.hero h2 { position: fixed; right: 2em; top: 1em; + height: 96%; } } @@ -254,12 +281,14 @@ div.hero h2 { border-top-left-radius: 1em; font-size: 1.1em; margin: 0; - padding: 0; + padding: 0.3em; } .TableOfContentsContainer__content { - border-width: 1px; + border-width: 0px; font-size: 0.5em; + height: inherit; + overflow: auto; } ul.TableOfContents ul { @@ -267,17 +296,17 @@ ul.TableOfContents ul { padding-left: 1em; } +.TableOfContents a:hover{ + text-decoration: underline; +} + /* ----- Icons on links --- */ .EditOn a::before{ content: '✏️ '; } -.Links a[href^="https://github.com/"]::before { - content: '🌐 '; -} - -.Links a[href^="https://git-repo.iml.unibe.ch/"]::before { +.Links a::before { content: '🌐 '; }