#!/bin/bash -e
set -o pipefail

#
# A corelight-fleet management provided helper script to disect the user database.
# Please do not take a dependency on any of these datastructures/schemas or this
# report format as it may change, even in minor release versions.
#


DATA_DIR="/var/lib/corelight-fleetd"
ETC_DIR="/etc"
#DATA_DIR="."
#ETC_DIR="."
FILE_ADMIN="${DATA_DIR}/admin"
FILE_SENSORDB="${DATA_DIR}/sensor-catalog"
FILE_SAML_INI="${ETC_DIR}/corelight-fleet-saml.ini"
###################
## SETUP
###################
SCRIPT_COMMAND_LINE_NAME="$0"
PRINT_ERROR=1
error_report() {
   if [ "$PRINT_ERROR" = 1 ]; then
      PRINT_ERROR=0
      echo "" >&2
      echo "Failed to generate access report. Please correct the above issues and try again." >&2
      echo "   * If files are missing, you may have to run the fleet management server once." >&2
      echo "   * If you see a permissions error, you may have to run 'sudo ${SCRIPT_COMMAND_LINE_NAME}'." >&2
      echo "   * If you see an error about sqlite3 or another commmand not found," >&2
      echo "      you may have to install this package from apt/yum." >&2
      echo "   * If you see checksum error, the file affected should be reverted to the original." >&2
      echo "" >&2
      exit 1
   fi
}
trap error_report ERR
DO_CLEANUP=1
TMP_FILE_NAME="`mktemp`"
TMP_DB2_NAME="`mktemp`"
TMP_DB_NAME="`mktemp`"
SCRATCH_FILE_NAME="`mktemp`"
SQLITE_ENV_FILE_NAME="`mktemp`"
cleanup_temp() {
   if [ "$DO_CLEANUP" = "1" ]; then
      for i in "$SQLITE_ENV_FILE_NAME" "$TMP_FILE_NAME"  "$TMP_DB_NAME"  "$TMP_DB2_NAME"  "$SCRATCH_FILE_NAME"; do
         if [ -f "$i" ]; then
            unlink "$i"
         fi
      done
   else
      echo "TMP_FILE_NAME: $TMP_FILE_NAME" >&2
      echo "TMP_DB_NAME: $TMP_DB_NAME" >&2
      echo "TMP_DB2_NAME: $TMP_DB2_NAME" >&2
      echo "SCRATCH_FILE_NAME: $SCRATCH_FILE_NAME" >&2
   fi
   error_report
}
trap cleanup_temp EXIT
output() {
   echo "$1" >>"$TMP_FILE_NAME"
}
output_pipe() {
   cat >>"$TMP_FILE_NAME"
}
output_section_head1() {
   local HEAD="$1"
   output '======================================================'
   output "= $HEAD"
   output '======================================================'
}

output_section_head2() {
   local HEAD="$1"
   output ''
   output "$HEAD"
   output '======================================================'
}
output_section_head3() {
   local HEAD="$1"
   output ''
   output "$HEAD"
   output '------------------------------------------------------'
}
add_line_numbers() {
   local LINE=""
   local LINE_NUM=0
   while read LINE; do
      let "LINE_NUM=LINE_NUM+1"
      echo "${LINE_NUM},$LINE"
   done
}

