#!/bin/sh
# Copyright (C) 2011-2022 Greenbone AG
#
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# This script synchronizes a GVM installation with the
# feed data from either the Greenbone Enterprise Feed (in
# case a access key is present) or else from the Greenbone
# Community Feed.

# SCRIPT_NAME is the name the scripts will use to identify itself and to mark
# log messages.
SCRIPT_NAME="greenbone-feed-sync"

########## LOG FUNCTIONS
########## =============

# LOG_CMD defines the command to use for logging. To have logger log to syslog
# only, remove "-s" here.
if [ -z "$LOG_CMD" ]
then
  LOG_CMD="logger -t $SCRIPT_NAME -s"
fi

TEST_LOGGER=$(logger -s "" 2>/dev/null)
HAS_LOGGER=$?

log_debug () {
  if [ $HAS_LOGGER -eq 0 ]; then
    $LOG_CMD -p daemon.debug "$1"
  else
    echo "$1"
  fi
}

log_info () {
  if [ $HAS_LOGGER -eq 0 ]; then
    $LOG_CMD -p daemon.info "$1"
  else
    echo "$1"
  fi
}

log_warning () {
  if [ $HAS_LOGGER -eq 0 ]; then
    $LOG_CMD -p daemon.warning "$1"
  else
    echo "$1"
  fi
}

log_err () {
  if [ $HAS_LOGGER -eq 0 ]; then
    $LOG_CMD -p daemon.err "$1"
  else
    echo "$1" >&2
  fi
}

log_notice () {
  if [ $HAS_LOGGER -eq 0 ]; then
    $LOG_CMD -p daemon.notice "$1"
  else
    echo "$1"
  fi
}

########## SETTINGS
########## ========

# PRIVATE_SUBDIR defines a subdirectory of the feed data directory
# where files not part of the feed or database will not be deleted by rsync.
if [ -z "$PRIVATE_SUBDIR" ]
then
  PRIVATE_SUBDIR="private"
fi

# RSYNC_SSH_OPTS contains options which should be passed to ssh for the rsync
# connection to the repository.
RSYNC_SSH_OPTS="-o \"UserKnownHostsFile=/dev/null\" -o \"StrictHostKeyChecking=no\""

# RSYNC_DEFAULT standard options for rsync
RSYNC_DEFAULT="--times --verbose --recursive --partial --progress"

# RSYNC_DELETE controls whether files which are not part of the repository will
# be removed from the local directory after synchronization. The default value
# for this setting is
# "--delete --exclude feed.xml --exclude $PRIVATE_SUBDIR/",
# which means that files which are not part of the feed, feed info or private
# directory will be deleted.
RSYNC_DELETE="--delete --exclude feed.xml --exclude \"$PRIVATE_SUBDIR/\""

# RSYNC_CHMOD specifies the permissions to chmod the files to.
RSYNC_CHMOD="--perms --chmod=Fugo+r,Fug+w,Dugo-s,Dugo+rx,Dug+w"

# RSYNC_COMPRESS specifies the compression level to use for the rsync connection.
RSYNC_COMPRESS="--compress-level=9"

# RSYNC_LINKS specifies how to handle links
RSYNC_LINKS="--copy-unsafe-links --hard-links"

# RSYNC_OPTIONS specifies common rsync options for downloading the feeed
RSYNC_OPTIONS="$RSYNC_DEFAULT $RSYNC_DELETE $RSYNC_CHMOD $RSYNC_COMPRESS $RSYNC_LINKS"

# RSYNC_COMMUNITY_BASE_URL defines the base rsync URL for the community feed
# not including the feed type (data or vulnerability) and version.
if [ -z "$RSYNC_COMMUNITY_BASE_URL" ]
then
  RSYNC_COMMUNITY_BASE_URL="rsync://feed.community.greenbone.net/community"
fi

