#!/usr/local/bin/bash
#       Name: SearchSploit - Exploit-DB's CLI search tool
#    Version: 4.2.1 (2022-11-11)
# Written by: Offensive Security, Unix-Ninja, and g0tmi1k
#   Homepage: https://gitlab.com/exploit-database/exploitdb
#     Manual: https://www.exploit-db.com/searchsploit
#
## NOTE:
#   Exit code '0' means finished successfully
#   Exit code '1' means something went wrong
#   Exit code '2' means help screen
#   Exit code '6' means updated packages (APT, brew or Git)
#-----------------------------------------------------------------------------#

## Settings File
rc_file=""

## Default options
CLIPBOARD=0
COLOUR=1
CVE=0
EDBID=0
EXACT=0
EXAMINE=0
FILEPATH=1
FUZZY=1
GETPATH=0
JSON=0
MIRROR=0
OVERFLOW=0
SCASE=0
VERBOSE=0
WEBLINK=0
XML=0
COLOUR_TAG=""
TAGS=""
SEARCH=""
EXCLUDE=""
CASE_TAG_GREP="-i"
CASE_TAG_FGREP="tolower"
AWK_SEARCH=""
FUZZY_SEARCH=""
VERSION=
COLOUR_OFF_GREP=
COLOUR_ON_GREP=
REGEX_GREP=

## Check if our grep supports --color (BSD vs GNU)
if grep --help 2>&1 | grep "[-]-color" >/dev/null 2>&1; then
  COLOUR_OFF_GREP="--color=never"
  COLOUR_ON_GREP="--color=always"
fi

## Check if our grep supports --perl-regexp (BSD vs GNU)
if grep --help 2>&1 | grep "[-]-perl-regexp" >/dev/null 2>&1; then
  REGEX_GREP="-P"
else
  REGEX_GREP="-E"
fi

## Set LANG variable to avoid illegal byte sequence errors
LANG=C

## Set TERM
export TERM=xterm-256color

## Usage info
##  - https://www.tldp.org/LDP/abs/html/standard-options.html
##  - https://monkey.org/~marius/unix-tools-hints.html
##  - https://clig.dev/
function usage() {
  echo "  Usage: ${progname} [options] term1 [term2] ... [termN]"
  echo ""
  echo "=========="
  echo " Examples "
  echo "=========="
  echo "  ${progname} afd windows local"
  echo "  ${progname} -t oracle windows"
  echo "  ${progname} -p 39446"
  echo "  ${progname} linux kernel 3.2 --exclude=\"(PoC)|/dos/\""
  echo "  ${progname} -s Apache Struts 2.0.0"
  echo "  ${progname} linux reverse password"
  echo "  ${progname} -j 55555 | jq"
  echo "  ${progname} --cve 2021-44228"
  echo ""
  echo "  For more examples, see the manual: https://www.exploit-db.com/searchsploit"
  echo ""
  echo "========="
  echo " Options "
  echo "========="
  echo "## Search Terms"
  echo "   -c, --case     [term]      Perform a case-sensitive search (Default is inSEnsITiVe)"
  echo "   -e, --exact    [term]      Perform an EXACT & order match on exploit title (Default is an AND match on each term) [Implies \"-t\"]"
  echo "                                e.g. \"WordPress 4.1\" would not be detect \"WordPress Core 4.1\")"
  echo "   -s, --strict               Perform a strict search, so input values must exist, disabling fuzzy search for version range"
  echo "                                e.g. \"1.1\" would not be detected in \"1.0 < 1.3\")"
  echo "   -t, --title    [term]      Search JUST the exploit title (Default is title AND the file's path)"
  echo "       --exclude=\"term\"       Remove values from results. By using \"|\" to separate, you can chain multiple values"
  echo "                                e.g. --exclude=\"term1|term2|term3\""
  echo "       --cve      [CVE]       Search for Common Vulnerabilities and Exposures (CVE) value"
  echo ""
  echo "## Output"
  echo "   -j, --json     [term]      Show result in JSON format"
  echo "   -o, --overflow [term]      Exploit titles are allowed to overflow their columns"
  echo "   -p, --path     [EDB-ID]    Show the full path to an exploit (and also copies the path to the clipboard if possible)"
  echo "   -v, --verbose              Display more information in output"
  echo "   -w, --www      [term]      Show URLs to Exploit-DB.com rather than the local path"
  echo "       --id                   Display the EDB-ID value rather than local path"
  echo "       --disable-colour       Disable colour highlighting in search results"
  echo ""
  echo "## Non-Searching"
  echo "   -m, --mirror   [EDB-ID]    Mirror (aka copies) an exploit to the current working directory"
  echo "   -x, --examine  [EDB-ID]    Examine (aka opens) the exploit using \$PAGER"
  echo ""
  echo "## Non-Searching"
  echo "   -h, --help                 Show this help screen"
  echo "   -u, --update               Check for and install any exploitdb package updates (brew, deb & git)"
  echo ""
  echo "## Automation"
  echo "       --nmap     [file.xml]  Checks all results in Nmap's XML output with service version"
  echo "                                e.g.: nmap [host] -sV -oX file.xml"
  echo ""
  echo "======="
  echo " Notes "
  echo "======="
  echo " * You can use any number of search terms"
  echo " * By default, search terms are not case-sensitive, ordering is irrelevant, and will search between version ranges"
  echo "   * Use '-c' if you wish to reduce results by case-sensitive searching"
  echo "   * And/Or '-e' if you wish to filter results by using an exact match"
  echo "   * And/Or '-s' if you wish to look for an exact version match"
  echo " * Use '-t' to exclude the file's path to filter the search results"
  echo "   * Remove false positives (especially when searching using numbers - i.e. versions)"
  echo " * When using '--nmap', adding '-v' (verbose), it will search for even more combinations"
  echo " * When updating or displaying help, search terms will be ignored"
  echo ""
  exit 2
}