filter_ini_section() {
   local SECTION="$1"
   local LINE=""
   local LINE_NUM=0
   local CURSECTION=""
   local DO_LINE_COUNT="$2"
   while read LINE; do
      let "LINE_NUM=LINE_NUM+1"
      #echo "L: $LINE"
      if [[ "$LINE" =~ ^([^\;\#]+)[\;\#] ]]; then
         LINE="${BASH_REMATCH[1]}"
         #echo "REMATCH1: $LINE"
      fi
      if [[ "$LINE" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]]; then
         LINE="${BASH_REMATCH[1]}"
         #echo "REMATCH2: $LINE"
      fi

      if [[ "$LINE" =~ ^\[([^\]]+)\] ]]; then
         CURSECTION="${BASH_REMATCH[1]}"
         #echo "SECT: $CURSECTION"
      elif [[ "$LINE" =~ ^([a-zA-Z0-9_\.-]*)[[:space:]]*=[[:space:]]*([a-zA-Z0-9_\.-]*) ]]; then
         local KEY="${BASH_REMATCH[1]}"
         local VAL="${BASH_REMATCH[2]}"
         #echo "KV: $KEY  $VAL"
         if [ "$CURSECTION" = "$SECTION" ]; then
            if [ "$DO_LINE_COUNT" != 1 ]; then
               echo "$KEY,$VAL"
            else
               echo "$LINE_NUM,$KEY,$VAL"
            fi
         fi
      fi
   done
}
print_with_header() {
   local HEADER="$1"
   echo "$HEADER"
   cat
}
print_ini_section() {
   local FILE="$1"
   local SECTION="$2"
   local DO_LINES="$3"
   cat "$FILE" |filter_ini_section "$SECTION"  "$DO_LINES"
}
verify_md5() {
   FILE="$1"
   MD5="$2"
   echo "$2 $1" |md5sum --quiet --check - >&2
}

run_sqlite() {
   local CMD="$1"
   local FULL_CMD="attach '${FILE_ADMIN}' as admin; attach '${FILE_SENSORDB}' as sensordb; attach '$TMP_DB_NAME' as tmpdb; $CMD"
   shift
   if ! sqlite3 $@ -header "$TMP_DB_NAME" "$FULL_CMD" |column  -t -s '|'; then
      echo "error running sqlite3: $FULL_CMD" >&2
      false  #exit
   fi
}

run_sqlite_min() {
   local CMD="$1"
   local FULL_CMD="attach '${FILE_ADMIN}' as admin; attach '${FILE_SENSORDB}' as sensordb; attach '$TMP_DB_NAME' as tmpdb; $CMD"
   shift
   if ! sqlite3 $@ -separator " " -noheader "$TMP_DB_NAME" "$FULL_CMD"; then
      echo "error running sqlite3: $FULL_CMD" >&2
      false  #exit
   fi
}

import_sqlite_pipe() {
   local TABLE_NAME="$1"
   local TABLE_SQL="$2"
   cat >"$SCRATCH_FILE_NAME"
   cat <<ENDOFFILE |sqlite3  "$TMP_DB_NAME"
.mode csv
$TABLE_SQL
.import '$SCRATCH_FILE_NAME' '$TABLE_NAME'
ENDOFFILE
}

###################
## VERIFY
###################

cat <<ENDOFFILE >"$SQLITE_ENV_FILE_NAME"

ENDOFFILE

#print_ini_section "$FILE_SAML_INI" "fleet_roles_to_saml_groups"
#verify_md5 "$FILE_STRUCT" "2739ac6154d5c939ab88901698b20640"

SAML_ENABLED=0
if print_ini_section "$FILE_SAML_INI" "" "" |grep -iq '^saml.enable, *true$'; then
   SAML_ENABLED=1
fi 
#echo $SAML_ENABLED

###################
## IMPORT
###################
if [ "$SAML_ENABLED" = 1 ]; then
# create saml_to_role
#print_ini_section "$FILE_SAML_INI" "fleet_roles_to_idp_groups" "1" |print_with_header "line_number,role,saml"
print_ini_section "$FILE_SAML_INI" "fleet_roles_to_idp_groups" "1" |sed 's/, /,/g' |import_sqlite_pipe "saml_to_role" "CREATE TABLE saml_to_role(line_number TEXT, role TEXT, saml TEXT);"
# create role_to_group
#cat "$FILE_MAPPING" |sed 's#read$#monitor,1#' |sed 's#write$#admin,2#' |add_line_numbers |grep ",.*," |print_with_header "line_number,record_type,role,grp,access,level" |sed 's/, /,/g' 
#cat "$FILE_MAPPING" |sed 's#read$#monitor,1#' |sed 's#write$#admin,2#' |add_line_numbers |grep ",.*," |sed 's/, /,/g' |import_sqlite_pipe "role_to_group" "CREATE TABLE role_to_group(line_number TEXT, record_type TEXT, role TEXT, grp TEXT, access TEXT, level TEXT);"

# create saml_to_group
run_sqlite "create table tmpdb.saml_to_group(line_number TEXT, saml TEXT, grp TEXT, access TEXT, level TEXT)"
run_sqlite "insert into tmpdb.saml_to_group(line_number, saml, grp, access, level) select sr.line_number, sr.saml, rg.grp, '', max(rg.level) from tmpdb.saml_to_role as sr join tmpdb.role_to_group as rg on sr.role = rg.role"

# create the invalid_role table
run_sqlite "create table tmpdb.invalid_role_to_group(line_number TEXT, Issue TEXT, Role TEXT, Group_ID TEXT)"
run_sqlite "insert into tmpdb.invalid_role_to_group(line_number, Issue, Role, Group_ID) select line_number, 'Sensor group not in database', role, grp from tmpdb.role_to_group where not exists ( select 'x' from domain.SensorGroup where domain.SensorGroup.ID = grp )"
run_sqlite "insert into tmpdb.invalid_role_to_group(line_number, Issue, Role, Group_ID) select line_number, 'Role not defined in .ini', role, grp from tmpdb.role_to_group where not exists ( select 'x' from tmpdb.saml_to_role where tmpdb.saml_to_role.role = tmpdb.role_to_group.role )"


# role has no groups
run_sqlite "create table tmpdb.saml_to_role_empty(line_number TEXT, SAML_Group TEXT, Role TEXT)"
run_sqlite "insert into tmpdb.saml_to_role_empty(line_number, SAML_Group, Role) select line_number, saml, role from tmpdb.saml_to_role where role != 'fleet_admin' and role != 'monitor' and role != 'admin' and not exists ( select 'x' from tmpdb.role_to_group where tmpdb.role_to_group.role = tmpdb.saml_to_role.role )"

# mangle saml_to_group
run_sqlite "UPDATE tmpdb.saml_to_group SET access = 'monitor' where level = 1"
run_sqlite "UPDATE tmpdb.saml_to_group SET access = 'admin' where level = 2"
run_sqlite "UPDATE tmpdb.saml_to_group SET access = '<INVALID>' where access = ''"

#run_sqlite 'select * from tmpdb.saml_to_role join tmpdb.role_to_group on tmpdb.saml_to_role.role = tmpdb.role_to_group.role'
fi

###################
## GENERATE
###################

output_section_head1 "Report Generated on `date`"

############
# 1. USERS
############

print_users_from_authenticator() {
   local AUTH="$1"
   local HEAD="$2"
   output_section_head2 "$HEAD"

   output_section_head3 "Users"
   run_sqlite "SELECT user as '   User   ', admin as '   Fleet Admin?   ' FROM (SELECT 0 as primary_order, username as user, fleet_admin as admin FROM admin.users as u WHERE u.authenticator = '$AUTH'  union all select 1, ' ', ' ' ORDER BY primary_order, u.username)" |output_pipe

   output_section_head3 "User Group Mapping"
   run_sqlite "SELECT user as '   User   ', sensor_group_id as '   Group ID   ', name as '   Group Name   ', (case access WHEN 2 then 'fleet admin' WHEN 1 then 'admin' WHEN 0 then 'monitor' ELSE '<??>' END) as '   Access   ' FROM (select sensor_group_id, sg.Name as name, username as user, (case fleet_admin WHEN 'true' then 2 else is_admin end) as access FROM admin.users as u join admin.permissions as p on p.user_id = u.id join domain.SensorGroup sg ON sg.ID = p.sensor_group_id WHERE u.authenticator = '$AUTH' ORDER BY u.username ASC, sg.Name ASC)" |output_pipe
   output ''
}


print_user_groups_from_authenticator() {
   local AUTH="$1"
   local HEAD="$2"
   output_section_head2 "$HEAD"
}

print_users_from_authenticator "local" "LOCAL FLEET USERS ('local' authentication)"
print_users_from_authenticator "tacacs-plus" "TACACS+ FLEET USERS ('tacacs+' authentication)"

if [ "$SAML_ENABLED" = 1 ]; then
   print_users_from_authenticator "SAML" "RECENT SAML FLEET USERS (PERMISSIONS AS OF LAST LOGIN; 'saml')"
fi

output ''



############
# 1. GROUPS
############
output_section_head2 "Fleet Groups"
run_sqlite "SELECT ID, Name FROM domain.SensorGroup" |output_pipe
output ''



if [ "$SAML_ENABLED" = "1" ]; then
   output ""
   output_section_head2 "SAML: SAML Security Groups Mapped to Global Roles"
   output "Users in these SAML security groups will gain access to all sensor groups on next login"
   output ""
   run_sqlite "select line_number as 'Line', saml as 'SAML_Group', role as 'Access' from tmpdb.saml_to_role where role = 'monitor' or role = 'admin' or role = 'fleet_admin' ORDER BY line_number"   |output_pipe
   output ""
   output_section_head2 "SAML: Role to sensor group mapping"
   run_sqlite 'select sr.line_number as INI_Line, rg.line_number as CSV_Line, sr.saml as SAML_Group, sr.role as Role, rg.access as 'Access', rg.grp as Group_ID, g.Name as Group_Name, g.Description as 'Description' from tmpdb.saml_to_role sr join tmpdb.role_to_group rg on sr.role = rg.role join domain.SensorGroup g on rg.grp = g.ID' |output_pipe

   output_section_head2 "SAML: Empty roles defined in .ini"
   run_sqlite 'select * from tmpdb.saml_to_role_empty order by line_number' |output_pipe
   output_section_head2 "SAML: Invalid entries in .csv"
   run_sqlite 'select * from tmpdb.invalid_role_to_group order by line_number' |output_pipe
fi

###################
## DUMP
###################
cat "$TMP_FILE_NAME"
PRINT_ERROR=0