# RSYNC_COMMUNITY_DATA_URL defines the rsync URL for the community data feed.
RSYNC_COMMUNITY_DATA_URL="${RSYNC_COMMUNITY_BASE_URL}/data-feed/@GMP_VERSION_FEED@/"

# RSYNC_COMMUNITY_CERT_URL defines the rsync URL for the community SCAP feed.
RSYNC_COMMUNITY_CERT_URL="${RSYNC_COMMUNITY_BASE_URL}/vulnerability-feed/@GMP_VERSION_FEED@/cert-data/"

# RSYNC_COMMUNITY_SCAP_URL defines the rsync URL for the community SCAP feed.
RSYNC_COMMUNITY_SCAP_URL="${RSYNC_COMMUNITY_BASE_URL}/vulnerability-feed/@GMP_VERSION_FEED@/scap-data/"

# If ENTERPRISE_FEED_HOST_OVERRIDE is set to a non-empty string,
#  the hostname of the enterprise feed server will overridden with it.
# Otherwise the server hostname from the feed access key will be used.
if [ -z "$ENTERPRISE_FEED_HOST_OVERRIDE" ]
then
  ENTERPRISE_FEED_HOST_OVERRIDE=""
fi

# ENTERPRISE_FEED_BASE_PATH defines the common base path for the feed data
#  on the enterprise feed server.
if [ -z "$ENTERPRISE_FEED_BASE_PATH" ]
then
  ENTERPRISE_FEED_BASE_PATH="/enterprise"
fi

# ENTERPRISE_FEED_DATA_PATH defines the path of the gvmd data feed
#  on the enterprise feed server.
ENTERPRISE_FEED_DATA_PATH="$ENTERPRISE_FEED_BASE_PATH/data-feed/@GMP_VERSION_FEED@/"

# ENTERPRISE_FEED_DATA_PATH defines the path of the CERT feed
#  on the enterprise feed server.
ENTERPRISE_FEED_CERT_PATH="$ENTERPRISE_FEED_BASE_PATH/vulnerability-feed/@GMP_VERSION_FEED@/cert-data/"

# ENTERPRISE_FEED_DATA_PATH defines the path of the SCAP feed
#  on the enterprise feed server.
ENTERPRISE_FEED_SCAP_PATH="$ENTERPRISE_FEED_BASE_PATH/vulnerability-feed/@GMP_VERSION_FEED@/scap-data/"

# PORT controls the outgoing TCP port for updates. If PAT/Port-Translation is
# not used, this should be "24". For some application layer firewalls or gates
# the value 22 (Standard SSH) is useful. Only change if you know what you are
# doing.
PORT=24


# LOCK_FILE is the name of the file used to lock the feed during sync or update.
if [ -z "$LOCK_FILE" ]
then
  LOCK_FILE="@GVM_FEED_LOCK_PATH@"
fi


########## GLOBAL VARIABLES
########## ================

VERSION=@GVMD_VERSION@

[ -r "@GVM_SYSCONF_DIR@/greenbone-feed-sync.conf" ] && . "@GVM_SYSCONF_DIR@/greenbone-feed-sync.conf"

if [ -z "$DROP_USER" ]; then
  DROP_USER="@GVM_DEFAULT_DROP_USER@"
fi

ACCESSKEY="@GVM_ACCESS_KEY_DIR@/gsf-access-key"

# Note when running as root or restart as $DROP_USER if defined
if [ $(id -u) -eq 0 ]
then
  if [ -z "$DROP_USER" ]
  then
    log_notice "Running as root"
  else
    log_notice "Started as root, restarting as $DROP_USER"
    su --shell /bin/sh --command "$0 $*" "$DROP_USER"
    exit $?
  fi
fi

# Determine whether a access key is present. If yes,
# then use the Greenbone Enterprise Feed. Else use the
# Greenbone Community Feed.
if [ -e $ACCESSKEY ]
then
  RESTRICTED=1

  if [ -z "$FEED_VENDOR" ]; then
    FEED_VENDOR="Greenbone AG"
  fi

  if [ -z "$FEED_HOME" ]; then
    FEED_HOME="https://www.greenbone.net/en/feed-comparison/"
  fi