## Update database check
function update() {
  arraylength="${#files_array[@]}"
  for (( i=0; i<${arraylength}; i++ )); do
    ## Check to see if we already have the value
    [[ "${tmp_package[*]}" =~ "${package_array[${i}]}" ]] \
      && continue

    ## Else save all the information
    tmp_git+=("${git_array[${i}]}")
    tmp_path+=("${path_array[${i}]}")
    tmp_package+=("${package_array[${i}]}")
  done

  ## Loop around all the new arrays
  arraylength="${#tmp_git[@]}"
  for (( i=0; i<${arraylength}; i++ )); do
    git="${tmp_git[${i}]}"
    path="${tmp_path[${i}]}"
    package="${tmp_package[${i}]}"

    ## Update from the repos (e.g. Kali)
    apt=$( apt-cache search "^${package}$" 2>/dev/null )        #dpkg -l "${package}" 2>/dev/null >/dev/null
    if [[ "$?" == "0" ]] && [[ "${apt}" != "" ]]; then
      updatedeb "${package}"
    else
      ## Update from homebrew (e.g. macOS/OSX)
      brew 2>/dev/null >/dev/null
      if [[ "$?" == "0" ]]; then
        ## This only really only updates "./searchsploit". The rest (can) come via git as its updated more frequently
        updatedbrew "${package}"
      fi

      ## Update via Git
      updategit "${package}" "${path}" "${git}"
    fi
  done

  ## Done
  exit 6
}

## Update database (via .deb/apt)
function updatedeb() {
  package_in="${1}"

  echo -e "[i] Updating via apt package management (Expect weekly-ish updates): ${package_in}\n"

  sudo apt update \
    || echo -e "\n[-] Issue with apt update (Please check network connectivity & apt SourcesList values)" 1>&2
  sudo apt -y install "${package_in}" \
    || echo -e "\n[-] Issue with apt upgrade" 1>&2

  echo -e "\n[*] apt update finished"
}

## Update database (via homebrew)
function updatedbrew() {
  package_in="${1}"

  echo -e "[i] Updating via brew package management\n"

  brew update \
    || echo -e "\n[-] Issue with brew update (Please check network connectivity)" 1>&2
  brew upgrade "${package_in}"

  echo -e "\n[*] Brew update finished"
}

## Update database (via Git)
function updategit() {
  package_in="${1}"
  path_in="${2}"
  git_in="${3}"

  echo -e "[i] Updating via Git (Expect daily updates): ${package_in} ~ ${path_in}\n"

  ## Make sure we are in the correct folder
  mkdir -p "${path_in}/" 2>/dev/null \
    || sudo mkdir -p "${path_in}/"
  cd "${path_in}/"

  ## Are we in a Git repo?
  if [[ "$( git rev-parse --is-inside-work-tree 2>/dev/null )" != "true" ]]; then
    if [[ "$( ls )" = "" ]]; then
      # If directory is empty, just clone
      echo -e "\n[-] Nothing here (${path_in}). Starting fresh..."
      git clone -v "${git_in}" "${path_in}/" 2>/dev/null \
        || sudo git clone -v "${git_in}" "${path_in}/"
    fi
  fi

  # Is our Git remote added? (aka wouldn't be via homebrew method)
  if [[ "$( git remote -v )" != *"upstream"*"${git_in}"* ]]; then
    echo -e "\n[-] Missing Git remote upstream (${git_in})"
    git init 2>/dev/null \
      || sudo git init
    git remote add upstream "${git_in}" 2>/dev/null \
      || sudo git remote add upstream "${git_in}"
  fi

  # Make sure to prep checkout first
  git checkout -- . 2>/dev/null \
    || sudo git checkout -- .

  # Get origin branch name
  origin_branch=$(git branch -l master main | sed 's/^* //')

  # Update from git
  echo -e "\n[i] Git pull'ing"
  git pull -v upstream $origin_branch 2>/dev/null \
    || sudo git pull -v upstream $origin_branch

  # If conflicts, clean and try again
  if [[ "$?" -ne 0 ]]; then
    echo -e "\n[-] Git conflict"
    git clean -d -fx "" \
      || sudo git clean -d -fx ""
    git pull -v upstream $origin_branch \
      || sudo git pull -v upstream $origin_branch
  fi

  echo -e "\n[*] Git update finished"
  echo "[i] Path: ${path_in}/"
}

## Printing dotted lines in the correct manner
function drawline() {
  printf "%0.s-" $( eval echo {1..$(( COL1 + 1 ))} )
  echo -n " "
  printf "%0.s-" $( eval echo {1..$(( COL2 - 1 ))} )
  echo ""
}

## Used in searchsploitout/nmap's XML
function validterm() {
  ## Check to see if its any phrases which would give a TON of incorrect results
  if   [ "$( echo ${1} | tr '[:upper:]' '[:lower:]' )" == "microsoft" ] \
    || [ "$( echo ${1} | tr '[:upper:]' '[:lower:]' )" == "microsoft windows" ] \
    || [ "$( echo ${1} | tr '[:upper:]' '[:lower:]' )" == "windows" ] \
    || [ "$( echo ${1} | tr '[:upper:]' '[:lower:]' )" == "apache" ] \
    || [ "$( echo ${1} | tr '[:upper:]' '[:lower:]' )" == "ftp" ] \
    || [ "$( echo ${1} | tr '[:upper:]' '[:lower:]' )" == "http" ] \
    || [ "$( echo ${1} | tr '[:upper:]' '[:lower:]' )" == "linux" ] \
    || [ "$( echo ${1} | tr '[:upper:]' '[:lower:]' )" == "net" ] \
    || [ "$( echo ${1} | tr '[:upper:]' '[:lower:]' )" == "network" ] \
    || [ "$( echo ${1} | tr '[:upper:]' '[:lower:]' )" == "oracle" ] \
    || [ "$( echo ${1} | tr '[:upper:]' '[:lower:]' )" == "ssh" ] \
    || [ "$( echo ${1} | tr '[:upper:]' '[:lower:]' )" == "unknown" ]; then
      echo -e "[-] Skipping term: ${1}   (Term is too general. Please re-search manually: $0 ${arg} ${1})\n" 1>&2
      ## Issues, return with something
      return 1
  fi

  ## No issues, return without anything
  return 0
}

