a1a17e81a1
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.
576 lines
20 KiB
Bash
576 lines
20 KiB
Bash
#!/bin/bash
|
|
|
|
#############################################################
|
|
#### Audit Log Analyzer Script for SELinux and AppArmor ####
|
|
#### Parses denial logs and suggests fix commands ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version 1.00 ####
|
|
#### ####
|
|
#### To use this script chmod it to 755 ####
|
|
#### or simply type bash <filename.sh> ####
|
|
#############################################################
|
|
|
|
# ── Colors ────────────────────────────────────────────────
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# ── Defaults ──────────────────────────────────────────────
|
|
MODE="recent"
|
|
OUTPUT_FILE=""
|
|
QUIET=0
|
|
TOTAL_DENIALS=0
|
|
UNIQUE_TYPES=0
|
|
SUGGESTED_FIXES=0
|
|
|
|
# ── Functions ─────────────────────────────────────────────
|
|
|
|
usage() {
|
|
echo -e "${BOLD}Audit Log Analyzer — SELinux & AppArmor${NC}"
|
|
echo ""
|
|
echo "Usage: $0 [OPTIONS]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --help Show this help message"
|
|
echo " --recent Analyze denials from the last hour only (default)"
|
|
echo " --all Analyze all denials in the log"
|
|
echo " --output FILE Save suggested fixes to FILE"
|
|
echo " --quiet Show suggestions only, suppress raw denial lines"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " sudo bash $0 --recent"
|
|
echo " sudo bash $0 --all --output fixes.txt"
|
|
echo " sudo bash $0 --quiet --output /tmp/fixes.txt"
|
|
exit 0
|
|
}
|
|
|
|
check_root() {
|
|
if [[ $EUID -ne 0 ]]; then
|
|
echo -e "${RED}Error: This script must be run as root.${NC}"
|
|
echo "Please run with: sudo bash $0"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
detect_mac_system() {
|
|
SELINUX_ACTIVE=0
|
|
APPARMOR_ACTIVE=0
|
|
|
|
# Check SELinux
|
|
if command -v getenforce &>/dev/null; then
|
|
SELINUX_STATUS=$(getenforce 2>/dev/null)
|
|
if [[ "$SELINUX_STATUS" == "Enforcing" || "$SELINUX_STATUS" == "Permissive" ]]; then
|
|
SELINUX_ACTIVE=1
|
|
fi
|
|
fi
|
|
|
|
# Check AppArmor
|
|
if command -v aa-status &>/dev/null; then
|
|
if aa-status &>/dev/null; then
|
|
APPARMOR_ACTIVE=1
|
|
fi
|
|
elif [[ -d /sys/module/apparmor ]]; then
|
|
APPARMOR_ACTIVE=1
|
|
fi
|
|
|
|
if [[ $SELINUX_ACTIVE -eq 0 && $APPARMOR_ACTIVE -eq 0 ]]; then
|
|
echo -e "${YELLOW}Warning: Neither SELinux nor AppArmor appears to be active on this system.${NC}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
output_line() {
|
|
local line="$1"
|
|
echo -e "$line"
|
|
if [[ -n "$OUTPUT_FILE" ]]; then
|
|
echo -e "$line" | sed 's/\x1b\[[0-9;]*m//g' >> "$OUTPUT_FILE"
|
|
fi
|
|
}
|
|
|
|
# ── SELinux Analysis ──────────────────────────────────────
|
|
|
|
parse_selinux_denial() {
|
|
local line="$1"
|
|
|
|
local scontext tcontext tclass perm comm name path
|
|
|
|
scontext=$(echo "$line" | grep -oP 'scontext=\K[^ ]+')
|
|
tcontext=$(echo "$line" | grep -oP 'tcontext=\K[^ ]+')
|
|
tclass=$(echo "$line" | grep -oP 'tclass=\K[^ ]+')
|
|
perm=$(echo "$line" | grep -oP '\{ \K[^}]+')
|
|
comm=$(echo "$line" | grep -oP 'comm="\K[^"]+')
|
|
name=$(echo "$line" | grep -oP 'name="\K[^"]+')
|
|
path=$(echo "$line" | grep -oP 'path="\K[^"]+')
|
|
|
|
if [[ $QUIET -eq 0 ]]; then
|
|
output_line "${RED}DENIAL:${NC} $line"
|
|
fi
|
|
|
|
output_line "${CYAN} Source context:${NC} $scontext"
|
|
output_line "${CYAN} Target context:${NC} $tcontext"
|
|
output_line "${CYAN} Class:${NC} $tclass"
|
|
output_line "${CYAN} Permission:${NC} $perm"
|
|
[[ -n "$comm" ]] && output_line "${CYAN} Command:${NC} $comm"
|
|
[[ -n "$path" ]] && output_line "${CYAN} Path:${NC} $path"
|
|
|
|
suggest_selinux_fix "$scontext" "$tcontext" "$tclass" "$perm" "$path" "$name"
|
|
output_line ""
|
|
}
|
|
|
|
suggest_selinux_fix() {
|
|
local scontext="$1" tcontext="$2" tclass="$3" perm="$4" path="$5" name="$6"
|
|
|
|
((SUGGESTED_FIXES++))
|
|
|
|
# Port binding denials
|
|
if [[ "$tclass" == "tcp_socket" || "$tclass" == "udp_socket" ]]; then
|
|
local stype
|
|
stype=$(echo "$scontext" | cut -d: -f3)
|
|
output_line "${GREEN} Suggested fix (port rule):${NC}"
|
|
output_line "${GREEN} semanage port -a -t ${stype} -p tcp <PORT_NUMBER>${NC}"
|
|
output_line "${YELLOW} (Replace <PORT_NUMBER> with the actual port)${NC}"
|
|
return
|
|
fi
|
|
|
|
# File access denials
|
|
if [[ -n "$path" && ("$tclass" == "file" || "$tclass" == "dir" || "$tclass" == "lnk_file") ]]; then
|
|
local ttype
|
|
ttype=$(echo "$tcontext" | cut -d: -f3)
|
|
output_line "${GREEN} Suggested fix (file context):${NC}"
|
|
output_line "${GREEN} semanage fcontext -a -t ${ttype} \"${path}\"${NC}"
|
|
output_line "${GREEN} restorecon -Rv \"${path}\"${NC}"
|
|
|
|
# Also check for boolean solutions
|
|
suggest_selinux_boolean "$scontext" "$tcontext" "$tclass" "$perm"
|
|
return
|
|
fi
|
|
|
|
# General boolean suggestion
|
|
suggest_selinux_boolean "$scontext" "$tcontext" "$tclass" "$perm"
|
|
}
|
|
|
|
suggest_selinux_boolean() {
|
|
local scontext="$1" tcontext="$2" tclass="$3" perm="$4"
|
|
local stype
|
|
stype=$(echo "$scontext" | cut -d: -f3)
|
|
|
|
# Try to find relevant booleans
|
|
if command -v getsebool &>/dev/null; then
|
|
local booleans
|
|
booleans=$(getsebool -a 2>/dev/null | grep -i "${stype%%_t}" | head -5)
|
|
if [[ -n "$booleans" ]]; then
|
|
output_line "${GREEN} Possibly relevant booleans:${NC}"
|
|
while IFS= read -r bool_line; do
|
|
local bool_name
|
|
bool_name=$(echo "$bool_line" | cut -d' ' -f1)
|
|
output_line "${GREEN} setsebool -P ${bool_name} on${NC}"
|
|
done <<< "$booleans"
|
|
fi
|
|
fi
|
|
|
|
output_line "${YELLOW} If no boolean applies, consider generating a custom policy module (see below).${NC}"
|
|
}
|
|
|
|
categorize_selinux_denial() {
|
|
local line="$1"
|
|
local tclass
|
|
tclass=$(echo "$line" | grep -oP 'tclass=\K[^ ]+')
|
|
|
|
case "$tclass" in
|
|
file|dir|lnk_file|fifo_file|sock_file)
|
|
echo "file_access"
|
|
;;
|
|
tcp_socket|udp_socket|rawip_socket|netlink_socket)
|
|
echo "network"
|
|
;;
|
|
*_port_t)
|
|
echo "port_binding"
|
|
;;
|
|
process|process2)
|
|
echo "process"
|
|
;;
|
|
*)
|
|
echo "other"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
analyze_selinux() {
|
|
output_line "${BOLD}═══════════════════════════════════════════════════${NC}"
|
|
output_line "${BOLD} SELinux Audit Log Analysis${NC}"
|
|
output_line "${BOLD}═══════════════════════════════════════════════════${NC}"
|
|
output_line ""
|
|
|
|
local selinux_status
|
|
selinux_status=$(getenforce 2>/dev/null)
|
|
output_line "${CYAN}SELinux status:${NC} $selinux_status"
|
|
output_line ""
|
|
|
|
# Gather denials
|
|
local denials=""
|
|
|
|
if [[ "$MODE" == "recent" ]]; then
|
|
if command -v ausearch &>/dev/null; then
|
|
denials=$(ausearch -m avc -ts recent 2>/dev/null | grep "type=AVC")
|
|
fi
|
|
# Fallback to log file
|
|
if [[ -z "$denials" && -f /var/log/audit/audit.log ]]; then
|
|
local one_hour_ago
|
|
one_hour_ago=$(date -d '1 hour ago' '+%s' 2>/dev/null)
|
|
if [[ -n "$one_hour_ago" ]]; then
|
|
denials=$(awk -v cutoff="$one_hour_ago" '
|
|
/type=AVC/ {
|
|
match($0, /msg=audit\(([0-9]+)\./, arr)
|
|
if (arr[1] >= cutoff) print
|
|
}
|
|
' /var/log/audit/audit.log)
|
|
fi
|
|
fi
|
|
else
|
|
if [[ -f /var/log/audit/audit.log ]]; then
|
|
denials=$(grep "type=AVC" /var/log/audit/audit.log)
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$denials" ]]; then
|
|
output_line "${GREEN}No AVC denials found.${NC}"
|
|
output_line ""
|
|
return
|
|
fi
|
|
|
|
# Group denials by category
|
|
declare -A categories
|
|
local denial_count=0
|
|
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
((denial_count++))
|
|
local category
|
|
category=$(categorize_selinux_denial "$line")
|
|
categories["$category"]+="$line"$'\n'
|
|
done <<< "$denials"
|
|
|
|
TOTAL_DENIALS=$denial_count
|
|
|
|
# Count unique types
|
|
local unique
|
|
unique=$(echo "$denials" | grep -oP 'tclass=\K[^ ]+' | sort -u | wc -l)
|
|
UNIQUE_TYPES=$unique
|
|
|
|
# Display grouped results
|
|
for category in "file_access" "network" "port_binding" "process" "other"; do
|
|
if [[ -n "${categories[$category]}" ]]; then
|
|
local label
|
|
case "$category" in
|
|
file_access) label="File Access Denials" ;;
|
|
network) label="Network Denials" ;;
|
|
port_binding) label="Port Binding Denials" ;;
|
|
process) label="Process Denials" ;;
|
|
other) label="Other Denials" ;;
|
|
esac
|
|
|
|
output_line "${BOLD}── ${label} ──────────────────────────────────${NC}"
|
|
output_line ""
|
|
|
|
while IFS= read -r denial_line; do
|
|
[[ -z "$denial_line" ]] && continue
|
|
parse_selinux_denial "$denial_line"
|
|
done <<< "${categories[$category]}"
|
|
fi
|
|
done
|
|
|
|
# Generate policy module suggestion with audit2allow
|
|
if command -v audit2allow &>/dev/null; then
|
|
output_line "${BOLD}── Policy Module Suggestion ──────────────────────${NC}"
|
|
output_line ""
|
|
local policy
|
|
if [[ "$MODE" == "recent" ]]; then
|
|
policy=$(ausearch -m avc -ts recent 2>/dev/null | audit2allow 2>/dev/null)
|
|
else
|
|
policy=$(audit2allow < /var/log/audit/audit.log 2>/dev/null)
|
|
fi
|
|
|
|
if [[ -n "$policy" ]]; then
|
|
output_line "${GREEN}audit2allow suggests the following policy:${NC}"
|
|
output_line "$policy"
|
|
output_line ""
|
|
output_line "${YELLOW}To create and install a custom module:${NC}"
|
|
output_line "${GREEN} ausearch -m avc -ts recent | audit2allow -M my_custom_policy${NC}"
|
|
output_line "${GREEN} semodule -i my_custom_policy.pp${NC}"
|
|
else
|
|
output_line "${CYAN}No policy suggestions generated by audit2allow.${NC}"
|
|
fi
|
|
output_line ""
|
|
else
|
|
output_line "${YELLOW}Note: Install audit2allow (policycoreutils-python-utils) for automatic policy generation.${NC}"
|
|
output_line ""
|
|
fi
|
|
}
|
|
|
|
# ── AppArmor Analysis ────────────────────────────────────
|
|
|
|
find_apparmor_log_source() {
|
|
if [[ -f /var/log/syslog ]]; then
|
|
echo "syslog"
|
|
elif [[ -f /var/log/kern.log ]]; then
|
|
echo "kern.log"
|
|
elif command -v journalctl &>/dev/null; then
|
|
echo "journalctl"
|
|
else
|
|
echo "none"
|
|
fi
|
|
}
|
|
|
|
parse_apparmor_denial() {
|
|
local line="$1"
|
|
|
|
local profile operation denied_mask path info
|
|
|
|
profile=$(echo "$line" | grep -oP 'profile="\K[^"]+')
|
|
[[ -z "$profile" ]] && profile=$(echo "$line" | grep -oP 'apparmor="\K[^"]+')
|
|
operation=$(echo "$line" | grep -oP 'operation="\K[^"]+')
|
|
denied_mask=$(echo "$line" | grep -oP 'requested_mask="\K[^"]+')
|
|
[[ -z "$denied_mask" ]] && denied_mask=$(echo "$line" | grep -oP 'denied_mask="\K[^"]+')
|
|
path=$(echo "$line" | grep -oP 'name="\K[^"]+')
|
|
info=$(echo "$line" | grep -oP 'info="\K[^"]+')
|
|
|
|
if [[ $QUIET -eq 0 ]]; then
|
|
output_line "${RED}DENIAL:${NC} $line"
|
|
fi
|
|
|
|
[[ -n "$profile" ]] && output_line "${CYAN} Profile:${NC} $profile"
|
|
[[ -n "$operation" ]] && output_line "${CYAN} Operation:${NC} $operation"
|
|
[[ -n "$path" ]] && output_line "${CYAN} Path:${NC} $path"
|
|
[[ -n "$denied_mask" ]] && output_line "${CYAN} Denied mask:${NC} $denied_mask"
|
|
[[ -n "$info" ]] && output_line "${CYAN} Info:${NC} $info"
|
|
|
|
suggest_apparmor_fix "$profile" "$operation" "$path" "$denied_mask"
|
|
output_line ""
|
|
}
|
|
|
|
suggest_apparmor_fix() {
|
|
local profile="$1" operation="$2" path="$3" denied_mask="$4"
|
|
|
|
((SUGGESTED_FIXES++))
|
|
|
|
# Build the permission string from the denied mask
|
|
local perm_str=""
|
|
case "$denied_mask" in
|
|
r) perm_str="r" ;;
|
|
w) perm_str="w" ;;
|
|
rw) perm_str="rw" ;;
|
|
x) perm_str="ix" ;;
|
|
rx) perm_str="rix" ;;
|
|
rwx) perm_str="rwix" ;;
|
|
k) perm_str="k" ;;
|
|
l) perm_str="l" ;;
|
|
m) perm_str="m" ;;
|
|
*) perm_str="$denied_mask" ;;
|
|
esac
|
|
|
|
if [[ -n "$path" && -n "$perm_str" ]]; then
|
|
output_line "${GREEN} Suggested rule to add to profile:${NC}"
|
|
output_line "${GREEN} ${path} ${perm_str},${NC}"
|
|
fi
|
|
|
|
# Show the profile file path
|
|
if [[ -n "$profile" ]]; then
|
|
local profile_file="/etc/apparmor.d/${profile//\//.}"
|
|
# Try to find the actual profile file
|
|
if [[ -f "/etc/apparmor.d/$profile" ]]; then
|
|
profile_file="/etc/apparmor.d/$profile"
|
|
elif [[ -f "/etc/apparmor.d/${profile//\//.}" ]]; then
|
|
profile_file="/etc/apparmor.d/${profile//\//.}"
|
|
else
|
|
# Search for it
|
|
local found
|
|
found=$(grep -rl "profile $profile" /etc/apparmor.d/ 2>/dev/null | head -1)
|
|
[[ -n "$found" ]] && profile_file="$found"
|
|
fi
|
|
output_line "${CYAN} Profile file:${NC} $profile_file"
|
|
fi
|
|
|
|
output_line "${YELLOW} Or run interactively:${NC}"
|
|
output_line "${GREEN} aa-logprof${NC}"
|
|
}
|
|
|
|
analyze_apparmor() {
|
|
output_line "${BOLD}═══════════════════════════════════════════════════${NC}"
|
|
output_line "${BOLD} AppArmor Audit Log Analysis${NC}"
|
|
output_line "${BOLD}═══════════════════════════════════════════════════${NC}"
|
|
output_line ""
|
|
|
|
# Show AppArmor status
|
|
if command -v aa-status &>/dev/null; then
|
|
local enforced loaded
|
|
enforced=$(aa-status 2>/dev/null | grep -c "enforce")
|
|
loaded=$(aa-status 2>/dev/null | grep -c "loaded")
|
|
output_line "${CYAN}AppArmor profiles loaded:${NC} $loaded"
|
|
output_line "${CYAN}Profiles in enforce mode:${NC} $enforced"
|
|
output_line ""
|
|
fi
|
|
|
|
# Find log source
|
|
local log_source
|
|
log_source=$(find_apparmor_log_source)
|
|
|
|
if [[ "$log_source" == "none" ]]; then
|
|
output_line "${RED}Error: Cannot find AppArmor log source.${NC}"
|
|
output_line "${YELLOW}Checked: /var/log/syslog, /var/log/kern.log, journalctl${NC}"
|
|
return
|
|
fi
|
|
|
|
# Gather denials
|
|
local denials=""
|
|
|
|
if [[ "$log_source" == "journalctl" ]]; then
|
|
if [[ "$MODE" == "recent" ]]; then
|
|
denials=$(journalctl --since "1 hour ago" --no-pager 2>/dev/null | grep -i "apparmor.*DENIED")
|
|
else
|
|
denials=$(journalctl --no-pager 2>/dev/null | grep -i "apparmor.*DENIED")
|
|
fi
|
|
else
|
|
local log_file
|
|
[[ "$log_source" == "syslog" ]] && log_file="/var/log/syslog"
|
|
[[ "$log_source" == "kern.log" ]] && log_file="/var/log/kern.log"
|
|
|
|
if [[ "$MODE" == "recent" ]]; then
|
|
local one_hour_ago
|
|
one_hour_ago=$(date -d '1 hour ago' '+%b %e %H:%M' 2>/dev/null)
|
|
if [[ -n "$one_hour_ago" ]]; then
|
|
denials=$(awk -v cutoff="$(date -d '1 hour ago' '+%s' 2>/dev/null)" '
|
|
/apparmor.*DENIED/ || /apparmor.*denied/ {
|
|
print
|
|
}
|
|
' "$log_file" | tail -100)
|
|
else
|
|
# Fallback: last 100 denial lines
|
|
denials=$(grep -i "apparmor.*DENIED" "$log_file" | tail -100)
|
|
fi
|
|
else
|
|
denials=$(grep -i "apparmor.*DENIED" "$log_file")
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$denials" ]]; then
|
|
output_line "${GREEN}No AppArmor denials found.${NC}"
|
|
output_line ""
|
|
return
|
|
fi
|
|
|
|
local denial_count=0
|
|
local -A seen_profiles
|
|
|
|
output_line "${BOLD}── AppArmor Denials ─────────────────────────────${NC}"
|
|
output_line ""
|
|
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
((denial_count++))
|
|
parse_apparmor_denial "$line"
|
|
|
|
local p
|
|
p=$(echo "$line" | grep -oP 'profile="\K[^"]+')
|
|
[[ -n "$p" ]] && seen_profiles["$p"]=1
|
|
done <<< "$denials"
|
|
|
|
TOTAL_DENIALS=$denial_count
|
|
UNIQUE_TYPES=${#seen_profiles[@]}
|
|
|
|
# Suggest aa-logprof for interactive fixing
|
|
output_line "${BOLD}── Interactive Fix Suggestion ────────────────────${NC}"
|
|
output_line ""
|
|
output_line "${YELLOW}For interactive profile updates, run:${NC}"
|
|
output_line "${GREEN} aa-logprof${NC}"
|
|
output_line ""
|
|
output_line "${YELLOW}To set a profile to complain mode for testing:${NC}"
|
|
for prof in "${!seen_profiles[@]}"; do
|
|
output_line "${GREEN} aa-complain $prof${NC}"
|
|
done
|
|
output_line ""
|
|
}
|
|
|
|
# ── Summary ───────────────────────────────────────────────
|
|
|
|
print_summary() {
|
|
output_line "${BOLD}═══════════════════════════════════════════════════${NC}"
|
|
output_line "${BOLD} Summary${NC}"
|
|
output_line "${BOLD}═══════════════════════════════════════════════════${NC}"
|
|
output_line ""
|
|
output_line " Total denials found: ${BOLD}${TOTAL_DENIALS}${NC}"
|
|
output_line " Unique denial types: ${BOLD}${UNIQUE_TYPES}${NC}"
|
|
output_line " Suggested fixes: ${BOLD}${SUGGESTED_FIXES}${NC}"
|
|
output_line ""
|
|
|
|
if [[ -n "$OUTPUT_FILE" ]]; then
|
|
output_line "${GREEN}Suggestions saved to: ${OUTPUT_FILE}${NC}"
|
|
output_line ""
|
|
fi
|
|
}
|
|
|
|
# ── Parse Arguments ───────────────────────────────────────
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--help|-h)
|
|
usage
|
|
;;
|
|
--recent)
|
|
MODE="recent"
|
|
shift
|
|
;;
|
|
--all)
|
|
MODE="all"
|
|
shift
|
|
;;
|
|
--output)
|
|
if [[ -z "$2" || "$2" == --* ]]; then
|
|
echo -e "${RED}Error: --output requires a filename argument.${NC}"
|
|
exit 1
|
|
fi
|
|
OUTPUT_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--quiet|-q)
|
|
QUIET=1
|
|
shift
|
|
;;
|
|
*)
|
|
echo -e "${RED}Unknown option: $1${NC}"
|
|
echo "Use --help for usage information."
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# ── Main ──────────────────────────────────────────────────
|
|
|
|
check_root
|
|
|
|
# Clear output file if specified
|
|
if [[ -n "$OUTPUT_FILE" ]]; then
|
|
true > "$OUTPUT_FILE"
|
|
fi
|
|
|
|
echo -e "${BOLD}Audit Log Analyzer v1.00${NC}"
|
|
echo -e "${CYAN}Mode: ${MODE}${NC}"
|
|
echo ""
|
|
|
|
detect_mac_system
|
|
|
|
if [[ $SELINUX_ACTIVE -eq 1 ]]; then
|
|
analyze_selinux
|
|
fi
|
|
|
|
if [[ $APPARMOR_ACTIVE -eq 1 ]]; then
|
|
analyze_apparmor
|
|
fi
|
|
|
|
print_summary
|