else
  RESTRICTED=0

  if [ -z "$FEED_VENDOR" ]; then
    FEED_VENDOR="Greenbone AG"
  fi

  if [ -z "$FEED_HOME" ]; then
    FEED_HOME="https://community.greenbone.net/t/about-greenbone-community-feed-gcf/1224"
  fi

fi

RSYNC=`command -v rsync`

# Current supported feed types (for --type parameter)
FEED_TYPES_SUPPORTED="CERT, SCAP or GVMD_DATA"

########## FUNCTIONS
########## =========

init_feed_type () {
  if [ -z "$FEED_TYPE" ]
  then
    echo "No feed type given to --type parameter"
    log_err "No feed type given to --type parameter"
    exit 1
  elif [ "CERT" = "$FEED_TYPE" ]
  then
    [ -r "@GVM_SYSCONF_DIR@/greenbone-certdata-sync.conf" ] && . "@GVM_SYSCONF_DIR@/greenbone-certdata-sync.conf"

    FEED_TYPE_LONG="CERT data"
    FEED_DIR="@GVM_CERT_DATA_DIR@"
    TIMESTAMP="$FEED_DIR/timestamp"
    SCRIPT_ID="CERTSYNC"

    if [ -z "$COMMUNITY_CERT_RSYNC_FEED" ]; then
      COMMUNITY_RSYNC_FEED="$RSYNC_COMMUNITY_CERT_URL"
    else
      COMMUNITY_RSYNC_FEED="$COMMUNITY_CERT_RSYNC_FEED"
    fi

    GSF_RSYNC_PATH="$ENTERPRISE_FEED_CERT_PATH"

    if [ -e $ACCESSKEY ]; then
      if [ -z "$FEED_NAME" ]; then
        FEED_NAME="Greenbone CERT Feed"
      fi
    else
      if [ -z "$FEED_NAME" ]; then
        FEED_NAME="Greenbone Community CERT Feed"
      fi
    fi
  elif [ "SCAP" = "$FEED_TYPE" ]
  then
    [ -r "@GVM_SYSCONF_DIR@/greenbone-scapdata-sync.conf" ] && . "@GVM_SYSCONF_DIR@/greenbone-scapdata-sync.conf"

    FEED_TYPE_LONG="SCAP data"
    FEED_DIR="@GVM_SCAP_DATA_DIR@"
    TIMESTAMP="$FEED_DIR/timestamp"
    SCRIPT_ID="SCAPSYNC"

    if [ -z "$COMMUNITY_SCAP_RSYNC_FEED" ]; then
      COMMUNITY_RSYNC_FEED="$RSYNC_COMMUNITY_SCAP_URL"
    else
      COMMUNITY_RSYNC_FEED="$COMMUNITY_SCAP_RSYNC_FEED"
    fi

    GSF_RSYNC_PATH="$ENTERPRISE_FEED_SCAP_PATH"

    if [ -e $ACCESSKEY ]; then
      if [ -z "$FEED_NAME" ]; then
        FEED_NAME="Greenbone SCAP Feed"
      fi
    else
      if [ -z "$FEED_NAME" ]; then
        FEED_NAME="Greenbone Community SCAP Feed"
      fi
    fi
  elif [ "GVMD_DATA" = "$FEED_TYPE" ]
  then
    [ -r "@GVM_SYSCONF_DIR@/greenbone-data-objects-sync.conf" ] && . "@GVM_SYSCONF_DIR@/greenbone-data-objects-sync.conf"

    FEED_TYPE_LONG="gvmd Data"
    FEED_DIR="@GVMD_FEED_DIR@"
    TIMESTAMP="$FEED_DIR/timestamp"
    SCRIPT_ID="GVMD_DATA_SYNC"

    if [ -z "$COMMUNITY_GVMD_DATA_RSYNC_FEED" ]; then
      COMMUNITY_RSYNC_FEED="$RSYNC_COMMUNITY_DATA_URL"
    else
      COMMUNITY_RSYNC_FEED="$COMMUNITY_GVMD_DATA_RSYNC_FEED"
    fi

    GSF_RSYNC_PATH="$ENTERPRISE_FEED_DATA_PATH"

    if [ -e $ACCESSKEY ]; then
      if [ -z "$FEED_NAME" ]; then
        FEED_NAME="Greenbone gvmd Data Feed"
      fi
    else
      if [ -z "$FEED_NAME" ]; then
        FEED_NAME="Greenbone Community gvmd Data Feed"
      fi
    fi
  else
    echo "Invalid feed type $FEED_TYPE given to --type parameter. Currently supported: $FEED_TYPES_SUPPORTED"
    log_err "Invalid feed type $FEED_TYPE given to --type parameter. Currently supported: $FEED_TYPES_SUPPORTED"
    exit 1
  fi
}