## Used in searchsploitout/nmap's XML
function searchsploitout() {
  ## Make sure there is a value
  [ "${software}" = "" ] \
    && return

  #echo "" 1>&2

  arg="-t"   ## Title search by default!
  [[ "${COLOUR}" != "1" ]] \
    && arg="${arg} --disable-colour"
  [[ "${EDBID}" == "1" ]] \
    && arg="${arg} --id"
  [[ "${JSON}" == "1" ]] \
    && arg="${arg} --json"
  [[ "${OVERFLOW}" == "1" ]] \
    && arg="${arg} --overflow"
  [[ "${FUZZY}" != "1" ]] \
    && arg="${arg} --strict"
  [[ "${WEBLINK}" == "1" ]] \
    && arg="${arg} --www"

  ## Try and remove terms that could confuse searches
  #software=$( echo "${software}" | sed 's_/_ _g' )
  software=$( echo "${software}" | sed -e 's/[^a-zA-Z0-9.]/ /g' )

  if [[ "${VERBOSE}" -eq 1 ]]; then
    ## Loop each word?
    tmp=""
    for word in $( echo ${software} ); do
      ## Add current search term on
      tmp="${tmp}${word}"

      ## Check to see if its any phrases which would give a TON of incorrect results
      validterm "${tmp}" \
        || continue

      ## Feedback
      echo "[i] $0 ${arg} ${tmp}" 1>&2
      out=$( bash "$0" ${arg} ${tmp} )

      ## Are there too many results?
      lines=$( echo -e "${out}" | wc -l )
      if [[ "${lines}" -gt 100 ]]; then
        echo -e "[-] Skipping output: ${tmp}   (Too many results, 100+. You'll need to force a search: $0 ${arg} ${tmp})\n" 1>&2
      ## Are there any results?
      elif [[ "${lines}" -gt 5 ]]; then
        echo -e "${out}\n\n"
      ## If there's no results
      else
        ## Exit for loop
        break
      fi

      ## Space out for the next word
      tmp="${tmp} "
    done

    ## Padding between loops
    echo -e "\n\n" 1>&2
  else
    ## Check to see if its any phrases which would give a TON of incorrect results
    validterm "${software}" \
      || return

    ## Feedback
    echo "[i] $0 ${arg} ${software}" 1>&2
    out=$( bash "$0" ${arg} ${software} )

    ## Are there too many results?
    lines=$( echo -e "${out}" | wc -l )
    if [[ "${lines}" -gt 100 ]]; then
      echo -e "[-] Skipping output: ${software}   (Too many results, 100+. You'll need to force a search: $0 ${arg} ${software})\n" 1>&2
    ## Are there any results?
    elif [[ "${lines}" -gt 5 ]]; then
      echo -e "${out}\n\n"
    fi
  fi
}

## Read XML file
function nmapxml() {
  ## Feedback to the end user
  echo -e "[i] Reading: '${FILE}'\n" 1>&2

  ## Read in XMP (IP, name, service and version)
  xmllint --xpath '//address/@addr|//service/@name|//service/@product|//service/@version' "${FILE}" \
    | sed -e $'s/addr=/\\\n[IP] /g;   s/name=/\\\n[NAME] /g;   s/product=/\\\n[PRODUCT] /g;   s/version=/\\\n[VERSION] /g;   s/"//g' \
    | grep -v '\[IP\].*\:' \
    | while read line; do
      type=$( echo "${line}" | cut -d" " -f 1 )
      input=$( echo "${line}" | cut -d" " -f 2- )

      case "${type}" in
        "[IP]")
          #[[ "${VERBOSE}" -eq 1 ]] && echo -e "\n\n\e[32m[*] IP: ${input}\e[39m" 1>&2
          ;;
        "[NAME]")
          ## If we have already looped around and got something, save it before moving onto the current value
          if [[ "${software}" ]]; then
            #searchsploitout
            echo "${software}"
          fi
          ## Something is better than nothing. Will just go on the default service that matches the port.   e.g. domain
          software="${input}"
          ## Might not get any more than this, if -sV failed
          ;;
        "[PRODUCT]")
          ## We have a name, but no version (yet?)   e.g. dnsmasq
          echo "${software}"
          software="${input}"
          echo "${software}"
          ;;
        "[VERSION]")
          software="${software} ${input}"
          ## Name & version. There isn't any more information to get, game over.   e.g. dnsmasq 2.72
          echo "${software}"
          software=
          ;;
      esac
  done | tr '[:upper:]' '[:lower:]' | awk '!x[$0]++' | while read software; do
    searchsploitout
  done
}

