blob: 5f407fcff5a07963221096931f612ea84289ad98 [file] [log] [blame]
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Copyright (c) 2024 - Greg Kroah-Hartman <gregkh@linuxfoundation.org>
#
# bippy - creates a json and/or mbox file on standard output in the proper
# format to submit a CVE based on a specific git SHA.
#
# Usage:
# bippy [loads of options, see the help text below]
#
# Right now only works with CVEs, will handle other identifiers as needed.
#
# Name comes from the phrase "you bet your bippy!" as said by David L. Morse.
#
# Requires:
# A kernel git tree with the SHA to be used in it
# jo - the json output tool, found at: https://github.com/jpmens/jo
# dyad - tool to find matching pairs of vulnerable:fixed kernel ids for a specific fix
# set to 1 to get some debugging logging messages (or use -v/--verbose option)
DEBUG=0
KERNEL_TREE=${CVEKERNELTREE}
COMMIT_TREE=${CVECOMMITTREE}
if [[ ! -d "${KERNEL_TREE}" ]] || [[ ! -d "${COMMIT_TREE}" ]]; then
echo "CVEKERNELTREE needs setting to the stable repo directory"
echo "CVECOMMITTREE needs setting to the Stable commit tree"
echo -e "\nEither manually export them or add them to your .bashrc/.zshrc et al."
echo -e "\nSee HOWTO in the root of this repo"
exit 1
fi
# don't use unset variables
set -o nounset
# set where the tool was run from,
# the name of our script,
# and the git version of it
DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
SCRIPT=${0##*/}
SCRIPT_VERSION=$(cd "${DIR}" && git ls-tree --abbrev=12 HEAD | grep "${SCRIPT}" | awk '{print $3}' | head -n 1)
# location of dyad helper script
dyad="${DIR}/dyad"
help() {
echo "Usage: $0 [OPTIONS]"
echo "Create a JSON or MBOX file to report a CVE based on a specific Linux kernel"
echo "git sha value."
echo ""
echo "Arguments:"
echo " -c, --cve=CVE_NUMBER The full CVE number to assign"
echo " -s, --sha=GIT_SHA The kernel git sha1 to assign the CVE to"
echo " --vulnerable=GIT_SHA The kernel git sha1 that this issue became vulnerable at. (optional)"
echo " -j, --json=JSON_FILENAME Output a JSON report to submit to CVE to the"
echo " specified filename"
echo " -m, --mbox=MBOX_FILENAME Output a mbox file to submit to the CVE"
echo " announce mailing list"
echo " --diff=DIFF_FILENAME File containing a diff for the changelog text to be applied. (optional)"
echo " --reference=REFERENCE_FILENAME File containing a list of url references to add to the json record. (optional)"
echo " -u, --user=EMAIL Email of user creating the record."
echo " -n, --name=NAME Name of the user creating the record."
echo " -h, --help This information"
echo " -v, --verbose Show debugging information to stdout"
echo ""
echo "Note, CVE_NUMBER and GIT_SHA are required, as well as at least one of"
echo "JSON_FILENAME and/or MBOX_FILENAME."
echo "If EMAIL or NAME is not specified, they will be taken from 'git config' user settings."
exit 1
}
dbg() {
if [[ ${DEBUG} -ge 1 ]] ; then
echo "$1"
fi
}
# Parse the command line
short_opts="j:m:c:s:u:n:hv"
long_opts="json:,mbox:,cve:,sha:,vulnerable:,user:,name:,diff:,reference:,help,verbose"
JSON_FILE=""
MBOX_FILE=""
DIFF_FILE=""
REFERENCE_FILE=""
CVE_NUMBER=""
GIT_SHA=""
GIT_VULNERABLE=""
EMAIL="$(git config --get user.email)"
NAME="$(git config --get user.name)"
TMP=$(getopt -o "${short_opts}" --long "${long_opts}" --name="${SCRIPT}" -- "$@")
eval set -- "${TMP}"
while :; do
dbg "arg=${1}"
case "${1}" in
-j | --json ) JSON_FILE="${2}"; shift 2 ;;
-m | --mbox ) MBOX_FILE="${2}"; shift 2 ;;
--diff ) DIFF_FILE="${2}"; shift 2 ;;
--reference ) REFERENCE_FILE="${2}"; shift 2 ;;
-c | --cve ) CVE_NUMBER="${2}"; shift 2 ;;
-s | --sha ) GIT_SHA="${2}"; shift 2 ;;
-u | --user ) EMAIL="${2}"; shift 2 ;;
-n | --name ) NAME="${2}"; shift 2 ;;
--vulnerable ) GIT_VULNERABLE="${2}"; shift 2 ;;
-h | --help ) help; shift ;;
-v | --verbose ) DEBUG=1; shift ;;
-- ) shift; break ;;
* ) help ;;
esac
done
dbg "CVE_NUMBER=${CVE_NUMBER}"
dbg "GIT_SHA=${GIT_SHA}"
dbg "JSON_FILE=${JSON_FILE}"
dbg "MBOX_FILE=${MBOX_FILE}"
dbg "DIFF_FILE=${DIFF_FILE}"
dbg "REFERENCE_FILE=${REFERENCE_FILE}"
dbg "GIT_VULNERABLE=${GIT_VULNERABLE}"
if [[ "${CVE_NUMBER}" == "" || "${GIT_SHA}" == "" || "${EMAIL}" == "" ]] ; then
help
fi
if [[ "${JSON_FILE}" == "" && "${MBOX_FILE}" == "" ]] ; then
help
fi
if [[ "${CVE_USER}" == "" ]] ; then
echo "CVE_USER not set"
help
fi
# Functions for us to use, main flow starts below at ======= point
# determine if a kernel version is a "-rc" release or not.
# Will return 1 if -rc, 0 if not
function version_is_rc
{
#dbg "version_is_rc($1)"
local VERSION=$1
if [[ "${VERSION}" =~ .*"rc" ]] ; then
#dbg "version_is_rc: ${VERSION} is -rc"
return 1
else
#dbg "version_is_rc: ${VERSION} is NOT -rc"
return 0
fi
}
function version_is_queue
{
local VERSION=$1
if [[ "${VERSION}" =~ .*"queue" ]] ; then
return 1
else
return 0
fi
}
# Determine if a kernel version is a "mainline" one, or if it is a stable
# kernel release. Will return 1 if mainline, 0 if not
function version_is_mainline
{
#dbg "version_is_mainline($1)"
local VERSION=$1
local REL_ARRAY=(${VERSION//./ })
local MAJOR=${REL_ARRAY[0]}
#local BASE=${REL_ARRAY[0]}.${REL_ARRAY[1]}.${REL_ARRAY[2]}
#local REL=${REL_ARRAY[3]}
#local MINOR=${REL_ARRAY[2]}
# If this is a 2.6.X release, just return now, we don't care about them
# anymore
if [[ "${MAJOR}" == "2" ]] ; then
return 1
fi
version_is_rc "${VERSION}"
local rc=$?
#echo "rc=$rc"
# If this is a -rc release, it's a mainline release
#if [[ "${VERSION}" =~ *"-rc"* ]] ; then
if [[ "${rc}" == "1" ]] ; then
return 1
fi
# If this is in a queue, it's not a mainline release
version_is_queue "${VERSION}"
local queue=$?
if [[ "${queue}" == "1" ]] ; then
return 0
fi
# if the REL_ARRAY only has 2 elements in it, it's a mainline release
# (X.Y, not X.Y.Z)
if [[ "${#REL_ARRAY[@]}" == "2" ]] ; then
return 1
fi
# Must be a stable release
return 0
}
# =======
# Main logic starts here
# Get the UUID we are going to use from the linux.uuid file in the directory
# where the script is. This allows us to change this if needed in the future
# (and it's easier to move between testing and production databases this way,
# as those require different uuids.)
orig_id=$(cat "${DIR}"/linux.uuid)
if [[ "${orig_id}" == "" ]]; then
echo "No UUID found to use at ${DIR}/linux.uuid, aborting"
exit 1
fi
dbg "orig_id=${orig_id}"
# go into the kernel tree, we need this to be a valid one
#cd ${KERNEL_TREE} || exit 1
# See if the SHA given to us is a valid SHA in the git repo.
# This tests if we have a valid kernel tree, AND we need a full/long SHA1 for
# many of the searches we do later on. If we stuck with a short one, some of
# the searches would give us false-positives as people use short shas in commit
# messages.
GIT_SHA_FULL=$(cd "${KERNEL_TREE}" && git log -1 --format="%H" "${GIT_SHA}")
if [[ "${GIT_SHA_FULL}" == "" ]] ; then
echo "error: git id ${GIT_SHA} is not found in the tree at ${KERNEL_TREE}"
exit 1
fi
# Grab a "real" 12 character short sha to use as well, we "know" this will not fail.
GIT_SHA_SHORT=$(cd "${KERNEL_TREE}" && git log -1 --abbrev=12 --format="%h" "${GIT_SHA_FULL}")
# Get the subject line of our sha
subject=$(cd "${KERNEL_TREE}" && git show --no-patch --pretty=format:"%s" "${GIT_SHA_FULL}" 2> /dev/null)
if [[ "${subject}" == "" ]] ; then
echo "error: git id ${GIT_SHA_FULL} is not found in the tree at ${KERNEL_TREE}"
exit 1
fi
dbg "subject=${subject}"
# Get the list of files affected in the change
files=$(cd "${KERNEL_TREE}" && git diff --name-only "${GIT_SHA_FULL}"^.."${GIT_SHA_FULL}" 2> /dev/null)
dbg "${GIT_SHA_FULL} touched the following files:"
while IFS= read -r entry; do
dbg " ${entry}"
done <<< "${files}"
# Grab the full commit text, we will use that for many things
# We strip off the signed-off-by stuff AFTER we are done with parsing
# this text
commit_text=$(cd "${KERNEL_TREE}" && git show --no-patch --pretty=format:"%B" "${GIT_SHA_FULL}")
#echo "commit_text=${commit_text}"
#
# Use dyad to find our pairs of vulnerable:fixed entries
v=""
if [[ "${GIT_VULNERABLE}" != "" ]]; then
v="--vulnerable=${GIT_VULNERABLE}"
fi
# We want to call dyad without quotes for the arguments as we "know" these
# arguments are ok, we just set them above explicitly.
# shellcheck disable=SC2086
dyad_out=$("${dyad}" ${v} ${GIT_SHA_FULL} | grep -v "^#")
dbg "dyad_out=${dyad_out}"
dyad_entries=()
for dyad_entry in ${dyad_out}; do
dyad_entries+=("${dyad_entry}")
done
dbg "dyad_entries: ${#dyad_entries[@]}"
#
# Generate the some readable (i.e. text) information, showing where
# vulnerabilities showed up, and where they were fixed, and also if they are
# not fixed at all. Do this by creating a list of messages that we will later
# dump into the mail message itself.
vuln_array_mbox=()
url_array=()
for entry in "${dyad_entries[@]}"; do
x=(${entry//:/ })
vuln=${x[0]}
vuln_git=${x[1]}
fix=${x[2]}
fix_git=${x[3]}
dbg " mbox: vuln=${vuln} vuln_git=${vuln_git} fix=${fix} fix_git=${fix_git}"
if [[ "${fix}" == "0" ]]; then
# Issue is not fixed, so say that:
vuln_array_mbox+=("Issue introduced in ${vuln} with commit ${vuln_git}")
continue
fi
# if the vulnerability showed up in the same releasae it was
# fixed in, then skip it for the mail message
if [[ "${vuln}" != "${fix}" ]]; then
if [[ "${vuln}" == "0" ]] ; then
# We do not know when it showed up, so just say it is fixed
vuln_array_mbox+=("Fixed in ${fix} with commit ${fix_git}")
else
# Report when it was introduced and when it was fixed.
vuln_array_mbox+=("Issue introduced in ${vuln} with commit ${vuln_git} and fixed in ${fix} with commit ${fix_git}")
fi
fi
url_array+=("https://git.kernel.org/stable/c/${fix_git}")
done
# For now, if we do not have ANYTHING to report in the mbox message (i.e. all
# fixes were done in the same kernel branch as the issue was vulnerable in)
# then just bail out because we can't create the json file very easily at all.
if [[ "${#vuln_array_mbox[@]}" == "0" ]]; then
echo "Despite having some vulnerable:fixed kernels, none were in an actual release, so aborting and not assigning a CVE to ${GIT_SHA_SHORT}"
exit 1
fi
#
# Iterate over the whole list of kernel pairs to try to determine what the "default status" is.
# If there is any "mainline kernel" that is touched by this issue, then the
# default status is "affected", otherwise it is "unaffected".
default_status="unaffected"
for entry in "${dyad_entries[@]}"; do
x=(${entry//:/ })
vuln=${x[0]}
fix=${x[2]}
# if vuln == 0 then the kernel has always been vulnerable
if [[ "${vuln}" == "0" ]]; then
default_status="affected"
continue
fi
# if the fix is in the same release, skip this entry as it "doesn't count"
if [[ "${vuln}" == "${fix}" ]]; then
continue
fi
# if the vuln kernel is mainline, we were vulnerable
version_is_mainline "${vuln}"
vuln_mainline=$?
if [[ "${vuln_mainline}" == "1" ]] ; then
default_status="affected"
fi
done
dbg "default_status=${default_status}"
vuln_array_json=""
url_string_json=""
git_array_json=""
#
# If this is an "affected" kernel, then we need to find the first mainline
# kernel where things went wrong, so create an "affected" and "unaffected" json
# entry for just this type of thing
if [[ "${default_status}" == "affected" ]]; then
for entry in "${dyad_entries[@]}"; do
x=(${entry//:/ })
vuln=${x[0]}
vuln_git=${x[1]}
fix=${x[2]}
fix_git=${x[3]}
dbg " json: vuln=${vuln} vuln_git=${vuln_git} fix=${fix} fix_git=${fix_git}"
if [[ "${vuln}" == "0" ]]; then
# We do not know when this first was a problem, so we
# default to 0 and handle it elsewhere in the logic as
# everything is "affected"
dbg "vuln=${vuln}"
else
version_is_mainline "${vuln}"
is_mainline=$?
if [[ "${is_mainline}" == "1" ]]; then
dbg " adding ${vuln} as where everything was affected"
vuln_array_json+="versions[]=$(jo -- \
-s version="${vuln}" \
-s status="affected" \
) "
vuln_array_json+="versions[]=$(jo -- \
-s version="0" \
-s lessThan="${vuln}" \
-s status="unaffected" \
-s versionType="semver" \
) "
break
fi
fi
done
fi
#
# Create the normal json entries, based on what is vulnerable and what is fixed
for entry in "${dyad_entries[@]}"; do
x=(${entry//:/ })
vuln=${x[0]}
vuln_git=${x[1]}
fix=${x[2]}
fix_git=${x[3]}
if [[ "${fix}" == "0" ]]; then
# FIXME: We are not generating the json pairs for this properly just yet.
# Our attempts at this seem to break something, so save this for later..
continue
fi
# If the vulnerable kernel is 0 then the git id is the first in
# history, which we just manually substitute in here.
if [[ "${vuln}" == "0" ]]; then
vuln_git="1da177e4c3f41524e886b7f1b8a0c1fc7321cac2" # ("Linux-2.6.12-rc2")
fi
# create the json array for the git ids
git_array_json+="versions[]=$(jo -- \
-s version="${vuln_git}" \
-s lessThan="${fix_git}" \
-s status="affected" \
-s versionType="git" \
) "
# Add the git sha of the fix to the "all fix commits" array
url_string_json+="references[]=$(jo -- -s url="https://git.kernel.org/stable/c/${fix_git}") "
# If the commit was found and fixed in the same release, let's not
# create a version number range as that will just confuse everyone
if [[ "${vuln}" == "${fix}" ]]; then
continue
fi
# create the json array for the version numbers
if [[ "${default_status}" == "unaffected" ]]; then
# this is easy, our pairs are the versions that are
# affected, no tricky matching needs to happen here
vuln_array_json+="versions[]=$(jo -- \
-s version="${vuln}" \
-s lessThan="${fix}" \
-s status="affected" \
-s versionType="semver" \
) "
else
# much more tricky, we now need to say what ranges are
# both affected, AND unaffected. We handled the
# "affected" range above, so now our pairs show where
# things are "unaffected".
#
# By default, everything is affected from the "root" to
# the commit in mainline, so we have described that
# already above the loop, so this is just going to be
# the affected list...
#
# Note, the "mainline" fix shows where things "stop",
# so that gets a "short" record.
version_is_mainline "${fix}"
is_mainline=$?
if [[ "${is_mainline}" == "1" ]]; then
vuln_array_json+="versions[]=$(jo -- \
-s version="${fix}" \
-s lessThanOrEqual="*" \
-s status="unaffected" \
-s versionType="original_commit_for_fix" \
) "
else
# This is a stable range, so make an unaffected
# range with a wildcard
REL_ARRAY=(${fix//./ })
MAJOR=${REL_ARRAY[0]}
MINOR=${REL_ARRAY[1]}
vuln_array_json+="versions[]=$(jo -- \
-s version="${fix}" \
-s lessThanOrEqual="${MAJOR}.${MINOR}.*" \
-s status="unaffected" \
-s versionType="semver" \
) "
fi
fi
done
# If there are url references to be added to the record, read them from the
# referenced file and add them to the url and json strings to spit back out
# later on.
if [[ "${REFERENCE_FILE}" != "" ]]; then
dbg "reading references from ${REFERENCE_FILE}"
if [[ -f "${REFERENCE_FILE}" ]]; then
while read -r ref; do
url_array+=("${ref}")
url_string_json+="references[]=$(jo -- -s url="${ref}") "
done < "${REFERENCE_FILE}"
else
echo "error: reference file ${REFERENCE_FILE} is not found"
exit 1
fi
fi
dbg "git_array_json=${git_array_json}"
dbg "url_string_json=${url_string_json}"
dbg "vuln_array_json=${vuln_array_json}"
dbg "vuln_array_mbox="
for entry in "${vuln_array_mbox[@]}"; do
dbg " ${entry}"
done
# Strip off all of the signed-off-by stuff out of the commit text.
# We have a long list of "tags" to drop in the file, "tags", so compose
# the sed regex from the file and run the changelog through sed to strip
# things off.
# tags consist of one-line-per-tag, and we search the beginning of the
# line and a ':' character. This saves us from doing a whole bunch of:
# commit_text=$(echo "${commit_text}" | sed -e '/^cc:/Id;/^signed-off-by:/Id')
# calls.
sed_script=""
readarray -t tags < <(cat "${DIR}"/tags)
#for tag in $(cat "${DIR}"/tags); do
for tag in "${tags[@]}"; do
sed_script+="/^${tag}:/Id;"
done
#dbg "sed_script=${sed_script}"
sed_file=$(mktemp -t bippy.XXXX || exit 1)
echo "${sed_script}" > "${sed_file}"
commit_text=$(echo "${commit_text}" | sed -f "${sed_file}")
rm "${sed_file}"
# Add a prefix of what this is for, as per the CVE requirements as documented:
# https://www.cve.org/ResourcesSupport/AllResources/CNARules#section_8-2_cve_record_prose_description_requirements
commit_text=$(printf "In the Linux kernel, the following vulnerability has been resolved:\n\n%s" "${commit_text}")
dbg "commit_text length is ${#commit_text}"
# Sometimes people want/need to change the changelog text, so if there is a
# diff for it, apply it now.
if [[ "${DIFF_FILE}" != "" ]]; then
dbg "applying diff file ${DIFF_FILE}"
patch_file=$(mktemp -t bippy.XXXX || exit 1)
echo "${commit_text}" > ${patch_file}
patch -p1 ${patch_file} ${DIFF_FILE}
commit_text=$(cat ${patch_file})
rm ${patch_file}
fi
# The json record description can only be 4096 bytes big (because bytes are
# expensive) So trim it at 4079, and add "---truncated---" text which brings it
# out to 4095, with 1 byte to spare incase people are off-by-one in their
# parsing logic
#json_commit_text=$(printf "%.4079s" "${commit_text}")
json_commit_text=$(printf "%.3982s" "${commit_text}") # really 3982 for now due to CVE backend issues
if [[ "${#commit_text}" != "${#json_commit_text}" ]]; then
# we truncated the text, so say so
json_commit_text=$(printf "%s\n---truncated---\n" "${json_commit_text}")
fi
dbg "json_commit_text length is ${#json_commit_text}"
#########################
# Compose the json knowing what we now know, using the 'jo' tool
#########################
if [[ "${JSON_FILE}" != "" ]] ; then
# NOTE, be VERY careful about the quoting around the bash
# variables when using 'jo', it isn't obvious, for some places
# we need the variables to be expanded without the "", and
# the shellcheck tool will complain, and test the heck out of
# any changes you make here, it seems to work as-is, so watch
# out, here lies many dragons. Comments have been added where
# needed and able to be used.
x_generator=$(jo -- engine="${SCRIPT}-${SCRIPT_VERSION}")
cveMetadata=$(jo -- assignerOrgId="${orig_id}" \
cveID="${CVE_NUMBER}" \
requesterUserId="${CVE_USER}" \
-s serial="1" \
state="PUBLISHED")
d=$(jo -- \
lang="en" \
-s value="${json_commit_text}" \
)
descriptions=$(jo -a -- "${d}")
providerMetadata=$(jo -- \
orgId="${orig_id}" \
)
f=""
while IFS= read -r entry; do
f+="${entry} "
done <<< "${files}"
# We want f to be expanded without quotes
# shellcheck disable=SC2086
program_files=$(jo -a -- ${f})
# We want vuln_array_json to be expanded without quotes
# shellcheck disable=SC2086
a=$(jo -- \
product="Linux" \
vendor="Linux" \
defaultStatus="${default_status}" \
repo="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git" \
programFiles="${program_files}" \
${vuln_array_json} \
)
# We want git_array_json to be expanded without quotes
# shellcheck disable=SC2086
ag=$(jo -- \
product="Linux" \
vendor="Linux" \
defaultStatus="unaffected" \
repo="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git" \
programFiles="${program_files}" \
${git_array_json} \
)
affected=$(jo -a -- "${ag}" "${a}")
# We want url_string_json to be expanded without quotes
# shellcheck disable=SC2086
cna=$(jo -- \
providerMetadata="${providerMetadata}" \
descriptions="${descriptions}" \
affected="${affected}" \
${url_string_json} \
title="${subject}" \
x_generator="${x_generator}" \
)
# We might just need the "cna" output, and not the "containers" output below.
# Test with the 'cve' tool a bit, I think this might be able to be dropped as
# the tool might provide it for us. If not, then just output the above cna
# record instead.
containers=$(jo -- cna="${cna}")
# output the final combination
jo -p -- \
containers="${containers}" \
cveMetadata="${cveMetadata}" \
-s dataType="CVE_RECORD" \
-s dataVersion="5.0" > "${JSON_FILE}"
dbg "json file written to ${JSON_FILE}"
fi # end json creation
#########################
# Compose the mbox file
#########################
if [[ "${MBOX_FILE}" != "" ]] ; then
cat << EOF > "${MBOX_FILE}"
From ${SCRIPT}-${SCRIPT_VERSION} Mon Sep 17 00:00:00 2001
From: ${NAME} <${EMAIL}>
To: <linux-cve-announce@vger.kernel.org>
Reply-to: <cve@kernel.org>, <linux-kernel@vger.kernel.org>
Subject: ${CVE_NUMBER}: ${subject}
Description
===========
${commit_text}
The Linux kernel CVE team has assigned ${CVE_NUMBER} to this issue.
Affected and fixed versions
===========================
EOF
for line in "${vuln_array_mbox[@]}"; do
echo " ${line}" >> "${MBOX_FILE}"
done
cat << EOF >> "${MBOX_FILE}"
Please see https://www.kernel.org for a full list of currently supported
kernel versions by the kernel community.
Unaffected versions might change over time as fixes are backported to
older supported kernel versions. The official CVE entry at
https://cve.org/CVERecord/?id=${CVE_NUMBER}
will be updated if fixes are backported, please check that for the most
up to date information about this issue.
Affected files
==============
The file(s) affected by this issue are:
EOF
while IFS= read -r entry; do
echo " ${entry}" >> "${MBOX_FILE}"
done <<< "${files}"
cat << EOF >> "${MBOX_FILE}"
Mitigation
==========
The Linux kernel CVE team recommends that you update to the latest
stable kernel version for this, and many other bugfixes. Individual
changes are never tested alone, but rather are part of a larger kernel
release. Cherry-picking individual commits is not recommended or
supported by the Linux kernel community at all. If however, updating to
the latest release is impossible, the individual changes to resolve this
issue can be found at these commits:
EOF
for url in "${url_array[@]}"; do
echo " ${url}" >> "${MBOX_FILE}"
done
dbg "mbox file written to ${MBOX_FILE}"
fi # end mbox creation
# all done!
exit 0