write_feed_xml () {
  if [ -r $TIMESTAMP ]
  then
    FEED_VERSION=`cat $TIMESTAMP`
  else
    FEED_VERSION=0
  fi

  mkdir -p $FEED_DIR
  echo '<feed id="6315d194-4b6a-11e7-a570-28d24461215b">' > $FEED_DIR/feed.xml
  echo "<type>$FEED_TYPE</type>" >> $FEED_DIR/feed.xml
  echo "<name>$FEED_NAME</name>" >> $FEED_DIR/feed.xml
  echo "<version>$FEED_VERSION</version>" >> $FEED_DIR/feed.xml
  echo "<vendor>$FEED_VENDOR</vendor>" >> $FEED_DIR/feed.xml
  echo "<home>$FEED_HOME</home>" >> $FEED_DIR/feed.xml
  echo "<description>" >> $FEED_DIR/feed.xml
  echo "This script synchronizes a $FEED_TYPE collection with the '$FEED_NAME'." >> $FEED_DIR/feed.xml
  echo "The '$FEED_NAME' is provided by '$FEED_VENDOR'." >> $FEED_DIR/feed.xml
  echo "Online information about this feed: '$FEED_HOME'." >> $FEED_DIR/feed.xml
  echo "</description>" >> $FEED_DIR/feed.xml
  echo "</feed>" >> $FEED_DIR/feed.xml
}

create_tmp_key () {
  KEYTEMPDIR=`mktemp -d`
  cp "$ACCESSKEY" "$KEYTEMPDIR"
  TMPACCESSKEY="$KEYTEMPDIR/gsf-access-key"
  chmod 400 "$TMPACCESSKEY"
}

remove_tmp_key () {
  rm -rf "$KEYTEMPDIR"
}

set_interrupt_trap () {
  trap "handle_interrupt $1" 2
}

handle_interrupt () {
  echo "$1:X" >&3
}

do_describe () {
  echo "This script synchronizes a $FEED_TYPE collection with the '$FEED_NAME'."
  echo "The '$FEED_NAME' is provided by '$FEED_VENDOR'."
  echo "Online information about this feed: '$FEED_HOME'."
}

do_feedversion () {
  if [ -r $TIMESTAMP ]; then
      cat $TIMESTAMP
  fi
}