## Build search terms
function buildterms() {
  tag_in="${1}"

  ## If we are to use colour ("--disable-colour"), add the values to search for between "or"
  if [[ "${COLOUR}" -eq 1 ]]; then
    [[ "${COLOUR_TAG}" ]] \
      && COLOUR_TAG="${COLOUR_TAG}|"
    COLOUR_TAG="${COLOUR_TAG}${tag_in}"
  fi

  ## Some regex to try and detect version
  ## Basic: major.minor[.build][.revision]  // major.minor[.maintenance][.build]  -- example: 1.2.3.4)
  ## Plus alphanumeric (e.g. alpha, beta): 1a, 2.2b, 3.3-c, 4.4-rc4, 5.5-r
  if ! echo "${tag_in}" | grep ${REGEX_GREP} -q "^(\d+)(\.?\d*)(\.?\d*)((\.|\-)?(\w*))$"; then
    FUZZY_SEARCH="${FUZZY_SEARCH} | grep ${COLOUR_OFF_GREP} -F ${CASE_TAG_GREP} \"${tag_in}\""
  fi

  ## Search both title AND path
  if [[ "${FILEPATH}" -eq 1 ]]; then
    ## Search command for each term (with case sensitive flag, "-c")
    SEARCH="${SEARCH} | grep ${COLOUR_OFF_GREP} -F ${CASE_TAG_GREP} \"${tag_in}\""

  ## Search just the title, NOT the path ("-t"/"-e")
  else
    ## If there is already a value, prepend text to get ready
    [[ "${AWK_SEARCH}" ]] \
      && AWK_SEARCH="${AWK_SEARCH}/ && ${CASE_TAG_FGREP}(\$3) ~ /"

    ## Escape any slashes
    tag_in="$( echo ${tag_in} | sed 's_/_\\/_g' )"

    ## Case sensitive ("-c")
    if [[ "${SCASE}" -eq 1 ]]; then
      AWK_SEARCH="${AWK_SEARCH}${tag_in}"
    else
      AWK_SEARCH="${AWK_SEARCH}$( echo ${tag_in} | tr '[:upper:]' '[:lower:]' )"
    fi
  fi
}

