#!/bin/sh
# Copyright (C) 2011-2020 Greenbone Networks GmbH
#
# 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 Security Feed (in
# case a GSF access key is present) or else from the Greenbone
# Community Feed.

log_notice () {
  $LOG_CMD -p daemon.notice "$1"
}


########## 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_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_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_COMPRESS specifies the compression level to use for the rsync connection.
RSYNC_COMPRESS="--compress-level=9"

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

# 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

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

# LOG_CMD defines the command to use for logging. To have logger log to stderr
# as well as syslog, add "-s" here.
LOG_CMD="logger -t $SCRIPT_NAME"

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


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

VERSION=21.4.3

[ -r "/etc/gvm/greenbone-feed-sync.conf" ] && . "/etc/gvm/greenbone-feed-sync.conf"

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

ACCESSKEY="/etc/gvm/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 GSF access key is present. If yes,
# then use the Greenbone Security Feed. Else use the
# Greenbone Community Feed.
if [ -e $ACCESSKEY ]
then
  RESTRICTED=1

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

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

else
  RESTRICTED=0

  if [ -z "$FEED_VENDOR" ]; then
    FEED_VENDOR="Greenbone Networks GmbH"
  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
########## =========

log_debug () {
  $LOG_CMD -p daemon.debug "$1"
}

log_info () {
  $LOG_CMD -p daemon.info "$1"
}

log_warning () {
  $LOG_CMD -p daemon.warning "$1"
}

log_err () {
  $LOG_CMD -p daemon.err "$1"
}

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 "/etc/gvm/greenbone-certdata-sync.conf" ] && . "/etc/gvm/greenbone-certdata-sync.conf"

    FEED_TYPE_LONG="CERT data"
    FEED_DIR="/var/lib/gvm/cert-data"
    TIMESTAMP="$FEED_DIR/timestamp"
    SCRIPT_ID="CERTSYNC"

    if [ -z "$COMMUNITY_CERT_RSYNC_FEED" ]; then
      COMMUNITY_RSYNC_FEED="rsync://feed.community.greenbone.net:/cert-data"
      # An alternative syntax which might work if the above doesn't:
      # COMMUNITY_RSYNC_FEED="rsync@feed.community.greenbone.net::cert-data"
    else
      COMMUNITY_RSYNC_FEED="$COMMUNITY_CERT_RSYNC_FEED"
    fi

    GSF_RSYNC_PATH="/cert-data"

    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 "/etc/gvm/greenbone-scapdata-sync.conf" ] && . "/etc/gvm/greenbone-scapdata-sync.conf"

    FEED_TYPE_LONG="SCAP data"
    FEED_DIR="/var/lib/gvm/scap-data"
    TIMESTAMP="$FEED_DIR/timestamp"
    SCRIPT_ID="SCAPSYNC"

    if [ -z "$COMMUNITY_SCAP_RSYNC_FEED" ]; then
      COMMUNITY_RSYNC_FEED="rsync://feed.community.greenbone.net:/scap-data"
      # An alternative syntax which might work if the above doesn't:
      # COMMUNITY_RSYNC_FEED="rsync@feed.community.greenbone.net::scap-data"
    else
      COMMUNITY_RSYNC_FEED="$COMMUNITY_SCAP_RSYNC_FEED"
    fi

    GSF_RSYNC_PATH="/scap-data"

    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 "/etc/gvm/greenbone-data-objects-sync.conf" ] && . "/etc/gvm/greenbone-data-objects-sync.conf"

    FEED_TYPE_LONG="gvmd Data"
    FEED_DIR="/var/lib/gvm/data-objects/gvmd"
    TIMESTAMP="$FEED_DIR/timestamp"
    SCRIPT_ID="GVMD_DATA_SYNC"

    if [ -z "$COMMUNITY_GVMD_DATA_RSYNC_FEED" ]; then
      COMMUNITY_RSYNC_FEED="rsync://feed.community.greenbone.net:/data-objects/gvmd/"
      # An alternative syntax which might work if the above doesn't:
      # COMMUNITY_RSYNC_FEED="rsync@feed.community.greenbone.net::data-objects/gvmd/"
    else
      COMMUNITY_RSYNC_FEED="$COMMUNITY_GVMD_DATA_RSYNC_FEED"
    fi

    GSF_RSYNC_PATH="/data-objects/gvmd/"

    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
}