is_feed_current () {
  if [ -r $TIMESTAMP ]
  then
    FEED_VERSION=`cat $TIMESTAMP`
  else
    FEED_VERSION=0
  fi

  if [ -z "$FEED_VERSION" ]
  then
    log_warning "Could not determine feed version."
    FEED_CURRENT=0
    return $FEED_CURRENT
  fi

  FEED_INFO_TEMP_DIR=`mktemp -d`

  if [ -e $ACCESSKEY ]
  then
    read feeduser < $ACCESSKEY
    if [ -z "$ENTERPRISE_FEED_HOST_OVERRIDE" ]
    then
      custid_at_host=`head -1 $ACCESSKEY | cut -d : -f 1`
    else
      custid=`head -1 $ACCESSKEY | cut -d @ -f 1`
      custid_at_host="${custid}@${ENTERPRISE_FEED_HOST_OVERRIDE}"
    fi

    if [ -z "$feeduser" ] || [ -z "$custid_at_host" ]
    then
      log_err "Could not determine credentials, aborting synchronization."
      rm -rf "$FEED_INFO_TEMP_DIR"
      exit 1
    fi

    RSYNC_SSH_PROXY_CMD=""

    create_tmp_key
    rsync -e "ssh $RSYNC_SSH_OPTS $RSYNC_SSH_PROXY_CMD -p $PORT -i $TMPACCESSKEY" $RSYNC_OPTIONS $custid_at_host:$GSF_RSYNC_PATH/timestamp "$FEED_INFO_TEMP_DIR"
    if [ $? -ne 0 ]
    then
      log_err "rsync failed, aborting synchronization."
      rm -rf "$FEED_INFO_TEMP_DIR"
      remove_tmp_key
      exit 1
    fi
    remove_tmp_key
  else
    # Sleep for five seconds (a previous feed might have been synced a few seconds before) to prevent
    # IP blocking due to network equipment in between keeping the previous connection too long open.
    sleep 5
    log_notice "No Greenbone Enterprise Feed key found, falling back to Greenbone Community Feed"
    eval "$RSYNC $RSYNC_OPTIONS \"$COMMUNITY_RSYNC_FEED/timestamp\" \"$FEED_INFO_TEMP_DIR\""
    if [ $? -ne 0 ]
    then
      log_err "rsync failed, aborting synchronization."
      rm -rf "$FEED_INFO_TEMP_DIR"
      exit 1
    fi
  fi

  FEED_VERSION_SERVER=`cat "$FEED_INFO_TEMP_DIR/timestamp"`

  if [ -z "$FEED_VERSION_SERVER" ]
  then
    log_err "Could not determine server feed version."
    rm -rf "$FEED_INFO_TEMP_DIR"
    exit 1
  fi

  # Check against FEED_VERSION
  if [ $FEED_VERSION -lt $FEED_VERSION_SERVER ]; then
    FEED_CURRENT=0
  else
    FEED_CURRENT=1
  fi

  # Cleanup
  rm -rf "$FEED_INFO_TEMP_DIR"

  return $FEED_CURRENT
}

do_help () {
  echo "$0: Sync feed data"

  if [ -e $ACCESSKEY ]
  then
    echo "Access key found: Using Greenbone Enterprise Feed"
  else
    echo "No access key found: Using Community Feed"
  fi

  echo " --describe      display current feed info"
  echo " --feedversion   display version of this feed"
  echo " --help          display this help"
  echo " --identify      display information"
  echo " --selftest      perform self-test"
  echo " --type <TYPE>   choose type of data to sync ($FEED_TYPES_SUPPORTED)"
  echo " --version       display version"
  echo ""
  exit 0
}

do_sync_community_feed () {
  if [ -z "$RSYNC" ]; then
    log_err "rsync not found!"
    log_err "No utility available in PATH environment variable to download Feed data"
    exit 1
  else
    log_notice "Will use rsync"

    # Sleep for five seconds (after is_feed_current) to prevent IP blocking due to
    # network equipment in between keeping the previous connection too long open.
    sleep 5

    log_notice "Using rsync: $RSYNC"
    log_notice "Configured $FEED_TYPE_LONG rsync feed: $COMMUNITY_RSYNC_FEED"

    mkdir -p "$FEED_DIR"
    eval "$RSYNC $RSYNC_OPTIONS \"$COMMUNITY_RSYNC_FEED\" \"$FEED_DIR\""

    if [ $? -ne 0 ]; then
      log_err "rsync failed. Your $FEED_TYPE_LONG might be broken now."
      exit 1
    fi
  fi
}