## Read in the values from files_*.csv
function findresults() {
  file_in="${1}"
  path_in="${2}"
  name_in="${3}"

  if [[ "${name_in}" == "Paper"* ]]; then
    url="papers"
  elif [[ "${name_in}" == "Shellcode"* ]]; then
    url="shellcodes"
  else
    url="exploits"
  fi

  ## JSON require full options ("--json")
  if [[ "${JSON}" -eq 1 ]] || [[ "${FUZZY}" -eq 1 ]]; then
    ## Read (id, path, title, date_published, author, type, platform, port, date_added, date_updated, verified, codes, tags, aliases, screenshot_url, application_url, source_url) separated between commas
    ## Needs to end with a `,` to match the awk search later for FUZZY_SEARCH with "sort -u"
    SEARCH="awk -F '[,]' '{print \$1\",\"\$2\",\"\$3\",\"\$4\",\"\$5\",\"\$6\",\"\$7\",\"\$8\",\"\$9\",\"\$10\",\"\$11\",\"\$12\",\"\$13\",\"\$14\",\"\$15\",\"\$16\",\"\$17}' \"${path_in}/${file_in}\""
    ## Read (id, path, title) separated between commas & search for less than (and grater than values) too
    FUZZY_SEARCH="awk -F '[,]' '{print \$1\",\"\$2\",\"\$3}' \"${path_in}/${file_in}\" | grep ${COLOUR_OFF_GREP} \"<\|>\""
  ## CVE ("--cve")
  elif [[ "${CVE}" -eq 1 ]]; then
    ## Read (id, path, title, codes) separated between commas (as these are the visible/common fields)
    SEARCH="awk -F '[,]' '{print \$1\",\"\$2\",\"\$3\",\"\$12}' \"${path_in}/${file_in}\""
  else
    ## Read (id, path, title) separated between commas (as these are the only visible fields)
    SEARCH="awk -F '[,]' '{print \$1\",\"\$2\",\"\$3}' \"${path_in}/${file_in}\""
  fi

  ## EXACT search command ("-e")
  if [[ "${EXACT}" -eq 1 ]]; then
    buildterms "${TAGS}"
  ## or AND search command?
  else
    ## For each term
    for TAG in ${TAGS}; do
      buildterms "${TAG}"
    done
  fi

  ## If we are NOT to use the path name ("-t"/"-e")
  [[ "${FILEPATH}" -eq 0 ]] \
    && SEARCH="${SEARCH} | awk -F '[,]' '${CASE_TAG_FGREP}(\$3) ~ /${AWK_SEARCH}/ {print}'"

  ## Remove any terms not wanted from the search
  [[ "${EXCLUDE}" ]] \
    && SEARCH="${SEARCH} | grep ${REGEX_GREP} -vi '${EXCLUDE}'"
  [[ "${EXCLUDE}" ]] && [[ "${FUZZY}" -eq 1 ]] \
    && FUZZY_SEARCH="${FUZZY_SEARCH} | grep ${REGEX_GREP} -vi '${EXCLUDE}'"

  ## If we are to use colour ("--disable-colour"), add the value here
  if [[ "${COLOUR_TAG}" ]] && [[ "${JSON}" -eq 0 ]]; then
    COLOUR_TAG="grep ${COLOUR_ON_GREP} -iE \"${COLOUR_TAG}|$\""
  fi

  ## Dynamically set column widths to the current screen size
  [[ "${WEBLINK}" -eq 1 ]] \
    && COL2=45 \
    || COL2=$(( 34 )) ## Max length + 2 ~ $ find .  ! -path '*/.*' -type f | awk '{ print length, $0 }' | sort -n -s | cut -d" " -f2- | tail -n 1
    #|| COL2=$(( ${#path_in} + 21 ))

  COL1=$(( $( tput cols ) - COL2 - 1 ))

  ## Search, format, and print results (--overflow)
  [[ "${OVERFLOW}" -eq 1 ]] \
    && FORMAT_COL1=${COL1} \
    || FORMAT_COL1=${COL1}'.'${COL1}

  ## Maximum length COL2 can be
  FORMAT_COL2=$(( ${COL2} - 2 ))

  ## Are we doing a fuzzy search & did we manage to detect the version
  if [[ "${FUZZY}" -eq 1 ]] && [[ -n "${VERSION}" ]]; then
    ## SubShells - http://mywiki.wooledge.org/BashFAQ/024
    while IFS= read -r TITLE; do
      while IFS= read -r RANGE; do
        ## Read in input and trim
        MIN=$( echo "${RANGE}" | awk -F '<' '{print $1}' | xargs )
        MAX=$( echo "${RANGE}" | awk -F '<' '{print $2}' | xargs )

        ## As its optional to put it, set a value if blank
        [ -z "${MIN}" ] \
          && MIN=0

        RESULT="$( printf '%s\n' "${MIN}" "${VERSION}" "${MAX}" | sort -V )"
        ## Sub if sort -v isn't working?   if (( $( echo "${MIN} <= ${VERSION}" | bc -l ) )) && (( $( echo "${MAX} >= ${VERSION}" | bc -l ) )); then
        ## ...else there is dpkg (if Debian)
        if [[ "$( echo "${RESULT}" | head -n 1 )" == "${MIN}" ]] && [[ "$( echo "${RESULT}" | tail -n 1 )" == "${MAX}" ]]; then
            [ -n "${ID}" ] \
              && ID="${ID}|"
            ID="${ID}$( echo ${TITLE} | awk -F ',' '{print $1}' )"
            ## Found one, no point going on
            break
        fi
      done < <(
        echo "${TITLE}" \
          | grep ${REGEX_GREP} -o "((\d+)(\.?\d*)(\.?\d*)((\.|\-)?(\d|x)*)(\s*))?((<|>)=?)(\s*)(\d+)(\.?\d*)(\.?\d*)((\.|\-)?(\d|x)*)" \
          | sed 's_=__;   s_>_<_'
      )
      ## Do the same search (just without the version) & loop around all the exploit titles (as thats where the versions are)
      ## Two main "parts"
      ##   (a.b.c.d )(<= e.f.g.h)
      ## This can be broken down more:
      ##   Group 1  == a & e == major             = [0-9]
      ##   Group 2  == b & f == minor             = .[0-9]     (optional)
      ##   Group 3  == c & g == build/maintenance = .[0-9]     (optional)
      ##   Group 4a == d & h == revision/build    = . OR -     (optional)
      ##   Group 4b ==                            = x OR [0-9] (optional)
      ## So it really is more like ~ (a)(.b)(.c)(.d)( )(<=)( )(e)(.f)(.g)(.h)
      ## NOTE: ..."x" is used as a wild card in titles
      ## Quick regex recap
      ##   Digit     == \d
      ##   Space     == \s
      ##   Group     == ( )
      ##   OR        == |
      ##   1 or more == +
      ##   0 or more == *
      ##   0 or 1    == ?
      ## Should support:
      ##   Exploit < 1 / <= 1.2 / < 1.2.3.4 / < 1.2.3.x
      ##   Exploit 1.0 < 1.2.3.4
      ## ...This can be better so it doesn't search in brackets: "Linux Kernel (Solaris 10 / < 5.10 138888-01) - Local Privilege Escalation"
    done < <(
        eval "${FUZZY_SEARCH}"
    )
  fi

  ## Magic search Fu + strip double quotes + Fix any escaping `\` (need todo it again for JSON only later: issues/#173)
  OUTPUT="$(
    ( \
      eval ${SEARCH}; \
      awk "/^(${ID}),/ {print}" "${path_in}/${file_in}" \
    ) \
    | sed 's/\"//g;   s_\\_\\\\_g' \
    | sort -u
  )"

  ## If there are no results, no point going on
  [[ -z "${OUTPUT}" ]] \
    && return

  ## Print JSON format (full options) ("--json")?
  if [[ "${JSON}" -eq 1 ]]; then
    ## Web link format ("--json --www")?
    if [[ "${WEBLINK}" -eq 1 ]]; then
      OUTPUT="$( echo "${OUTPUT}" \
        | sed 's_\\_\\\\_g' \
        | awk -F ',' '{ printf "\\n\\t\\t'{'\"Title\":\"%s\",\"URL\":\"https://www.exploit-db.com/'${url}'/%s\"},", $3, $1 }' )"
    ## Just the EDB-ID ("--json --id")?
    elif [[ "${EDBID}" -eq 1 ]]; then
      OUTPUT="$( echo "${OUTPUT}" \
        | sed 's_\\_\\\\_g' \
        | awk -F ',' '{ printf "\\n\\t\\t'{'\"Title\":\"%s\",\"EDB-ID\":\"%s\",\"Path\":\"'${path_in}/'%s\"},", $3, $1, $2 }' )"
    ## Default JSON ("--json")?
    else
      OUTPUT="$( echo "${OUTPUT}" \
        | sed 's_\\_\\\\_g' \
        | awk -F ',' '{ printf "\\n\\t\\t'{'\"Title\":\"%s\",\"EDB-ID\":\"%s\",\"Date_Published\":\"%s\",\"Date_Added\":\"%s\",\"Date_Updated\":\"%s\",\"Author\":\"%s\",\"Type\":\"%s\",\"Platform\":\"%s\",\"Port\":\"%s\",\"Verified\":\"%s\",\"Codes\":\"%s\",\"Tags\":\"%s\",\"Aliases\":\"%s\",\"Screenshot\":\"%s\",\"Application\":\"%s\",\"Source\":\"%s\",\"Path\":\"'${path_in}/'%s\"},", $3, $1, $4, $9, $10, $5, $6, $7, $8, $11, $12, $13, $14, $15, $16, $17, $2}' )"
    fi
    OUTPUT="$( echo -e ${OUTPUT} \
      | sort -f \
      | sed '$ s/,$//' )"
  ## Web link format ("--www")?
  elif [[ "${WEBLINK}" -eq 1 ]]; then
    OUTPUT="$( echo "${OUTPUT}" \
      | awk -F ',' '{ printf "%-'${FORMAT_COL1}'s | %s\n", $3, "https://www.exploit-db.com/'${url}'/"$1 }' \
      | sort -f )"
  ## Just the EDB-ID ("--id")?
  elif [[ "${EDBID}" -eq 1 ]]; then
    OUTPUT="$( echo "${OUTPUT}" \
      | awk -F ',' '{ printf "%-'${FORMAT_COL1}'s | %s\n", $3, $1 }' \
      | sort -f )"
  ## Default view
  else
    OUTPUT="$( echo "${OUTPUT}" \
      | sed 's_,exploits/_,_;   s_,shellcodes/_,_;   s_,papers/_,_' \
      | awk -F ',' '{ printf "%-'${FORMAT_COL1}'s | %.'${FORMAT_COL2}'s\n", $3, $2 }' \
      | sort -f )"
  fi

  ## Display colour highlights ("--disable-colour")?
  if [[ "${COLOUR_TAG}" ]] && [[ "${JSON}" -eq 0 ]] && [[ "${OUTPUT}" ]]; then
    OUTPUT=$( echo -e "${OUTPUT}" | eval ${COLOUR_TAG} )
  fi
}

