Files
linux-scripts/service-dependency-mapper.sh
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

595 lines
23 KiB
Bash
Executable File

#!/usr/bin/env bash
#########################################################################################
#### service-dependency-mapper.sh — Map systemd service dependencies ####
#### Shows what depends on a service and what it requires ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version 1.00 ####
#### ####
#### Usage: ####
#### ./service-dependency-mapper.sh nginx ####
#### ./service-dependency-mapper.sh --all --format tree ####
#### ./service-dependency-mapper.sh postgresql --reverse ####
#### ./service-dependency-mapper.sh --critical ####
#### ####
#### See --help for all options. ####
#########################################################################################
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────
SERVICE=""
MODE="forward"
MAP_ALL="false"
CRITICAL="false"
DEPTH=3
FORMAT="tree"
COLOR="auto"
# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
# ── Colors ────────────────────────────────────────────────────────────
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
setup_colors() {
if [[ "$COLOR" == "never" ]]; then
RED="" GREEN="" YELLOW="" BOLD="" DIM="" CYAN="" RESET=""
return
fi
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
else
RED="" GREEN="" YELLOW="" BOLD="" DIM="" CYAN="" RESET=""
fi
}
# ── Logging ───────────────────────────────────────────────────────────
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
# ══════════════════════════════════════════════════════════════════════
# USAGE
# ══════════════════════════════════════════════════════════════════════
usage() {
cat <<EOF
${SCRIPT_NAME} — Map systemd service dependencies
USAGE:
${SCRIPT_NAME} SERVICE [OPTIONS]
${SCRIPT_NAME} --all [OPTIONS]
${SCRIPT_NAME} --critical [OPTIONS]
OPTIONS:
--forward Show what this service requires (default)
--reverse Show what depends ON this service
--all Map all active services
--critical Show services with the most reverse dependencies
--depth N Depth limit for dependency traversal (default: 3)
--format FORMAT Output format: tree, flat, dot, json (default: tree)
--no-color Disable colored output
--help Show this help
EXAMPLES:
# Show what nginx requires
./${SCRIPT_NAME} nginx
# Show what breaks if postgresql stops
./${SCRIPT_NAME} postgresql --reverse
# Show all active service dependencies as a tree
./${SCRIPT_NAME} --all --format tree
# Find highest-impact services
./${SCRIPT_NAME} --critical
# Generate Graphviz DOT output
./${SCRIPT_NAME} nginx --format dot | dot -Tpng -o nginx-deps.png
# Shallow scan
./${SCRIPT_NAME} nginx --depth 1
EOF
}
# ══════════════════════════════════════════════════════════════════════
# ARGUMENT PARSING
# ══════════════════════════════════════════════════════════════════════
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--forward)
MODE="forward"; shift ;;
--reverse)
MODE="reverse"; shift ;;
--all)
MAP_ALL="true"; shift ;;
--critical)
CRITICAL="true"; shift ;;
--depth)
DEPTH="$2"; shift 2 ;;
--format)
FORMAT="$2"; shift 2 ;;
--no-color)
COLOR="never"; shift ;;
--help|-h)
usage; exit 0 ;;
-*)
err "Unknown option: $1"; usage; exit 1 ;;
*)
SERVICE="$1"; shift ;;
esac
done
if [[ "$MAP_ALL" == "false" && "$CRITICAL" == "false" && -z "$SERVICE" ]]; then
err "No service specified. Use --all, --critical, or provide a service name."
usage
exit 1
fi
# Validate format
case "$FORMAT" in
tree|flat|dot|json) ;;
*) err "Unknown format: $FORMAT"; exit 1 ;;
esac
# Validate depth is a positive integer
if ! [[ "$DEPTH" =~ ^[0-9]+$ ]] || [[ "$DEPTH" -lt 1 ]]; then
err "Depth must be a positive integer"
exit 1
fi
}
# ══════════════════════════════════════════════════════════════════════
# HELPERS
# ══════════════════════════════════════════════════════════════════════
# Normalize unit name — append .service if no suffix present
normalize_unit() {
local unit="$1"
if [[ "$unit" != *.* ]]; then
echo "${unit}.service"
else
echo "$unit"
fi
}
# Get active state of a unit
get_active_state() {
systemctl show -p ActiveState --value "$1" 2>/dev/null || echo "unknown"
}
# Get load state of a unit
get_load_state() {
systemctl show -p LoadState --value "$1" 2>/dev/null || echo "unknown"
}
# Get unit description
get_description() {
systemctl show -p Description --value "$1" 2>/dev/null || echo ""
}
# Color a unit name based on its active state
color_unit() {
local unit="$1"
local state
state="$(get_active_state "$unit")"
case "$state" in
active) echo -e "${GREEN}${unit}${RESET}" ;;
failed) echo -e "${RED}${unit}${RESET}" ;;
inactive) echo -e "${YELLOW}${unit}${RESET}" ;;
*) echo -e "${DIM}${unit}${RESET}" ;;
esac
}
# Get list of all active units (services, sockets, timers, targets)
get_active_units() {
systemctl list-units --type=service,socket,timer,target --state=active \
--no-pager --no-legend --plain 2>/dev/null | awk '{print $1}'
}
# ══════════════════════════════════════════════════════════════════════
# DEPENDENCY COLLECTION
# ══════════════════════════════════════════════════════════════════════
# Collect dependencies using systemctl list-dependencies
# Args: unit, direction (forward|reverse), max_depth
# Outputs lines: "depth unit" (tab-indented raw from systemctl, parsed)
collect_deps() {
local unit="$1"
local direction="$2"
local max_depth="$3"
local reverse_flag=""
[[ "$direction" == "reverse" ]] && reverse_flag="--reverse"
# systemctl list-dependencies outputs an indented tree with unicode chars
# We parse the depth from leading whitespace and extract the unit name
systemctl list-dependencies "$unit" $reverse_flag --no-pager --plain 2>/dev/null | \
tail -n +2 | while IFS= read -r line; do
# Count leading spaces (each level = 2 spaces)
local stripped="${line#"${line%%[! ]*}"}"
local spaces=$(( ${#line} - ${#stripped} ))
local depth=$(( spaces / 2 ))
# Respect depth limit
if [[ $depth -ge $max_depth ]]; then
continue
fi
# Clean unit name (remove any tree-drawing characters)
local dep_unit
dep_unit="$(echo "$stripped" | sed 's/[●○]//g' | xargs)"
if [[ -n "$dep_unit" ]]; then
echo "${depth} ${dep_unit}"
fi
done
}
# ══════════════════════════════════════════════════════════════════════
# SERVICE INFO
# ══════════════════════════════════════════════════════════════════════
print_service_info() {
local unit="$1"
local active_state load_state description
active_state="$(get_active_state "$unit")"
load_state="$(get_load_state "$unit")"
description="$(get_description "$unit")"
echo ""
echo -e "${BOLD}Service Info${RESET}"
echo " ──────────────────────────────────────────────────────────────"
echo -e " Name: ${BOLD}${unit}${RESET}"
echo -e " Description: ${description}"
echo -e " Load State: ${load_state}"
case "$active_state" in
active) echo -e " Active State: ${GREEN}${active_state}${RESET}" ;;
failed) echo -e " Active State: ${RED}${active_state}${RESET}" ;;
inactive) echo -e " Active State: ${YELLOW}${active_state}${RESET}" ;;
*) echo -e " Active State: ${DIM}${active_state}${RESET}" ;;
esac
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# OUTPUT FORMATTERS
# ══════════════════════════════════════════════════════════════════════
# ── Tree output ───────────────────────────────────────────────────────
output_tree() {
local unit="$1"
local direction="$2"
local deps
deps="$(collect_deps "$unit" "$direction" "$DEPTH")"
if [[ "$direction" == "forward" ]]; then
echo -e "${BOLD}Forward Dependencies${RESET} (what ${unit} requires):"
else
echo -e "${BOLD}Reverse Dependencies${RESET} (what depends on ${unit}):"
fi
echo " ──────────────────────────────────────────────────────────────"
if [[ -z "$deps" ]]; then
echo " (none)"
echo ""
return
fi
while IFS= read -r line; do
local depth="${line%% *}"
local dep_unit="${line#* }"
local indent=""
for (( i=0; i<depth; i++ )); do
indent+=" "
done
local colored
colored="$(color_unit "$dep_unit")"
echo -e " ${indent}├── ${colored}"
done <<< "$deps"
echo ""
}
# ── Flat output ───────────────────────────────────────────────────────
output_flat() {
local unit="$1"
local direction="$2"
local deps
deps="$(collect_deps "$unit" "$direction" "$DEPTH")"
if [[ -z "$deps" ]]; then
return
fi
while IFS= read -r line; do
local dep_unit="${line#* }"
echo "$dep_unit"
done <<< "$deps" | sort -u
}
# ── DOT output ────────────────────────────────────────────────────────
output_dot() {
local unit="$1"
local direction="$2"
local deps
deps="$(collect_deps "$unit" "$direction" "$DEPTH")"
echo "digraph dependencies {"
echo " rankdir=LR;"
echo " node [shape=box, fontname=\"monospace\", fontsize=10];"
echo " edge [color=\"#555555\"];"
echo ""
# Color the root node
local root_state
root_state="$(get_active_state "$unit")"
local root_color="#4CAF50"
[[ "$root_state" == "failed" ]] && root_color="#F44336"
[[ "$root_state" == "inactive" ]] && root_color="#FF9800"
echo " \"${unit}\" [style=filled, fillcolor=\"${root_color}\", fontcolor=white];"
echo ""
if [[ -n "$deps" ]]; then
while IFS= read -r line; do
local dep_unit="${line#* }"
if [[ "$direction" == "forward" ]]; then
echo " \"${unit}\" -> \"${dep_unit}\";"
else
echo " \"${dep_unit}\" -> \"${unit}\";"
fi
done <<< "$deps"
fi
echo "}"
}
# ── JSON output ───────────────────────────────────────────────────────
output_json() {
local unit="$1"
local direction="$2"
local deps active_state load_state description
deps="$(collect_deps "$unit" "$direction" "$DEPTH")"
active_state="$(get_active_state "$unit")"
load_state="$(get_load_state "$unit")"
description="$(get_description "$unit")"
echo "{"
echo " \"unit\": \"${unit}\","
echo " \"active_state\": \"${active_state}\","
echo " \"load_state\": \"${load_state}\","
echo " \"description\": \"${description}\","
echo " \"direction\": \"${direction}\","
echo " \"depth_limit\": ${DEPTH},"
echo " \"dependencies\": ["
local first="true"
if [[ -n "$deps" ]]; then
while IFS= read -r line; do
local depth="${line%% *}"
local dep_unit="${line#* }"
local dep_state
dep_state="$(get_active_state "$dep_unit")"
if [[ "$first" == "true" ]]; then
first="false"
else
echo ","
fi
printf ' {"unit": "%s", "depth": %s, "active_state": "%s"}' \
"$dep_unit" "$depth" "$dep_state"
done <<< "$deps"
echo ""
fi
echo " ]"
echo "}"
}
# ══════════════════════════════════════════════════════════════════════
# IMPACT SUMMARY
# ══════════════════════════════════════════════════════════════════════
print_impact_summary() {
local unit="$1"
local reverse_deps
reverse_deps="$(collect_deps "$unit" "reverse" "$DEPTH")"
local count=0
if [[ -n "$reverse_deps" ]]; then
count="$(echo "$reverse_deps" | wc -l)"
fi
echo -e "${BOLD}Impact Summary${RESET}"
echo " ──────────────────────────────────────────────────────────────"
if [[ $count -eq 0 ]]; then
echo -e " Stopping ${BOLD}${unit}${RESET} would affect ${GREEN}0${RESET} other units."
elif [[ $count -le 3 ]]; then
echo -e " Stopping ${BOLD}${unit}${RESET} would affect ${YELLOW}${count}${RESET} other unit(s)."
else
echo -e " Stopping ${BOLD}${unit}${RESET} would affect ${RED}${count}${RESET} other unit(s)."
fi
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# CRITICAL MODE
# ══════════════════════════════════════════════════════════════════════
run_critical() {
echo ""
echo -e "${BOLD}Critical Services — ranked by reverse dependency count${RESET}"
echo -e "${DIM}(services with the most units depending on them)${RESET}"
echo " ══════════════════════════════════════════════════════════════"
echo ""
local units
units="$(get_active_units)"
if [[ -z "$units" ]]; then
err "No active units found"
exit 1
fi
# Collect reverse dep counts
local -a results=()
while IFS= read -r unit; do
local reverse_deps count=0
reverse_deps="$(collect_deps "$unit" "reverse" "$DEPTH" 2>/dev/null)"
if [[ -n "$reverse_deps" ]]; then
count="$(echo "$reverse_deps" | wc -l)"
fi
if [[ $count -gt 0 ]]; then
results+=("${count} ${unit}")
fi
done <<< "$units"
# Sort by count descending, show top 20
printf '%s\n' "${results[@]}" | sort -rn | head -20 | while IFS= read -r line; do
local count="${line%% *}"
local unit="${line#* }"
local colored
colored="$(color_unit "$unit")"
if [[ $count -ge 10 ]]; then
printf " ${RED}%4d${RESET} %b\n" "$count" "$colored"
elif [[ $count -ge 5 ]]; then
printf " ${YELLOW}%4d${RESET} %b\n" "$count" "$colored"
else
printf " ${GREEN}%4d${RESET} %b\n" "$count" "$colored"
fi
done
echo ""
echo -e "${DIM} Top 20 shown. Higher count = more impact if stopped.${RESET}"
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# ALL MODE
# ══════════════════════════════════════════════════════════════════════
run_all() {
local units
units="$(get_active_units)"
if [[ -z "$units" ]]; then
err "No active units found"
exit 1
fi
echo ""
echo -e "${BOLD}All Active Service Dependencies${RESET} (${MODE} / depth ${DEPTH})"
echo " ══════════════════════════════════════════════════════════════"
echo ""
while IFS= read -r unit; do
case "$FORMAT" in
tree)
echo -e "${CYAN}${unit}${RESET}"
output_tree "$unit" "$MODE"
;;
flat)
echo -e "# ${unit}"
output_flat "$unit" "$MODE"
echo ""
;;
dot)
output_dot "$unit" "$MODE"
;;
json)
output_json "$unit" "$MODE"
;;
esac
done <<< "$units"
}
# ══════════════════════════════════════════════════════════════════════
# SINGLE SERVICE MODE
# ══════════════════════════════════════════════════════════════════════
run_single() {
local unit
unit="$(normalize_unit "$SERVICE")"
# Verify the unit exists
local load_state
load_state="$(get_load_state "$unit")"
if [[ "$load_state" == "not-found" ]]; then
err "Unit '${unit}' not found"
exit 1
fi
case "$FORMAT" in
tree)
echo ""
echo -e "${BOLD}Service Dependency Map — ${unit}${RESET}"
echo -e "${DIM}$(date -u '+%Y-%m-%d %H:%M:%S UTC')${RESET}"
print_service_info "$unit"
if [[ "$MODE" == "forward" || "$MODE" == "both" ]]; then
output_tree "$unit" "forward"
fi
if [[ "$MODE" == "reverse" || "$MODE" == "both" ]]; then
output_tree "$unit" "reverse"
fi
# Always show both directions for single-service tree mode
if [[ "$MODE" == "forward" ]]; then
output_tree "$unit" "reverse"
elif [[ "$MODE" == "reverse" ]]; then
output_tree "$unit" "forward"
fi
print_impact_summary "$unit"
;;
flat)
output_flat "$unit" "$MODE"
;;
dot)
output_dot "$unit" "$MODE"
;;
json)
output_json "$unit" "$MODE"
;;
esac
}
# ══════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════
main() {
parse_args "$@"
setup_colors
if [[ "$CRITICAL" == "true" ]]; then
run_critical
elif [[ "$MAP_ALL" == "true" ]]; then
run_all
else
run_single
fi
}
main "$@"