do_sync_enterprise_feed () {
  log_notice "Found Greenbone Enterprise Feed access key, trying to synchronize with Greenbone $FEED_TYPE_LONG Repository ..."
  notsynced=1

  mkdir -p "$FEED_DIR"
  read feeduser < $ACCESSKEY
  if [ -z "$ENTERPRISE_FEED_HOST_OVERRIDE" ]
  then
    custid_at_host=`head -1 $ACCESSKEY | cut -d : -f 1`
  else
    custid=`head -1 $ACCESSKEY | cut -d @ -f 1`
    custid_at_host="${custid}@${ENTERPRISE_FEED_HOST_OVERRIDE}"
  fi

  if [ -z "$feeduser" ] || [ -z "$custid_at_host" ]
  then
    log_err "Could not determine credentials, aborting synchronization."
    exit 1
  fi

  while [ 0 -ne "$notsynced" ]
  do

    RSYNC_SSH_PROXY_CMD=""

    create_tmp_key
    rsync -e "ssh $RSYNC_SSH_OPTS $RSYNC_SSH_PROXY_CMD -p $PORT -i $TMPACCESSKEY" $RSYNC_OPTIONS $custid_at_host:$GSF_RSYNC_PATH/ $FEED_DIR
    if [ 0 -ne "$?" ]; then
      log_err "rsync failed, aborting synchronization."
      remove_tmp_key
      exit 1
    fi
    remove_tmp_key
    notsynced=0
  done

  log_notice "Synchronization with the Greenbone $FEED_TYPE_LONG Repository successful."
}

sync_feed_data(){
  if [ -e $ACCESSKEY ]
  then
    do_sync_enterprise_feed
  else
    do_sync_community_feed
  fi

  write_feed_xml
}

do_self_test () {
  if [ -z "$SELFTEST_STDERR" ]
  then
    SELFTEST_STDERR=0
  fi

  if [ -z "$RSYNC" ]
  then
    if [ 0 -ne $SELFTEST_STDERR ]
    then
      echo "rsync not found (required)." 1>&2
    fi
    log_err "rsync not found (required)."
    SELFTEST_FAIL=1
  fi
}


########## START
########## =====

while test $# -gt 0; do
  case "$1" in
    "--version"|"--identify"|"--describe"|"--feedversion"|"--selftest"|"--feedcurrent")
      if [ -z "$ACTION" ]; then
        ACTION="$1"
      fi
      ;;
    "--help")
      do_help
      exit 0
      ;;
    "--type")
      FEED_TYPE=$(echo "$2" | tr '[:lower:]-' '[:upper:]_')
      shift
      ;;
  esac
  shift
done

init_feed_type

write_feed_xml

case "$ACTION" in
  --version)
    echo $VERSION
    exit 0
    ;;
  --identify)
    echo "$SCRIPT_ID|$SCRIPT_NAME|$VERSION|$FEED_NAME|$RESTRICTED|$SCRIPT_ID"
    exit 0
    ;;
  --describe)
    do_describe
    exit 0
    ;;
  --feedversion)
    do_feedversion
    exit 0
    ;;
  --selftest)
    SELFTEST_FAIL=0
    SELFTEST_STDERR=1
    do_self_test
    exit $SELFTEST_FAIL
    ;;
  --feedcurrent)
    is_feed_current
    exit $?
    ;;
esac

SELFTEST_FAIL=0
do_self_test
if [ $SELFTEST_FAIL -ne 0 ]
then
  exit 1
fi

is_feed_current
if [ $FEED_CURRENT -eq 1 ]
then
  log_notice "Feed is already current, skipping synchronization."
  exit 0
fi
(
  chmod +660 $LOCK_FILE
  flock -n 9
  if [ $? -eq 1 ]; then
    log_notice "Sync in progress, exiting."
    exit 1
  fi
  date > $LOCK_FILE
  sync_feed_data
  echo -n > $LOCK_FILE
) 9>>$LOCK_FILE

exit 0