function printresults() {
  title_in="${1}"
  path_in="${2}"
  json_title="$( echo ${title_in} | tr /a-z/ /A-Z/ )"

  ## Print header if in JSON ("--json")
  if [[ "${JSON}" -eq 1 ]]; then
    printf ",\n\t\"DB_PATH_${json_title}\": \"${path_in}\",\n"
    printf "\t\"RESULTS_${json_title}\": ["
  ## ASCII table
  else
    drawline
    printf "%-${COL1}s %s" " ${title_in} Title"
    if [[ "${WEBLINK}" -eq 1 ]]; then
      echo "|  URL"
    elif [[ "${EDBID}" -eq 1 ]]; then
      echo "|  EDB-ID"
    else
      echo "|  Path"
      #echo "    > Results (0)"
      #
      #printf "%-${COL1}s "
      #echo "| (${path_in}/)"
    fi
    drawline
  fi

  ## Show content
  [[ "${OUTPUT}" ]] \
    && echo "${OUTPUT}"

  ## Print footer if in JSON ("--json")
  if [[ "${JSON}" -eq 1 ]]; then
    printf "\t]"
  else
    drawline
  fi
}

#-----------------------------------------------------------------------------#

## Locate setting file
## User home folder config
if [[ -f "${HOME}/.searchsploit_rc" ]]; then
  rc_file="${HOME}/.searchsploit_rc"
## Global config
elif [[ -f "/etc/searchsploit_rc" ]]; then
  rc_file="/etc/searchsploit_rc"
## Method #1 - File itself
elif [[ -f "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/.searchsploit_rc" ]]; then
  rc_file="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/.searchsploit_rc"
## Method #2 - Symbolic link
elif [[ -f "$( dirname "$( readlink "$0" )" )/.searchsploit_rc" ]]; then
  rc_file="$( dirname "$( readlink "$0" )" )/.searchsploit_rc"
## Manually specified?
elif [[ ! -f "${rc_file}" ]]; then
  echo "[!] Could not find: rc_file ~ ${rc_file}"
  exit 1
fi

## Use config file
source "${rc_file}"

#-----------------------------------------------------------------------------#

## Check for empty arguments
if [[ $# -eq 0 ]]; then
  usage >&2
fi

## Parse long arguments
ARGS="-"
for param in "$@"; do
  if [[ "${param}" == "--case" ]]; then
    SCASE=1
  elif [[ "${param}" == "--disable-colour" ]] || [[ "${param}" == "--disablecolour" ]] || [[ "${param}" == "--disable-color" ]] || [[ "${param}" == "--disablecolor" ]]; then
    COLOUR=""
  elif [[ "${param}" == "--cve" ]]; then
    CVE=1
  elif [[ "${param}" == "--exact" ]]; then
    EXACT=1
  elif [[ "${param}" == "--examine" ]] || [[ "${param}" == "--open" ]] || [[ "${param}" == "--view" ]]; then
    GETPATH=1
    EXAMINE=1
  elif [[ "${param}" == "--help" ]]; then
    usage >&2
  elif [[ "${param}" == "--id" ]]; then
    EDBID=1
  elif [[ "${param}" == "--json" ]]; then
    JSON=1
  elif [[ "${param}" == "--mirror" ]] || [[ "${param}" == "--copy" ]] || [[ "${param}" == "--dup" ]] || [[ "${param}" == "--duplicate" ]]; then
    GETPATH=1
    MIRROR=1
  elif [[ "${param}" == "--nmap" ]]; then
    XML=1
  elif [[ "${param}" == "--overflow" ]]; then
    OVERFLOW=1
  elif [[ "${param}" == "--path" ]]; then
    GETPATH=1
    CLIPBOARD=1
  elif [[ "${param}" == "--strict" ]]; then
    FUZZY=0
  elif [[ "${param}" == "--title" ]]; then
    FILEPATH=0
  elif [[ "${param}" == "--update" ]]; then
    update
  elif [[ "${param}" == "--verbose" ]]; then
    VERBOSE=1
  elif [[ "${param}" == "--www" ]]; then
    WEBLINK=1
  elif [[ "${param}" =~ "--exclude=" ]]; then
    EXCLUDE="$( echo "${param}" | cut -d '=' -f 2- )"
  else
    if [[ "${param:0:1}" == "-" ]]; then
      ARGS=${ARGS}${param:1}
      shift
      continue
    fi
    TAGS="${TAGS} ${param//[\`\']/_}"
  fi
done

## Parse short arguments
while getopts "cehjmnopstuvwx" arg "${ARGS}"; do
  if [[ "${arg}" = "?" ]]; then
    usage >&2;
  fi
  case ${arg} in
    c) SCASE=1;;
    e) EXACT=1;;
    h) usage >&2;;
    j) JSON=1;;
    m) GETPATH=1; MIRROR=1;;
    n) XML=1;;
    o) OVERFLOW=1;;
    p) GETPATH=1; CLIPBOARD=1;;
    s) FUZZY=0;;
    t) FILEPATH=0;;
    u) update;;
    v) VERBOSE=1;;
    w) WEBLINK=1;;
    x) GETPATH=1; EXAMINE=1;;
  esac
  shift $(( OPTIND - 1 ))