# This function uses gos-state-manager to get information about the settings.
# gos-state-manager is only available on a Greenbone OS.
# If gos-state-manager is missing the settings values can not be retrieved.
#
# Input: option
# Output: value as string or empty String if gos-state-manager is not installed
#         or option not set
get_value ()
{
  value=""
  key=$1
  if which gos-state-manager 1>/dev/null 2>&1
  then
    if gos-state-manager get "$key.value" 1>/dev/null 2>&1
    then
      value="$(gos-state-manager get "$key.value")"
    fi
  fi
  echo "$value"
}

is_feed_current () {
  if [ -r $TIMESTAMP ]
  then
    FEED_VERSION=`cat $TIMESTAMP`
  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
    custid_at_host=`head -1 $ACCESSKEY | cut -d : -f 1`

    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

    gsmproxy=$(get_value proxy_feed | sed -r -e 's/^.*\/\///' -e 's/:([0-9]+)$/ \1/')
    syncport=$(get_value syncport)
    if [ "$syncport" ]
    then
      PORT="$syncport"
    fi

    if [ -z "$gsmproxy" ] || [ "$gsmproxy" = "proxy_feed" ]
    then
      RSYNC_SSH_PROXY_CMD=""
    else
      if [ -e $GVM_SYSCONF_DIR/proxyauth ] && [ -r $GVM_SYSCONF_DIR/proxyauth ]; then
        RSYNC_SSH_PROXY_CMD="-o \"ProxyCommand corkscrew $gsmproxy %h %p $GVM_SYSCONF_DIR/proxyauth\""
      else
        RSYNC_SSH_PROXY_CMD="-o \"ProxyCommand corkscrew $gsmproxy %h %p\""
      fi
    fi
    create_tmp_key
    rsync -e "ssh $RSYNC_SSH_OPTS $RSYNC_SSH_PROXY_CMD -p $PORT -i $TMPACCESSKEY" -ltvrP $RSYNC_CHMOD $RSYNC_DELETE $RSYNC_COMPRESS $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 Security Feed access key found, falling back to Greenbone Community Feed"
    eval "$RSYNC -ltvrP $RSYNC_CHMOD \"$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 "GSF access key found: Using Greenbone Security Feed"
  else
    echo "No GSF 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_rsync_community_feed () {
  if [ -z "$RSYNC" ]; then
    log_err "rsync not found!"
  else
    # 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 -ltvrP $RSYNC_CHMOD $RSYNC_DELETE \"$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_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"
    do_rsync_community_feed
  fi
}

sync_feed_data(){
  if [ -e $ACCESSKEY ]
  then
    log_notice "Found Greenbone Security Feed subscription file, trying to synchronize with Greenbone $FEED_TYPE_LONG Repository ..."
    notsynced=1

    mkdir -p "$FEED_DIR"
    read feeduser < $ACCESSKEY
    custid_at_host=`head -1 $ACCESSKEY | cut -d : -f 1`

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

    while [ 0 -ne "$notsynced" ]
    do

      gsmproxy=$(get_value proxy_feed | sed -r -e 's/^.*\/\///' -e 's/:([0-9]+)$/ \1/')
      syncport=$(get_value syncport)
      if [ "$syncport" ]
      then
        PORT="$syncport"
      fi

      if [ -z "$gsmproxy" ] || [ "$gsmproxy" = "proxy_feed" ]
      then
        RSYNC_SSH_PROXY_CMD=""
      else
        if [ -e $GVM_SYSCONF_DIR/proxyauth ] && [ -r $GVM_SYSCONF_DIR/proxyauth ]; then
          RSYNC_SSH_PROXY_CMD="-o \"ProxyCommand corkscrew $gsmproxy %h %p $GVM_SYSCONF_DIR/proxyauth\""
        else
          RSYNC_SSH_PROXY_CMD="-o \"ProxyCommand corkscrew $gsmproxy %h %p\""
        fi
      fi
      create_tmp_key
      rsync -e "ssh $RSYNC_SSH_OPTS $RSYNC_SSH_PROXY_CMD -p $PORT -i $ACCESSKEY" -ltvrP $RSYNC_CHMOD $RSYNC_DELETE $RSYNC_COMPRESS $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."
  else
    log_notice "No Greenbone Security Feed access key found, falling back to Greenbone Community Feed"
    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
