Files
securityonion/salt/soc/files/bin/salt-relay.sh
Corey Ogburn ad28ea275f Better state management
When salt-cp runs it's course and finds it can't send a file, it outputs a report saying as much but the exit code will be zero. Now we remove the filename and node from the response and look for `True` to know if it succeeded. Also, respect the cleanup flag on success or failure.

Check the status of the decryption process before importing.

No longer decrypt locally, issue salt command for the remote client to do the decrypting.
2023-06-20 09:41:14 -06:00

305 lines
8.5 KiB
Bash
Executable File

#!/bin/bash
# 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
# https://securityonion.net/license; you may not use this file except in compliance with the
# Elastic License 2.0.
PIPE_OWNER=${PIPE_OWNER:-socore}
PIPE_GROUP=${PIPE_GROUP:-socore}
SOC_PIPE=${SOC_PIPE:-/opt/so/conf/soc/salt/pipe}
CMD_PREFIX=${CMD_PREFIX:-""}
PATH=${PATH}:/usr/sbin
function log() {
echo "$(date) | $1"
}
function make_pipe() {
path=$1
log "Creating pipe: $path"
rm -f "${path}"
mkfifo "${path}"
chmod 0660 "${path}"
chown ${PIPE_OWNER}:${PIPE_GROUP} "${path}"
}
make_pipe "${SOC_PIPE}"
function list_minions() {
response=$($CMD_PREFIX so-minion -o=list)
exit_code=$?
if [[ $exit_code -eq 0 ]]; then
log "Successful command execution"
$(echo "$response" > "${SOC_PIPE}")
else
log "Unsuccessful command execution: $exit_code"
$(echo "false" > "${SOC_PIPE}")
fi
}
function manage_minion() {
request=$1
op=$(echo "$request" | jq -r .operation)
id=$(echo "$request" | jq -r .id)
response=$($CMD_PREFIX so-minion "-o=$op" "-m=$id")
exit_code=$?
if [[ exit_code -eq 0 ]]; then
log "Successful command execution"
$(echo "true" > "${SOC_PIPE}")
else
log "Unsuccessful command execution: $response ($exit_code)"
$(echo "false" > "${SOC_PIPE}")
fi
}
function manage_user() {
request=$1
op=$(echo "$request" | jq -r .operation)
max_tries=10
tries=0
while [[ $tries -lt $max_tries ]]; do
case "$op" in
add)
email=$(echo "$request" | jq -r .email)
password=$(echo "$request" | jq -r .password)
role=$(echo "$request" | jq -r .role)
firstName=$(echo "$request" | jq -r .firstName)
lastName=$(echo "$request" | jq -r .lastName)
note=$(echo "$request" | jq -r .note)
log "Performing user '$op' for user '$email' with firstname '$firstName', lastname '$lastName', note '$note' and role '$role'"
response=$(echo "$password" | $CMD_PREFIX so-user "$op" --email "$email" --firstName "$firstName" --lastName "$lastName" --note "$note" --role "$role" --skip-sync)
exit_code=$?
;;
add|enable|disable|delete)
email=$(echo "$request" | jq -r .email)
log "Performing user '$op' for user '$email'"
response=$($CMD_PREFIX so-user "$op" --email "$email" --skip-sync)
exit_code=$?
;;
addrole|delrole)
email=$(echo "$request" | jq -r .email)
role=$(echo "$request" | jq -r .role)
log "Performing '$op' for user '$email' with role '$role'"
response=$($CMD_PREFIX so-user "$op" --email "$email" --role "$role" --skip-sync)
exit_code=$?
;;
password)
email=$(echo "$request" | jq -r .email)
password=$(echo "$request" | jq -r .password)
log "Performing '$op' operation for user '$email'"
response=$(echo "$password" | so-user "$op" --email "$email" --skip-sync)
exit_code=$?
;;
profile)
email=$(echo "$request" | jq -r .email)
firstName=$(echo "$request" | jq -r .firstName)
lastName=$(echo "$request" | jq -r .lastName)
note=$(echo "$request" | jq -r .note)
log "Performing '$op' update for user '$email' with firstname '$firstName', lastname '$lastName', and note '$note'"
response=$($CMD_PREFIX so-user "$op" --email "$email" --firstName "$firstName" --lastName "$lastName" --note "$note")
exit_code=$?
;;
sync)
log "Performing '$op'"
response=$($CMD_PREFIX so-user "$op")
exit_code=$?
;;
*)
response="Unsupported user operation: $op"
exit_code=1
;;
esac
tries=$((tries+1))
if [[ "$response" == "Another process is using so-user"* ]]; then
log "Retrying after brief delay to let so-user unlock ($tries/$max_tries)"
sleep 5
else
break
fi
done
if [[ exit_code -eq 0 ]]; then
log "Successful command execution: $response"
$(echo "true" > "${SOC_PIPE}")
else
log "Unsuccessful command execution: $response ($exit_code)"
$(echo "false" > "${SOC_PIPE}")
fi
}
function manage_salt() {
request=$1
op=$(echo "$request" | jq -r .operation)
minion=$(echo "$request" | jq -r .minion)
if [[ -s $minion || "$minion" == "null" ]]; then
minion=$(cat /etc/salt/minion | grep "id:" | awk '{print $2}' | sed "s/'//g")
fi
case "$op" in
state)
log "Performing '$op' for '$state' on minion '$minion'"
state=$(echo "$request" | jq -r .state)
response=$($CMD_PREFIX salt --async "$minion" state.apply "$state" queue=2)
exit_code=$?
;;
highstate)
log "Performing '$op' on minion $minion"
response=$($CMD_PREFIX salt --async "$minion" state.highstate queue=2)
exit_code=$?
;;
activejobs)
response=$($CMD_PREFIX salt-run jobs.active -out json -l quiet)
log "Querying active salt jobs"
$(echo "$response" > "${SOC_PIPE}")
return
;;
*)
response="Unsupported salt operation: $op"
exit_code=1
;;
esac
if [[ exit_code -eq 0 ]]; then
log "Successful command execution: $response"
$(echo "true" > "${SOC_PIPE}")
else
log "Unsuccessful command execution: $response ($exit_code)"
$(echo "false" > "${SOC_PIPE}")
fi
}
function send_file() {
request=$1
from=$(echo "$request" | jq -r .from)
to=$(echo "$request" | jq -r .to)
node=$(echo "$request" | jq -r .node)
[ $(echo "$request" | jq -r .cleanup) != "true" ] ; cleanup=$?
log "From: $from"
log "To: $to"
log "Node: $node"
log "Cleanup: $cleanup"
log "encrypting..."
gpg --passphrase "infected" --batch --symmetric --cipher-algo AES256 "$from"
fromgpg="$from.gpg"
filename=$(basename "$fromgpg")
log "sending..."
response=$($CMD_PREFIX salt-cp -C "$node" "$fromgpg" "$to")
# salt-cp returns 0 even if the file transfer fails, so we need to check the response.
# Remove the node and filename from the response on the off-chance they contain
# the word "True" in them
echo $response | sed "s/$node//" | sed "s/$filename//" | grep True
exit_code=$?
rm -f "$fromgpg"
log Response:$'\n'"$response"
log "Exit Code: $exit_code"
if [[ $cleanup -eq 1 ]]; then
log "Cleaning up file $from"
rm -f "$from"
fi
if [[ exit_code -eq 0 ]]; then
$(echo "true" > "${SOC_PIPE}")
else
$(echo "false" > "${SOC_PIPE}")
fi
}
function import_file() {
request=$1
node=$(echo "$request" | jq -r .node)
file=$(echo "$request" | jq -r .file)
importer=$(echo "$request" | jq -r .importer)
log "Node: $node"
log "File: $file"
log "Importer: $importer"
filegpg="$file.gpg"
log "decrypting..."
$CMD_PREFIX "salt '$node' cmd.run 'gpg --passphrase \"infected\" --batch --decrypt \"$filegpg\" > \"$file\"'"
decrypt_code=$?
if [[ $decrypt_code -eq 0 ]]; then
log "importing..."
case $importer in
pcap)
response=$($CMD_PREFIX "salt '$node' cmd.run 'so-import-pcap $file --json'")
exit_code=$?
;;
evtx)
response=$($CMD_PREFIX "salt '$node' cmd.run 'so-import-evtx $file --json'")
exit_code=$?
;;
*)
response="Unsupported importer: $importer"
exit_code=1
;;
esac
else
response="Failed to decrypt file: $file"
exit_code=$decrypt_code
fi
rm -f "$file" "$filegpg"
log Response:$'\n'"$response"
log "Exit Code: $exit_code"
if [[ exit_code -eq 0 ]]; then
# trim off the node header ("manager_standalone:\n") and parse out the URL
url=$(echo "$response" | tail -n +2 | jq -r .url)
$(echo "$url" > "${SOC_PIPE}")
else
log "false"
$(echo "false" > "${SOC_PIPE}")
fi
}
while true; do
log "Listening for request"
request=$(cat ${SOC_PIPE})
if [[ "$request" != "" ]]; then
command=$(echo "$request" | jq -r .command)
log "Received request; command=${command}"
case "$command" in
list-minions)
list_minions
;;
manage-minion)
manage_minion "${request}"
;;
manage-user)
manage_user "${request}"
;;
manage-salt)
manage_salt "${request}"
;;
send-file)
send_file "${request}"
;;
import-file)
import_file "${request}"
;;
*)
log "Unsupported command: $command"
$(echo "false" > "${SOC_PIPE}")
;;
esac
# allow remote reader to get a clean reader before we try to read again on next loop
sleep 1
fi
done