done

#-----------------------------------------------------------------------------#

## Check for files_*.csv
arraylength="${#files_array[@]}"
for (( i=0; i<${arraylength}; i++ )); do
  files="${path_array[${i}]}/${files_array[${i}]}"

  if [[ -f "${files}" ]]; then
    continue
  ## Method #1 - File itself
  elif [[ -f "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/${files_array[${i}]}" ]]; then
    echo "[i] Found (#1): $( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/${files_array[${i}]}" 1>&2
    echo "[i] To remove this message, please edit \"${rc_file}\" which has \"package_array: ${package_array[${i}]}\" to point too: path_array+=(\"$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )\")" 1>&2
    echo 1>&2
    path_array[${i}]="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
  ## Method #2 - Symbolic link
  elif [[ -f "$( dirname "$( readlink "$0" )" )/${files_array[${i}]}" ]]; then
    echo "[i] Found (#2): $( dirname "$( readlink "$0" )" )/${files_array[${i}]}" 1>&2
    echo "[i] To remove this message, please edit \"${rc_file}\" which has \"package_array: ${package_array[${i}]}\" to point too: path_array+=(\"$( dirname "$( readlink "$0" )" )\")" 1>&2
    echo 1>&2
    path_array[${i}]="$( dirname "$( readlink "$0" )" )"
  else
    #echo "[!] Could not find: ${files}" 1>&2
    #echo "[i] To remove this message, please remove \"${files_array[${i}]}\" (package_array: ${package_array[${i}]}) from \"${rc_file}\"" 1>&2
    #echo 1>&2
    unset "files_array[${i}]"
    unset "path_array[${i}]"
    unset "name_array[${i}]"
    unset "git_array[${i}]"
    unset "package_array[${i}]"
  fi
done

#-----------------------------------------------------------------------------#

## Read in XML
if [[ "${XML}" -eq 1 ]]; then
  ## Trim white spaces
  FILE=$( echo ${TAGS} | xargs )

  ## Is there a file?
  if [[ ! -f "${FILE}" ]]; then
    echo -e "\n[!] Could not find file: ${FILE}" 1>&2
    exit 1
  fi

  if ! hash xmllint 2>/dev/null; then
    echo -e "\n[!] Please install xmllint" 1>&2
    echo -e "[i]   Kali Linux: sudo apt -y install libxml2-utils" 1>&2
    exit 1
  fi

  if [[ "${VERBOSE}" -ne 1 ]]; then
    echo "[i] SearchSploit's XML mode (without verbose enabled).   To enable: ${progname} -v --xml..." 1>&2
  fi

  ## Do the magic
  nmapxml

  ## Done
  exit 0
fi

