#!/bin/bash set -e if ! command -v curl &>/dev/null; then printf "%s\n" "[err] curl not found" exit 1 fi function usage() { printf "%s\n" \ "set up command canaries for early intrusion detection via webhook notifications" \ "usage: $(basename ${0}) " \ "" \ "options" \ "-c commands to monitor; repeatable, e.g., -c whoami -c id" \ "-s notification service: discord|slack" \ "-u webhook url" \ "-m http method, default POST" \ "-x
webhook headers; repeatable, e.g., -x 'key1: value1' -x 'key2: value2'" \ "-t channel or destination id for notifications" \ "-h print this help message and exit" \ "" \ "examples" \ "discord" \ "$(basename ${0}) -s discord -c whoami -c id -u https://discord.com/api/webhooks/xxx" \ "" \ "slack" \ "$(basename ${0}) -s slack -c whoami -c id -u https://slack.com/api/chat.postmessage -t mychannel -x 'authorization: bearer xoxb-a-b-c'" \ "" \ "output" \ "generates canaries.txt with shell functions to append to /etc/bash.bashrc or ~/.bashrc." exit 1 } function validate_url() { local url="${1}" if ! [[ "${url}" =~ ^https:// ]]; then printf "%s\n" "[err] invalid url: ${url} (must be https)" exit 1 fi } function validate_method() { local method="${1}" if ! [[ "${method}" =~ ^(GET|POST|PUT)$ ]]; then printf "[err] invalid http method: ${method} (use GET, POST, or PUT)" exit 1 fi } function validate_command() { local cmd="${1}" if ! command -v "${cmd}" &>/dev/null; then printf "[wrn] command '${cmd}' not found in path" fi } output_file="canaries.txt" method="POST" commands=() webhook_headers=() channel="" while getopts "c:s:u:m:x:t:h" opts; do case "${opts}" in c) commands+=("${OPTARG}");; s) service="${OPTARG}";; u) webhook_url="${OPTARG}";; m) method="${OPTARG}";; x) webhook_headers+=("${OPTARG}");; t) channel="${OPTARG}";; h) usage;; *) usage;; esac done if test ${#commands[@]} -eq 0; then printf "%s\n" "[err] at least one command (-c) is required" usage fi if ! test "${webhook_url}" && ! test "${service}"; then printf "%s\n" "[err] webhook url (-u) or service (-s) is required" usage fi validate_method "${method}" for cmd in "${commands[@]}"; do validate_command "${cmd}" done test "${webhook_url}" && validate_url "${webhook_url}" case "${service}" in discord) webhook_url="${webhook_url:-}" webhook_headers+=("Content-Type: application/json") webhook_data="{\"username\":\"notifications-bot\", \"content\":\"**Command Canary Triggered**\nHostname: \${HOSTNAME}\nCommand: \${FUNCNAME[0]}\nTime: \$(date -u --iso-8601=seconds)\"}" ;; slack) webhook_headers+=("Content-Type: application/json") ! test "${channel}" || { printf "%s\n" "[err] slack requires a channel (-t)"; exit 1; } webhook_data="{\"text\":\"**Command Canary Triggered**\\nHostname: \${HOSTNAME}\\nCommand: \${FUNCNAME[0]}\\nTime: \$(date -u --iso-8601=seconds)\",\"channel\":\"${channel}\"}" ;; *) usage ;; esac curl_headers="" for header in "${webhook_headers[@]}"; do curl_headers+="-H '${header}' " done >"${output_file}" for cmd in "${commands[@]}"; do sanitized_data=$(printf "%s" "${webhook_data}" | sed -e 's/"/\\"/g') cat >> "${output_file}" << EOF function ${cmd}() { ( $(which curl) -sSkL \\ -X ${method} \\ ${curl_headers} \\ --data "${sanitized_data}" \\ ${webhook_url} \\ -o /dev/null 2>&1 >/dev/null & ) command ${cmd} "\${@}" } EOF done printf "%s\n" \ "[inf] successfully created ${output_file}" \ "[inf] to enable canaries, append the contents to:" \ " - System-wide: /etc/bash.bashrc" \ " - User-specific: ~/.bashrc"