#!/bin/sh

CONFDIR=~/.selocal;

if [ $# -lt 1 ] || [ "$1" = "-h" ] || [ "$1" == "--help" ];
then
  echo "Usage: $(basename $0) [<command>] [<options>] <rule|line>";
  echo "";
  echo "Command can be one of:";
  echo "  -l, --list		List the content of a SELinux module";
  echo "  -a, --add		Add an entry to a SELinux module";
  echo "  -d, --delete		Remove an entry from a SELinux module";
  echo "  -M, --list-modules	List the modules currently known by $(basename $0)";
  echo "  -u, --update-dep	Update the dependencies for the rules";
  echo "  -b, --build		Build the SELinux module (.pp) file (requires privs)";
  echo "  -L, --load		Load the SELinux module (.pp) file (requires privs)";
  echo "  -B, --backup		Present the type enforcement rules as they are";
  echo "  -R, --restore		Restore the type enforcement rules from file";
  echo "";
  echo "Options can be one of:";
  echo "  -m, --module <module>		Module name to use (default: selocal)";
  echo "  -c, --comment <comment>	Comment (with --add)";
  echo "";
  echo "The option -a requires that a rule is given, like so:";
  echo "  $(basename $0) -a \"dbadm_role_change(staff_r)\"";
  echo "The option -d requires that a line number, as shown by the --list, is given, like so:";
  echo "  $(basename $0) -d 12";
  echo "The option -R requires a file that replaces the current type";
  echo "  $(basename $0) -R last-backup.te";
  echo "";
  echo "WARNING!"
  echo "The numbers change with every modification, so please rerun --list again before every delete."
  exit 1;
fi

# Initialize configuration
if [ ! -d "${CONFDIR}" ];
then
  echo "Creating ${CONFDIR} configuration directory.";
  mkdir "${CONFDIR}";
fi

if [ ! -d "${CONFDIR}/changes" ] && [ -f "${CONFDIR}/track-changes" ];
then
  mkdir "${CONFDIR}/changes";
fi

# Check if a role is known for the current policy
validateRole() {
  seinfo -r$1 > /dev/null 2>&1;
  return $?;
}

# Check if a type is known for the current policy
validateType() {
  seinfo -t$1 > /dev/null 2>&1;
  return $?;
}

# Check if an attribute is known for the current policy
validateAttribute() {
  seinfo -a$1 > /dev/null 2>&1;
  return $?;
}

# Update dependencies
updatedep() {
  typeset MODNAME="$1";

  awk 'BEGIN {f=1}; f{print;} /# REQSTART/ {f=0;}' ${CONFDIR}/${MODNAME}.te > ${CONFDIR}/${MODNAME}.te.new;

  # Interfaces
  for TYPEDEF in $(awk 'BEGIN {f=0}; f{print;} /# POLICYSTART/ {f=1;}' ${CONFDIR}/${MODNAME}.te | \
    sed -e 's:#.*::g' | \
    grep ')' | \
    sed -e 's:[a-zA-Z0-9_]*(\([^)]*\)):\1:g' | \
    sed -e 's:,[ ]: :g' | \
    tr -d '\n' | tr " " "\n" | sort | uniq);
  do
    if [[ "${TYPEDEF}" == *"_r" ]];
    then
      validateRole ${TYPEDEF} && echo "role ${TYPEDEF};" >> ${CONFDIR}/${MODNAME}.te.new;
    fi

    if [[ "${TYPEDEF}" == *"_t" ]];
    then
      validateType ${TYPEDEF} && echo "type ${TYPEDEF};" >> ${CONFDIR}/${MODNAME}.te.new;
    fi

    if [[ ! -d "/sys/fs/selinux/class/${TYPEDEF}" ]] && [[ "x${TYPEDEF:0:1}" != "x\"" ]]
    then
      validateAttribute ${TYPEDEF} && echo "attribute ${TYPEDEF};" >> ${CONFDIR}/${MODNAME}.te.new;
    fi
  done

  # Others, first get types
  # allow, dontaudit, auditallow, type_transition and type_change all have a type at second
  # and third argument
  for TYPEDEF in $(awk 'BEGIN {f=0}; f{print;} /# POLICYSTART/ {f=1;}' ${CONFDIR}/${MODNAME}.te | \
    grep -E '^[ ]*(allow|type_transition|dontaudit|auditallow|type_change) ' | sed -e 's|:| |g' | awk '{print $2" "$3" "}' | \
    tr -d '\n' | tr " " "\n" | sort | uniq);
  do
    if [[ "${TYPEDEF}" == *"_t" ]];
    then
      validateType ${TYPEDEF} && echo "type ${TYPEDEF};" >> ${CONFDIR}/${MODNAME}.te.new;
    fi

    if [[ "${TYPEDEF}" != "self" ]] && [[ ! -d "/sys/fs/selinux/class/${TYPEDEF}" ]] && [[ "x${TYPEDEF:0:1}" != "x\"" ]]
    then
      validateAttribute ${TYPEDEF} && echo "attribute ${TYPEDEF};" >> ${CONFDIR}/${MODNAME}.te.new;
    fi
  done
  # type_transition and type_change also has a type at fourth
  for TYPEDEF in $(awk 'BEGIN {f=0}; f{print;} /# POLICYSTART/ {f=1;}' ${CONFDIR}/${MODNAME}.te | \
    grep -E '^[ ]*(type_transition|type_change) ' | sed -e 's|:| |g' | awk '{print $5}' | \
    tr -d '\n' | tr " " "\n" | sort | uniq);
  do
    if [[ "${TYPEDEF}" == *"_t" ]];
    then
      validateType ${TYPEDEF} && echo "type ${TYPEDEF};" >> ${CONFDIR}/${MODNAME}.te.new;
    fi

    if [[ "${TYPEDEF}" != "self" ]] && [[ ! -d "/sys/fs/selinux/class/${TYPEDEF}" ]] && [[ "x${TYPEDEF:0:1}" != "x\"" ]]
    then
      validateAttribute ${TYPEDEF} && echo "attribute ${TYPEDEF};" >> ${CONFDIR}/${MODNAME}.te.new;
    fi
  done


  awk '/# REQSTOP/,0 {print;}' ${CONFDIR}/${MODNAME}.te >> ${CONFDIR}/${MODNAME}.te.new;

  cat "${CONFDIR}/${MODNAME}.te.new" > "${CONFDIR}/${MODNAME}.te"
  rm -f "${CONFDIR}/${MODNAME}.te.new"
}

# New module template
newtemplate() {
  typeset MODNAME=$1;

  cat > ${CONFDIR}/${MODNAME}.te << EOF
policy_module(${MODNAME}, 0.1)

gen_require(\`
  # Add in requirements that selocal cannot manage below this but before the REQSTART line
  # REQSTART

  # REQSTOP
')

# Add in policy rules that selocal cannot manage below this but before the POLICYSTART line

# POLICYSTART
EOF
}

# Load the module
loadmodule() {
  typeset MODNAME="$1";
  echo "## Loading ${MODNAME}.pp (in ~/.selocal):"
  echo "# semodule -i ~/.selocal/${MODNAME}.pp"
  semodule -i ~/.selocal/${MODNAME}.pp;
  return $?;
}

# Build the module
buildmodule() {
  typeset MODNAME="$1";
  typeset POLICYTYPE=$(awk -F'=' '/^SELINUXTYPE=/ {print $2}' /etc/selinux/config);
  typeset RC=0;

  pushd ~/.selocal;
  echo "## Building ${MODNAME}.pp (in ~/.selocal):";
  echo "# make -f /usr/share/selinux/${POLICYTYPE}/include/Makefile ${MODNAME}.pp"
  make -f /usr/share/selinux/${POLICYTYPE}/include/Makefile ${MODNAME}.pp;
  RC=$?;

  popd;
  return ${RC};
}

# Add a rule to the local module
addrule() {
  typeset MODNAME="${1}";
  typeset RULE="${2}";
  typeset COMMENT="${3}";

  if [ ! -f ${CONFDIR}/${MODNAME}.te ];
  then
    newtemplate ${MODNAME};
  fi

  cat "${CONFDIR}/${MODNAME}.te" > "${CONFDIR}/${MODNAME}.te.new";
  echo "${RULE} # ${COMMENT}" >> "${CONFDIR}/${MODNAME}.te.new";
  [ -f "${CONFDIR}/track-changes" ] && diff -u "${CONFDIR}/${MODNAME}.te" "${CONFDIR}/${MODNAME}.te.new" > "${CONFDIR}/changes/$(date +%Y%m%d-%H%M%S-%N).patch"
  cat "${CONFDIR}/${MODNAME}.te.new" > "${CONFDIR}/${MODNAME}.te";
  rm -f "${CONFDIR}/${MODNAME}.te.new";
  updatedep ${MODNAME};
}

# Delete a rule from the local module
delrule() {
  typeset MODNAME="${1}";
  typeset RULEID="${2}";

  echo "${RULEID}" | grep -q -E '^[0-9]*$';
  if [ $? -ne 0 ];
  then
    echo "Line number \"${RULEID}\" is not a valid id, pelase use a (strictly positive) number.";
    return;
  fi

  if [ -z "${RULEID}" ] || [ "${RULEID}" -lt 8 ];
  then
    echo "Line number \"${RULEID}\" is not a valid id, please use a (strictly positive) number.";
    return;
  fi

  if [ ! -f "${CONFDIR}/${MODNAME}.te" ];
  then
    echo "# WARNING Module \"${MODNAME}\" is not known yet (no rules assigned)";
    return;
  fi

  echo "Removing line ${RULEID} from module ${MODNAME} (${CONFDIR}/${MODNAME}.te)";
  awk "(NR!=${RULEID}) {print;}" "${CONFDIR}/${MODNAME}.te" > "${CONFDIR}/${MODNAME}.te.new";
  awk "(NR==${RULEID}) {print;}" "${CONFDIR}/${MODNAME}.te" | sed -e 's|^|Removed following line: |g';

  [ -f "${CONFDIR}/track-changes" ] && diff -u "${CONFDIR}/${MODNAME}.te" "${CONFDIR}/${MODNAME}.te.new" > "${CONFDIR}/changes/$(date +%Y%m%d-%H%M%S-%N).patch";
  cat "${CONFDIR}/${MODNAME}.te.new" > "${CONFDIR}/${MODNAME}.te";
  rm -f "${CONFDIR}/${MODNAME}.te.new";

  updatedep ${MODNAME};
}

# List rules
listrules() {
  typeset MODNAME="${1}";
  if [ ! -f ${CONFDIR}/${MODNAME}.te ];
  then
    echo "# WARNING Module ${MODNAME} is not known yet (no rules assigned)";
    return;
  fi

  awk 'BEGIN {f=0; line=1}; /# POLICYSTART/ {f=1; if(getline == 0) {f=0};}; f{printf NR": "; print}' ${CONFDIR}/${MODNAME}.te | sed -e 's:# $::g';
}

# List modules
listmods() {
  ls ${CONFDIR} | grep .te$ | sed -e 's:\.te::g';
}

# Backup module
backuprules() {
  typeset MODNAME="${1}";
  cat "${CONFDIR}/${MODNAME}.te"
}

# Restore module
restorerules() {
  typeset MODNAME="${1}";
  typeset RESTOREFILE="${2}";

  cat "${RESTOREFILE}" > "${CONFDIR}/${MODNAME}.te";
}

listflag=0;
delflag=0;
addflag=0;
modflag=0;
listmodflag=0;
buildflag=0;
loadflag=0;
updateflag=0;
commentflag=0;
backupflag=0;
restoreflag=0;
comment="";
modname="";
rule="";

eval set -- "$(getopt -n $(basename $0) -s sh -o MlbLuadm:c:BR --long list-modules,list,build,load,update-dep,add,delete,module:,comment:,backup,restore -- "$@")"
while [ $# -gt 0 ];
do
  case "$1" in
	(-l) listflag=1;;
	(--list) listflag=1;;
	(-a) addflag=1;;
	(--add) addflag=1;;
	(-d) delflag=1;;
	(--delete) delflag=1;;
	(-m) modflag=1; modname="$2"; shift;;
	(--module) modflag=1; modname="$2"; shift;;
	(-M) listmodflag=1;;
	(--list-modules) listmodflag=1;;
	(-c) commentflag=1; comment="$2"; shift;;
	(--comment) commentflag=1; comment="$2"; shift;;
	(-b) buildflag=1;;
	(--build) buildflag=1;;
	(-L) loadflag=1;;
	(--load) loadflag=1;;
	(-u) updateflag=1;;
	(--update-dep) updateflag=1;;
	(-B) backupflag=1;;
	(--backup) backupflag=1;;
	(-R) restoreflag=1;;
	(--restore) restoreflag=1;;
	(--) rule="$2"; shift; break;;
	(-*) echo "$(basename $0): error: Unrecognized option $1" 1>&2; exit 1;;
	(*) break;;
  esac
  shift;
done

if [ $((${listflag} + ${delflag} + ${addflag} + ${listmodflag})) -gt 1 ];
then
  echo "$(basename $0): error: (only) one option of (-l, -a, -d or -M) is needed" 1>&2;
  exit 1;
fi

if [ -z "${modname}" ];
then
  modname="selocal";
fi

if [ ${backupflag} -eq 1 ];
then
  backuprules "${modname}";
fi

if [ ${restoreflag} -eq 1 ];
then
  restorerules "${modname}" "${rule}";
fi

if [ ${listflag} -eq 1 ];
then
  listrules "${modname}";
fi

if [ ${addflag} -eq 1 ];
then
  addrule "${modname}" "${rule}" "${comment}"
fi

if [ ${delflag} -eq 1 ];
then
  delrule "${modname}" "${rule}"
fi

if [ ${listmodflag} -eq 1 ];
then
  listmods
fi

if [ ${updateflag} -eq 1 ];
then
  updatedep "${modname}";
fi

if [ ${buildflag} -eq 1 ];
then
  buildmodule "${modname}";
fi

if [ ${loadflag} -eq 1 ];
then
  loadmodule "${modname}";
fi