## Print the full path. If pbcopy/xclip is available then copy to the clipboard
if [[ "${GETPATH}" -eq 1 ]]; then
  for exploit in ${TAGS}; do
    ## Get EDB-ID from input
    edbdb="$( echo ${exploit} | rev | cut -d '/' -f1 | rev | cut -d'-' -f1 | cut -d'.' -f1 | tr -dc '0-9' )"

    ## Loop until we find something
    arraylength="${#files_array[@]}"
    for (( i=0; i<${arraylength}; i++ )); do
      files="${path_array[${i}]}/${files_array[${i}]}"

      ## Check to see if the files_*.csv has a value
      line=$( grep -m 1 -E "^${edbdb}," "${files}" )

      if [[ "${line}" ]]; then
        path="$( echo ${line} | cut -d ',' -f 2 )"
        location="${path_array[${i}]}/${path}"
        name="${name_array[${i}]}"

        if [[ "${name}" == "Paper"* ]]; then
          url="papers/${edbdb}"
        elif [[ "${name}" == "Shellcode"* ]]; then
          url="shellcodes/${edbdb}"
        else
          url="exploits/${edbdb}"
        fi

        break
      fi
    done

    ## Did we find the exploit?
    if [[ -f "${location}" ]]; then
      ## Get title
      title=$( grep -m 1 "${path}" "${files}" | cut -d ',' -f 3 | sed 's/"//g' )

      ## Get codes
      codes=$( grep -m 1 "${path}" "${files}" | cut -d ',' -f 12 | sed 's/"//g; s/;/, /g' )
      if [ -z "${codes}" ]; then
        codes="N/A"
      fi

      ## Get verified status
      verified=$( grep -m 1 "${path}" "${files}" | cut -d ',' -f 11 | sed 's/"//g' )
      if [ "${verified}" = "1" ]; then
        verified="True"
      else
        verified="False"
      fi

      ## File type
      fileinfo="$( file -b "${location}" 2>/dev/null || echo "<missing file package>" )"

      ## How long is the name?
      PADDING=$(( 9 - ${#name} ))

      ## Display out
      printf "%-${PADDING}s%s"
      echo "${name}: ${title}"
      echo "      URL: https://www.exploit-db.com/${url}"
      ## Path is useful doing --mirror
      echo "     Path: ${location}"
      echo "    Codes: ${codes}"
      echo " Verified: ${verified}"
      echo "File Type: ${fileinfo}"
#     echo ""

      ## Copy to clipboard?
      if [[ "${CLIPBOARD}" -eq 1 ]]; then
        ## Are any copy programs available?
        if hash xclip 2>/dev/null || hash pbcopy 2>/dev/null; then
          ## Linux (Will require ${DISPLAY})
          if hash xclip 2>/dev/null; then
            echo -ne "${location}" | xclip -selection clipboard 2>/dev/null
            echo "Copied EDB-ID #${edbdb}'s path to the clipboard"
          ## macOS/OSX
          elif hash pbcopy 2>/dev/null; then
            echo -ne "${location}" | pbcopy
            echo "Copied EDB-ID #${edbdb}'s path to the clipboard"
          fi
        fi

        ## Done (early!)
        exit 0
      fi

      ## Open the exploit up?
      if [[ "${EXAMINE}" -eq 1 ]]; then
        if [[ "${PAGER}" ]]; then
          /bin/sh -c "${PAGER} ${location}"
        elif [[ -f "$( which pager 2>/dev/null )" ]]; then
          pager "${location}"
        else
          less "${location}"
        fi
        echo -e "\n"
      fi

      if [[ "${MIRROR}" -eq 1 ]]; then
        cp -i "${location}" "$( pwd )/"
        echo "Copied to: $( pwd )/$( basename ${location} )"
        echo -e "\n"
      fi
    else
      ## Feedback
      echo "[!] Could not find EDB-ID #${edbdb}"
      echo -e "\n"
    fi
  done

  ## Done
  exit 0
fi

#-----------------------------------------------------------------------------#

## Are we are doing an exact match ("-e")? If so, do NOT check folder path (Implies "-t").
[[ "${EXACT}" -eq 1 ]] \
  && FILEPATH=0

## Case sensitive ("-c"), remove the default flags
[[ "${SCASE}" -eq 1 ]] \
  && CASE_TAG_GREP="" \
  && CASE_TAG_FGREP=""

## Remove leading space
TAGS="$( echo ${TAGS} | sed -e 's/^[[:space:]]//' )"

## Check to see if the version of "sort" is supported
echo | sort -V 2>/dev/null >/dev/null
if [ $? -ne "0" ]; then
  echo "[-] 'sort' doesn't support '-V'" 1>&2
  echo "[i] Enabling '${progname} --strict'" 1>&2
  FUZZY=0
fi

## Some regex to try and detect version
## Basic: major.minor[.build][.revision]  // major.minor[.maintenance][.build]  -- example: 1.2.3.4)
## Plus alphanumeric (e.g. alpha, beta): 1a, 2.2b, 3.3-c, 4.4-rc4, 5.5-r
for tag_in in ${TAGS}; do
  if echo "${tag_in}" | grep ${REGEX_GREP} -q "^(\d+)(\.?\d*)(\.?\d*)((\.|\-)?(\w*))$"; then
    ## 1.2.3-4abc
    VERSION=$( echo "${tag_in}" | grep ${REGEX_GREP} -o "^(\d+)(\.?\d*)(\.?\d*)((\.|\-)?(\w*))$" )
    [[ -n "${VERSION}" ]] && [[ "${VERBOSE}" -eq 1 ]] \
      && echo "[i] Version ID: ${VERSION}"

    ## 1.2.3-4
    CLEANVERSION=$( echo "${tag_in}" | grep ${REGEX_GREP} -o "^(\d*\.?)(\d*\.?)(\d*\.?)((\.|\-)?(\d+))" )
    if [[ -n "${CLEANVERSION}" ]] && [[ "${CLEANVERSION}" != "${VERSION}" ]]; then
      VERSION="${CLEANVERSION}"

      [[ "${VERBOSE}" -eq 1 ]] \
        && echo "[i] Clean ID: ${VERSION}"
    fi
  fi
done

## Did not get a version? If so, no point doing a fuzzy search
if [[ "${FUZZY}" -eq 1 ]] && [[ -z "${VERSION}" ]] && [[ "${VERBOSE}" -eq 1 ]]; then
  echo "[i] Unable to detect version in terms: ${TAGS}" 1>&2
  echo "[i] Enabling '${progname} --strict'" 1>&2
  FUZZY=0
fi

## Is it just a single tag, disable fuzzy
[[  "${TAGS}" != *" "* ]] \
  && FUZZY=0

#-----------------------------------------------------------------------------#

## Print header if in JSON ("--json")
[[ "${JSON}" -eq 1 ]] \
  && printf "{\n\t\"SEARCH\": \"${TAGS}\""

## Check for files_*.csv
arraylength="${#files_array[@]}"
for (( i=0; i<${arraylength}; i++ )); do
  ## Search
  findresults "${files_array[${i}]}" "${path_array[${i}]}" "${name_array[${i}]}"
  ## Print results if in JSON ("--json") or if there are any results
  if ([[ "${JSON}" -eq 1 ]] || [[ "${OUTPUT}" ]]); then
    printresults "${name_array[${i}]}" "${path_array[${i}]}"
  ## Summary if NOT JSON ("--json")
  elif [[ "${JSON}" -eq 0 ]]; then
    echo "${name_array[${i}]}s: No Results"
  fi
  ## Reset
  COLOUR_TAG=""
done

## Print footer if in JSON ("--json")
[[ "${JSON}" -eq 1 ]] \
  && printf "\n}\n"

## Done
exit 0
