complete rewrite of so-status

This commit is contained in:
Jason Ertel
2022-09-16 17:46:52 -04:00
parent deb19d24b8
commit 9542a5ada2
4 changed files with 151 additions and 276 deletions

View File

@@ -193,8 +193,15 @@ sostatus_log:
- name: /opt/so/log/sostatus/status.log - name: /opt/so/log/sostatus/status.log
- mode: 644 - mode: 644
common_pip_dependencies:
pip.installed:
- user: root
- pkgs:
- rich
- target: /usr/lib64/python3.6/site-packages
# Install sostatus check cron # Install sostatus check cron
'/usr/sbin/so-status -q; echo $? > /opt/so/log/sostatus/status.log 2>&1': '/usr/sbin/so-status -j > /opt/so/log/sostatus/status.log 2>&1':
cron.present: cron.present:
- user: root - user: root
- minute: '*/1' - minute: '*/1'

View File

@@ -1,301 +1,165 @@
#!/bin/bash #!/usr/bin/env python3
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one # Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at # or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
# https://securityonion.net/license; you may not use this file except in compliance with the # https://securityonion.net/license; you may not use this file except in compliance with the
# Elastic License 2.0. # Elastic License 2.0.
if ! [ "$(id -u)" = 0 ]; then import json
echo "This command must be run as root" import os
exit 1 from rich import box
fi from rich.console import Console
from rich.table import Table
import subprocess
import sys
import time
display_help() { EXPECTED_CONTAINERS_FILE = "/opt/so/conf/so-status/so-status.conf"
cat <<HELP_USAGE HIGHSTATE_COMPLETE_FILE = "/opt/so/log/salt/lasthighstate"
UPTIME_FILE = "/proc/uptime"
$0 [-h] [-q|--quiet] def showUsage(options, args):
print('Usage: {} [OPTIONS]'.format(sys.argv[0]))
-h Show this message. print(' Options:')
-q|--quiet Suppress the output and only return a print(' -h - Prints this usage information')
single status code for overall status print(' -q - Suppress output; useful for automation of exit code value')
0:Ok, 1:Error, 2:Starting/Pending, 99:Installing SO print(' -j - Output in JSON format')
HELP_USAGE print('')
} print(' Exit codes:')
print(' 0 - Success, system appears to be running correctly')
# Constants print(' 1 - Error, one or more subsystems are not running')
QUIET=false print(' 2 - System is starting')
EXITCODE=0 print(' 99 - Installation in progress')
SYSTEM_START_TIME=$(date -d "$(</proc/uptime awk '{print $1}') seconds ago" +%s) sys.exit(1)
# file populated by salt.lasthighstate state at end of successful highstate run
LAST_HIGHSTATE_END=$([ -e "/opt/so/log/salt/lasthighstate" ] && date -r /opt/so/log/salt/lasthighstate +%s || echo 0)
LAST_SOSETUP_LOG=$([ -e "/root/sosetup.log" ] && date -r /root/sosetup.log +%s || echo 0)
HIGHSTATE_RUNNING=$(salt-call --local saltutil.running --out=json | jq -r '.local[].fun' | grep -q 'state.highstate' && echo $?)
ERROR_STRING="ERROR"
SUCCESS_STRING="OK"
PENDING_STRING="PENDING"
MISSING_STRING='MISSING'
DISABLED_STRING='DISABLED'
WAIT_START_STRING='WAIT_START'
STARTING_STRING='STARTING'
CALLER=$(ps -o comm= $PPID)
declare -a BAD_STATUSES=("removing" "paused" "exited" "dead")
declare -a PENDING_STATUSES=("paused" "created" "restarting")
declare -a GOOD_STATUSES=("running")
declare -a DISABLED_CONTAINERS=()
mapfile -t DISABLED_CONTAINERS < <(sort -u /opt/so/conf/so-status/so-status.conf | grep "^\s*#" | tr -d "#")
declare -a temp_container_name_list=() def fail(msg):
declare -a temp_container_state_list=() print(msg, file=sys.stderr)
sys.exit(1)
declare -a container_name_list=()
declare -a container_state_list=()
declare -a expected_container_list=() def check_system_status(options, console):
code = 0
highstate_end_time = 0
try:
highstate_end_time = os.path.getmtime(HIGHSTATE_COMPLETE_FILE)
uptime_file = open(UPTIME_FILE, "r")
uptime_contents = uptime_file.readline()
uptime = uptime_contents.split()
if len(uptime) != 2:
fail("Unable to determine system uptime")
system_start_time = time.time() - float(uptime[0])
if highstate_end_time < system_start_time:
code = 2
except OSError:
code = 99
# {% raw %} return code
compare_lists() {
local found=0
create_expected_container_list def output(options, console, code, data):
if "-j" in options:
summary = { "status_code": code, "containers": data }
print(json.dumps(summary))
elif "-q" not in options:
if code == 2:
console.print(" [bright_yellow]:hourglass: [bold white]System appears to be starting. No highstate has completed since the system was restarted.")
elif code == 99:
console.print(" [bright_red]:exclamation: [bold white]Installation does not appear to be complete. A highstate has not fully completed.")
else:
table = Table(title = "Security Onion Status", row_styles = ["dim", ""], show_edge = False, safe_box = True, box = box.MINIMAL)
table.add_column("Container", justify="right", style="white", no_wrap=True)
table.add_column("Status", justify="left", style="green", no_wrap=True)
table.add_column("Details", justify="right", style="cyan", no_wrap=True)
data.sort(key = lambda x: x['Name'])
for container in data:
color = "[green]"
if container['Status'] != "running":
color = "[bright_red]"
table.add_row(container['Name'], color + container['Status'], container['Details'])
console.print()
console.print(table)
console.print()
if code == 0:
console.print(" [green]:heavy_check_mark: [bold white]This onion is ready to make your adversaries cry!")
elif code == 1:
console.print(" [bright_red]:exclamation: [bold white]Check container logs and /opt/so/log for more details.")
console.print()
if [[ ${#expected_container_list[@]} = 0 ]]; then def check_container_status(options, console):
container_name_list="${temp_container_name_list[*]}" code = 0
container_state_list="${temp_container_state_list[*]}" cli = "docker"
return 1 proc = subprocess.run([cli, 'ps', '--format', '{{json .}}'], stdout=subprocess.PIPE, encoding="utf-8")
fi if proc.returncode != 0:
fail("Container system error; unable to obtain container process statuses")
for intended_item in "${expected_container_list[@]}"; do container_list = []
found=0 expected_file = open(EXPECTED_CONTAINERS_FILE, "r")
for i in "${!temp_container_name_list[@]}"; do for container in expected_file:
[[ ${temp_container_name_list[$i]} = "$intended_item" ]] \ container = container.strip()
&& found=1 \ exists = False
&& container_name_list+=("${temp_container_name_list[$i]}") \ details = { "Name": container, "Status": "missing", "Details": ""}
&& container_state_list+=("${temp_container_state_list[$i]}") \
&& break
done
if [[ $found = 0 ]]; then
container_name_list+=("$intended_item")
container_state_list+=("missing")
fi
done
}
# {% endraw %} try:
# Podman format
containers_data = json.loads(proc.stdout)
for item in containers_data:
if item['Names'][0] == container:
details['Status'] = item['State']
details['Details'] = item['Status']
container_list.append(details)
exists = True
if item['State'] != "running":
code = 1
break
create_expected_container_list() { except:
# Docker format
for line in proc.stdout.splitlines():
if len(line) > 2:
item = json.loads(line)
if item['Names'] == container:
details['Status'] = item['State']
details['Details'] = item['Status']
container_list.append(details)
exists = True
if item['State'] != "running":
code = 1
break
mapfile -t expected_container_list < <(sort -u /opt/so/conf/so-status/so-status.conf | tr -d "#") if not exists:
container_list.append(details)
code = 1
return code, container_list
}
populate_container_lists() { def check_status(options, console):
systemctl is-active --quiet docker container_list = []
code = check_system_status(options, console)
if code == 0:
code, container_list = check_container_status(options, console)
output(options, console, code, container_list)
return code
if [[ $? = 0 ]]; then
mapfile -t docker_raw_list < <(curl -s --unix-socket /var/run/docker.sock http:/v1.40/containers/json?all=1 \
| jq -c '.[] | { Name: .Names[0], State: .State }' \
| tr -d '/{"}')
else
exit 1
fi
local container_name="" def main():
local container_state="" options = []
args = sys.argv[1:]
for option in args:
if option.startswith("-"):
options.append(option)
args.remove(option)
for line in "${docker_raw_list[@]}"; do if len(args) != 0 or "-h" in options:
container_name="$( echo $line | sed -e 's/Name:\(.*\),State:\(.*\)/\1/' )" # Get value in the first search group (container names) showUsage(options, None)
container_state="$( echo $line | sed -e 's/Name:\(.*\),State:\(.*\)/\2/' )" # Get value in the second search group (container states)
temp_container_name_list+=( "${container_name}" ) if os.environ["USER"] != "root":
temp_container_state_list+=( "${container_state}" ) fail("This program must be run as root")
done
compare_lists console = Console()
} sys.exit(check_status(options, console))
parse_status() {
local service_name=${1}
local container_state=${2}
for state in "${GOOD_STATUSES[@]}"; do if __name__ == "__main__":
[[ $container_state = "$state" ]] && [[ $QUIET = "false" ]] && printf $SUCCESS_STRING && return 0 || [[ $container_state = "$state" ]] && return 0 main()
done
for state in "${BAD_STATUSES[@]}"; do
[[ " ${DISABLED_CONTAINERS[@]} " =~ " ${service_name} " ]] && [[ $QUIET = "false" ]] && printf $DISABLED_STRING && return 0 || [[ " ${DISABLED_CONTAINERS[@]} " =~ " ${service_name} " ]] && return 0
done
# if a highstate has finished running since the system has started
# then the containers should be running so let's check the status
if [ $LAST_HIGHSTATE_END -ge $SYSTEM_START_TIME ]; then
[[ $container_state = "missing" ]] && [[ $QUIET = "false" ]] && printf $MISSING_STRING && return 1 || [[ $container_state = "missing" ]] && [[ "$EXITCODE" -lt 2 ]] && EXITCODE=1 && return 1
for state in "${PENDING_STATUSES[@]}"; do
[[ $container_state = "$state" ]] && [[ $QUIET = "false" ]] && printf $PENDING_STRING && return 0
done
# This is technically not needed since the default is error state
for state in "${BAD_STATUSES[@]}"; do
[[ $container_state = "$state" ]] && [[ $QUIET = "false" ]] && printf $ERROR_STRING && return 1 || [[ $container_state = "$state" ]] && [[ "$EXITCODE" -lt 2 ]] && EXITCODE=1 && return 1
done
[[ $QUIET = "false" ]] && printf $ERROR_STRING && return 1 || [[ "$EXITCODE" -lt 2 ]] && EXITCODE=1 && return 1
# if a highstate has not run since system start time, but a highstate is currently running
# then show that the containers are STARTING
elif [[ "$HIGHSTATE_RUNNING" == 0 ]]; then
[[ $QUIET = "false" ]] && printf $STARTING_STRING && return 2 || EXITCODE=2 && return 2
# if a highstate has not finished running since system startup and isn't currently running
# then just show that the containers are WAIT_START; waiting to be started
else
[[ $QUIET = "false" ]] && printf $WAIT_START_STRING && return 2 || EXITCODE=2 && return 2
fi
}
# {% raw %}
print_line() {
local service_name=${1}
local service_state="$( parse_status ${1} ${2} )"
local columns=$(tput cols)
local state_color="\e[0m"
local PADDING_CONSTANT=15
if [[ $service_state = "$ERROR_STRING" ]] || [[ $service_state = "$MISSING_STRING" ]]; then
state_color="\e[1;31m"
if [[ "$EXITCODE" -eq 0 ]]; then
EXITCODE=1
fi
elif [[ $service_state = "$SUCCESS_STRING" ]]; then
state_color="\e[1;32m"
elif [[ $service_state = "$PENDING_STRING" ]] || [[ $service_state = "$DISABLED_STRING" ]] || [[ $service_state = "$STARTING_STRING" ]] || [[ $service_state = "$WAIT_START_STRING" ]]; then
state_color="\e[1;33m"
EXITCODE=2
fi
printf " $service_name "
for i in $(seq 0 $(( $columns - $PADDING_CONSTANT - ${#service_name} - ${#service_state} ))); do
printf "${state_color}%b\e[0m" "-"
done
printf " [ "
printf "${state_color}%b\e[0m" "$service_state"
printf "%s \n" " ]"
}
non_term_print_line() {
local service_name=${1}
local service_state="$( parse_status ${1} ${2} )"
if [[ $service_state = "$ERROR_STRING" ]] || [[ $service_state = "$MISSING_STRING" ]]; then
if [[ "$EXITCODE" -eq 0 ]]; then
EXITCODE=1
fi
elif [[ $service_state = "$PENDING_STRING" ]] || [[ $service_state = "$DISABLED_STRING" ]] || [[ $service_state = "$STARTING_STRING" ]] || [[ $service_state = "$WAIT_START_STRING" ]]; then
EXITCODE=2
fi
printf " $service_name "
for i in $(seq 0 $(( 35 - ${#service_name} - ${#service_state} ))); do
printf "-"
done
printf " [ "
printf "$service_state"
printf "%s \n" " ]"
}
main() {
# if running from salt
if [ "$CALLER" == 'salt-call' ] || [ "$CALLER" == 'salt-minion' ]; then
printf "\n"
printf "Checking Docker status\n\n"
systemctl is-active --quiet docker
if [[ $? = 0 ]]; then
non_term_print_line "Docker" "running"
else
non_term_print_line "Docker" "exited"
fi
populate_container_lists
printf "\n"
printf "Checking container statuses\n\n"
local num_containers=${#container_name_list[@]}
for i in $(seq 0 $(($num_containers - 1 ))); do
non_term_print_line ${container_name_list[$i]} ${container_state_list[$i]}
done
printf "\n"
# else if running from a terminal
else
if [ "$QUIET" = true ]; then
if [ $SYSTEM_START_TIME -lt $LAST_SOSETUP_LOG ]; then
exit 99
fi
print_or_parse="parse_status"
else
print_or_parse="print_line"
local focus_color="\e[1;34m"
printf "\n"
printf "${focus_color}%b\e[0m" "Checking Docker status\n\n"
fi
systemctl is-active --quiet docker
if [[ $? = 0 ]]; then
${print_or_parse} "Docker" "running"
else
${print_or_parse} "Docker" "exited"
fi
populate_container_lists
if [ "$QUIET" = false ]; then
printf "\n"
printf "${focus_color}%b\e[0m" "Checking container statuses\n\n"
fi
local num_containers=${#container_name_list[@]}
for i in $(seq 0 $(($num_containers - 1 ))); do
${print_or_parse} ${container_name_list[$i]} ${container_state_list[$i]}
done
if [ "$QUIET" = false ]; then
printf "\n"
fi
fi
}
# {% endraw %}
while getopts ':hq' OPTION; do
case "$OPTION" in
h)
display_help
exit 0
;;
q)
QUIET=true
;;
\?)
display_help
exit 0
;;
esac
done
main
exit $EXITCODE

View File

@@ -6,11 +6,13 @@ soc:
description: Customize the login page with a specific markdown-formatted message. description: Customize the login page with a specific markdown-formatted message.
file: True file: True
global: True global: True
syntax: md
motd__md: motd__md:
title: Overview Page title: Overview Page
description: Customize the overview page with specific markdown-formatted content. Images can be used but must be hosted from another host that is accessible by the users' browser. description: Customize the overview page with specific markdown-formatted content. Images can be used but must be hosted from another host that is accessible by the users' browser.
file: True file: True
global: True global: True
syntax: md
custom__js: custom__js:
title: Custom Javascript title: Custom Javascript
description: Customize SOC UI behavior with custom Javascript code. Custom Javascript not provided by Security Onion Solutions is unsupported, and should be removed prior to requesting support and prior to performing upgrades. description: Customize SOC UI behavior with custom Javascript code. Custom Javascript not provided by Security Onion Solutions is unsupported, and should be removed prior to requesting support and prior to performing upgrades.

View File

@@ -11,10 +11,12 @@
if [[ ! "`pidof -x $(basename $0) -o %PPID`" ]]; then if [[ ! "`pidof -x $(basename $0) -o %PPID`" ]]; then
SOSTATUSLOG=/var/log/sostatus/status.log SOSTATUSLOG=/var/log/sostatus/status.log
SOSTATUSSTATUS=$(cat /var/log/sostatus/status.log) SOSTATUSCODE=$(jq -r .status_code /var/log/sostatus/status.log)
SOSTATUSJSON=$(cat /var/log/sostatus/status.log)
if [ -f "$SOSTATUSLOG" ]; then if [ -f "$SOSTATUSLOG" ]; then
echo "sostatus status=$SOSTATUSSTATUS" echo "sostatus status=$SOSTATUSCODE"
echo "sostatus json=$SOSTATUSJSON"
else else
exit 0 exit 0
fi fi