| #!/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 |
| # id_found_in - tool to find what kernel a specific SHA is in |
| |
| # set to 1 to get some debugging logging messages (or use -v/--verbose option) |
| DEBUG=0 |
| |
| # TODO - make these options that are not hard-coded |
| KERNEL_TREE="/home/gregkh/linux/stable/linux-stable" |
| FOUND_IN="/home/gregkh/linux/stable/commit_tree/id_found_in" |
| |
| # Might be dropped if we don't need the full "container" output, see at the |
| # bottom for more details |
| USER="gregkh@linuxfoundation.org" |
| |
| # 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="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" |
| SCRIPT=${0##*/} |
| SCRIPT_VERSION=$(cd "${DIR}" && git ls-tree --abbrev=12 HEAD | grep "${SCRIPT}" | awk '{print $3}') |
| |
| # global variables |
| vuln_kernels=() |
| fixed_pairs=() |
| |
| 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 " -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 " -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." |
| exit 1 |
| } |
| |
| dbg() { |
| if [[ ${DEBUG} -ge 1 ]] ; then |
| echo "$1" |
| fi |
| } |
| |
| |
| # Parse the command line |
| short_opts="j:m:c:s:hv" |
| long_opts="json:,mbox:,cve:,sha:,help,verbose" |
| JSON_FILE="" |
| MBOX_FILE="" |
| CVE_NUMBER="" |
| GIT_SHA="" |
| |
| 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 ;; |
| -c | --cve ) CVE_NUMBER="${2}"; shift 2 ;; |
| -s | --sha ) GIT_SHA="${2}"; shift 2 ;; |
| -h | --help ) help ;; |
| -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}" |
| |
| if [[ "${CVE_NUMBER}" == "" || "${GIT_SHA}" == "" ]] ; then |
| help |
| fi |
| if [[ "${JSON_FILE}" == "" && "${MBOX_FILE}" == "" ]] ; then |
| help |
| fi |
| |
| |
| # Functions for us to use, main flow starts below at ======= point |
| |
| |
| # Given a short SHA value in $1 (we hope), turn it into an "expanded" sha and |
| # then look up where that commit came from. |
| # Might be multiple kernels, so parse accordingly |
| find_fix() { |
| FIX=$1 |
| |
| id="" |
| if [[ ${FIX} =~ [[:xdigit:]]{12} ]] ; then |
| id=${BASH_REMATCH[0]} |
| else |
| # Let's try it again in a cruder way |
| id=$(echo "${FIX}" | sed -e 's/^[ \t]*//' | cut -f 2 -d ':' | sed -e 's/^[ \t]*//' | cut -f 1 -d ' ') |
| fi |
| |
| if [ "${id}" == "" ] ; then |
| # can't find a valid sha or something resembing it, so just return |
| return |
| fi |
| |
| long_id=$(cd ${KERNEL_TREE} && git log -1 --format="%H" "${id}") |
| if [ "${long_id}" == "" ] ; then |
| # git id is not a valid one, so just return |
| return |
| fi |
| |
| release=$("${FOUND_IN}" "${long_id}") |
| echo "${release} " |
| return |
| } |
| |
| # 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 the "major.minor" versions of 2 kernels are matching |
| # will return 1 if true, 0 if not |
| function version_match |
| { |
| local VERSION_1=$1 |
| local VERSION_2=$2 |
| |
| local REL_ARRAY_1=(${VERSION_1//./ }) |
| local REL_ARRAY_2=(${VERSION_2//./ }) |
| local MAJOR_1=${REL_ARRAY_1[0]} |
| local MAJOR_2=${REL_ARRAY_2[0]} |
| |
| if [[ "${MAJOR_1}" == "${MAJOR_2}" ]] ; then |
| local MINOR_1=${REL_ARRAY_1[1]} |
| local MINOR_2=${REL_ARRAY_2[1]} |
| if [[ "${MINOR_1}" == "${MINOR_2}" ]] ; then |
| # dbg "version_match: ${1} ${2}: succeeded" |
| return 1 |
| fi |
| fi |
| |
| # dbg "version_match: ${1} ${2}: failed" |
| return 0 |
| } |
| |
| |
| # 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}" |
| |
| # Fixes matching, this is the "meat" of the logic for what people are really |
| # looking for as no one ever seemed to do it in the past, and is one of the |
| # primary reasons we are tracking these ourselves now. |
| # |
| # The logic here is a bit complex, as we have a number of different situations |
| # that a commit could have. We track all of this in different variables: |
| # og_vuln: |
| # "Original vulnerable kernel version" |
| # Version where in mainline the problem first showed up. If this can not |
| # be determined, "0" is used here. |
| # vuln_kernels: |
| # "Vulnerable kernels" |
| # An array of kernel versions that describes where the problem first |
| # occured. This list is not always present and can not always be |
| # determined, so it might be empty. If the vulnerable kernel(s) can be |
| # determined, this variable will be an array of a it will be a mix of |
| # stable releases and hopefully a release in Linus's tree (but NOT a -rc |
| # release, as we don't track them this way because we do not consider |
| # them to be valid versions to care about.) Note, the list is sorted in |
| # "sort -V" order, and Linus's releases CAN be in the middle of the list, |
| # so beware of this when trying to match versions up (see below for |
| # matching logic.) |
| # fixed_kernels: |
| # "Fixed kernels" |
| # An array of kernel versions where the fix showed up in. As we always |
| # have at least one valid commit id (by virtue of this being required to |
| # run this script), this array will always have at least one version |
| # number in it. |
| # Note, this list will contain numbers that either describe a stable |
| # release (X.Y.Z from linux-stable) or a released version (X.Y from |
| # Linus's tree, we do not track -rc releases because they are development |
| # kernels.) |
| # |
| # Here's the different states, with example commit |
| # ids so that they can be used for testing: |
| # |
| # - Commit is only in a stable kernel tree, with no Fixes: tag |
| # For this the og_vuln will be set to "0" and fixed_kernels will have just |
| # one stable kernel release version in the array. |
| # Example: 10d75984495f ("Revert "NFSD: Fix possible sleep during nfsd4_release_lockowner()"") |
| # |
| # - Commit is only in a stable kernel tree, with a Fixes: tag |
| # For this the og_vuln will be set to the released version and fixed_kernels |
| # will have just a one stable kernel release in the array. |
| # Example: 2a8664583d4d ("fs: sysfs_emit_at: Remove PAGE_SIZE alignment check") |
| # |
| # - Commit is only in a stable kernel tree, with multiple Fixes: tags |
| # For this the og_vuln will be set to the released version and fixed_kernels |
| # will have multiple stable kernel releases in the array. |
| # Example: rare, I can't find one... |
| # |
| # - Commit is in Linus's tree, no stable trees, with no Fixes: tag |
| # For this the og_vuln will be set to 0 and fixed_kernels will have just the |
| # single released version in the array. |
| # This is rare, the example here is not really a bugfix, but you can use it |
| # for testing: |
| # Example: 94959c0e796e ("i2c: make i2c_bus_type const") |
| # |
| # - Commit is in Linus and stable trees, with no Fixes: tag |
| # For this the og_vuln will be set to 0 and fixed_kernels will be an array of |
| # versions where the commit is present. |
| # Example: c481016bb4f8 ("ASoC: qcom: sc8280xp: limit speaker volumes") |
| # |
| # - Commit is in Linus and stable trees, with Fixes: tag |
| # For this the og_vuln will be set to the "oldest" released kernel (by using |
| # 'sort -V' which should catch the correct version), and fixed_kernels will |
| # be an array of versions where the commit is present. |
| # Example: d9407ff11809 ("pds_core: Prevent health thread from running during reset/remove") |
| # |
| # |
| # Matching versions |
| # Ideally we want to match up when a vulnerability showed up, and when it was |
| # fixed, for each supported kernel branch we have at the time (list of |
| # supported kernel branches can be found on www.kernel.org) This logic isn't |
| # the most obvious, so here's our current solution in pseudo-code: |
| # For every fixed_entry in fixed_kernels do: |
| # if fixed_entry is a -rc kernel |
| # continue |
| # if og_vuln == 0 |
| # create fix_pair(og_vuln, fixed_entry) |
| # continue |
| # for every vuln_entry in vuln_kernels do: |
| # if fixed_entry == vuln_entry |
| # continue, this is something added in this tree, so no need for an entry here. |
| # if fixed_entry == X.Y-type release (i.e. Linus release) && vuln_entry == X.Y-type release |
| # create fix_pair(vuln_entry, fixed_entry) |
| # continue |
| # if fixed_entry X.Y == vuln_entry X.Y |
| # create fix_pair(vuln_entry, fixed_entry) |
| # continue |
| # create fix_pair(og_vuln, fixed_entry) |
| # |
| # This matching allows us to catch places where a fix was done in Linus's tree, |
| # for a specific commit in Linus's tree, as well as catch when fixes are made |
| # in stable branches for issues created in that same stable branch. |
| # Here are some good commits to use for testing this all out: |
| # d6938c1c76c6 ("ipvs: avoid stat macros calls from preemptible context") |
| # c95f919567d6 ("nfc: llcp_core: Hold a ref to llcp_local->dev when holding a ref to llcp_local") |
| # 38d75297745f ("watchdog: set cdev owner before adding") |
| # |
| |
| |
| # Look in the commit text to see if there is any "Fixes:" lines |
| # if so, look them up to see what kernels they were released in. Need to do |
| # this with the "expanded" SHA value, the short one will give us too many |
| # false-positives when it shows up in other Fixes: tags |
| fixes_lines=$(echo "${commit_text}" | grep -i "fixes:" | sed -e 's/^[ \t]*//' | cut -f 2 -d ':' | sed -e 's/^[ \t]*//' | cut -f 1 -d ' ') |
| dbg "fixes_lines=${fixes_lines}" |
| og_vuln="0" |
| og_git="1da177e4c3f4" # "Linux-2.6.12-rc2" |
| if [[ "${fixes_lines}" != "" ]] ; then |
| # figure out what kernels this commit fixes, (i.e. which are |
| # vulnerable) and turn them into an array |
| v=() |
| for fix_line in ${fixes_lines}; do |
| x=$(find_fix ${fix_line}) |
| v+=${x} |
| og_git=${fix_line} # fixme, what about multiples? |
| done |
| #dbg "v=${v[@]} size=${#v[@]}" |
| # now sort and uniq the list of versions |
| vuln_kernels=($(echo "${v[@]}" | sed 's/ /\n/g' | sort -V | uniq)) |
| |
| if [[ "${#vuln_kernels[@]}" == "0" ]] ; then |
| dbg "no vuln_kernels_found!" |
| else |
| dbg "${#vuln_kernels[@]} vuln_kernels found:" |
| for x in ${vuln_kernels[@]}; do |
| dbg " $x" |
| done |
| |
| # Figure out the "original" oldest commit where this showed up, |
| # we need this as a default value in case we can't match up |
| # anything else to it, this must be a Linus release |
| for x in ${vuln_kernels[@]}; do |
| version_is_mainline "${x}" |
| mainline=$? |
| if [[ "${mainline}" == "1" ]] ; then |
| og_vuln="${x}" |
| break |
| fi |
| done |
| |
| # Ick, no fixes version was found to be a mainline release, so |
| # we must have a stable-tree-only regression, so pick the |
| # "first" version as it must be the one. |
| if [[ "${og_vuln}" == "0" ]] ; then |
| og_vuln=${vuln_kernels[0]} |
| # og_git will be already set to the fixes |
| # commit, based on the loop up above, so no need |
| # to set it again. |
| fi |
| fi |
| dbg "og_vuln=${og_vuln}" |
| dbg "og_git=${og_git}" |
| fi |
| |
| # Find the fixed kernels where this release was done |
| fixed_kernels=$("${FOUND_IN}" "${GIT_SHA_FULL}") |
| dbg "fixed_kernels=${fixed_kernels}" |
| |
| |
| create_fix_pair() |
| { |
| local v=$1 |
| local f=$2 |
| local v_git=$3 |
| local f_git=$4 |
| fixed_pairs+=("${v}:${f}:${v_git}:${f_git}") |
| |
| dbg "pair=${v}:${f}:${v_git}:${f_git}" |
| } |
| |
| find_stable_git_id() |
| { |
| #>&2 echo "find_stable_git_id: \"${1}\" \"${2}\"" |
| local og_git=${1} |
| local fixed_version=${2} |
| |
| local fixed_array=(${fixed_version//./ }) |
| local fixed_major=${fixed_array[0]} |
| local fixed_minor=${fixed_array[1]} |
| |
| local stable_git=$(cd ${KERNEL_TREE} && git log -1 --abbrev=12 --oneline --grep=${og_git} "v${fixed_major}.${fixed_minor}".."v${fixed_version}" | awk '{print $1}') |
| #>&2 echo "fine_stable_git_id: stable_git=${stable_git}" |
| |
| echo "${stable_git}" |
| } |
| |
| find_mainline_git_id() |
| { |
| local fixed_version=${1} |
| local stable_git=$(cd ${KERNEL_TREE} && git log -1 --abbrev=12 --oneline "v${fixed_version}" | awk '{print $1}') |
| echo "${stable_git}" |
| } |
| |
| git_full_id() |
| { |
| local short_id=${1} |
| local long_id=$(cd ${KERNEL_TREE} && git log -1 --format="%H" "${short_id}") |
| echo "${long_id}" |
| } |
| |
| |
| # Do the crazy matching mess listed up above |
| fe="" |
| ve="" |
| # the default state is unaffected, unless a mainline kernel is touched, |
| # and then we will switch it below to affected. |
| default_status="unaffected" |
| for fixed_entry in ${fixed_kernels[@]}; do |
| create=0 |
| |
| # figure out if this is a -rc kernel or not |
| version_is_mainline "${fixed_entry}" |
| fixed_entry_mainline=$? |
| version_is_rc "${fixed_entry}" |
| fixed_entry_rc=$? |
| version_is_queue "${fixed_entry}" |
| fixed_entry_queue=$? |
| |
| # Print out some debugging to see if kernel version detection is working: |
| dbg_string="${fixed_entry} is " |
| if [[ "${fixed_entry_rc}" == "1" ]]; then |
| dbg_string+="-rc and " |
| fi |
| if [[ "${fixed_entry_mainline}" == "1" ]]; then |
| dbg_string+="mainline" |
| else |
| dbg_string+="not mainline" |
| fi |
| dbg "${dbg_string}" |
| |
| # if a commit is in a newer "stable" release, but not in an older one |
| # and only in the queue there, we get "queue-X.Y as a fixed kernel, |
| # which we don't want to have here, so filter that out. |
| if [[ "${fixed_entry_queue}" == "1" ]] ; then |
| dbg "${fixed_entry} is in queue" |
| continue |
| fi |
| |
| # We do care about fixes in -rc kernels, as that is "mainline" and the |
| # logic of "affected/unaffected" depends on having a mainline commit, |
| # so that is why the following check is commented out on purpose. |
| # if [[ "${fixed_entry_rc}" == "1" ]] ; then |
| # dbg "${fixed_entry} is -rc" |
| # continue |
| # fi |
| |
| # If we do not know what the root release is, then just create the pair |
| if [[ "${og_vuln}" == "0" ]] ; then |
| if [[ "${fixed_entry_mainline}" == "1" ]]; then |
| create_fix_pair ${og_vuln} ${fixed_entry} ${og_git} ${GIT_SHA_SHORT} |
| default_status="affected" |
| else |
| fe=$(find_stable_git_id ${GIT_SHA_FULL} ${fixed_entry}) |
| create_fix_pair ${og_vuln} ${fixed_entry} ${og_git} ${fe} |
| fi |
| create=1 |
| continue |
| fi |
| |
| for vuln_entry in ${vuln_kernels[@]}; do |
| if [[ "${fixed_entry}" == "${vuln_entry}" ]] ; then |
| # this entry was broken and fixed in the same version, |
| # so skip it |
| create=1 # fake that an entry was created. |
| break |
| fi |
| |
| version_is_mainline "${vuln_entry}" |
| vuln_entry_mainline=$? |
| if [[ "${vuln_entry_mainline}" == "1" ]] ; then |
| if [[ "${fixed_entry_mainline}" == "1" ]]; then |
| create_fix_pair ${vuln_entry} ${fixed_entry} ${og_git} ${GIT_SHA_SHORT} |
| default_status="affected" |
| else |
| fe=$(find_stable_git_id ${GIT_SHA_FULL} ${fixed_entry}) |
| create_fix_pair ${vuln_entry} ${fixed_entry} ${og_git} ${fe} |
| fi |
| create=1 |
| break |
| fi |
| |
| version_match ${vuln_entry} ${fixed_entry} |
| match=$? |
| if [[ "${match}" == "1" ]] ; then |
| if [[ "${fixed_entry_mainline}" == "1" ]]; then |
| create_fix_pair ${vuln_entry} ${fixed_entry} ${og_git} ${GIT_SHA_SHORT} |
| default_status="affected" |
| else |
| ve=$(find_stable_git_id ${og_git} ${vuln_entry}) |
| if [[ "${ve}" == "" ]] ; then |
| ve=${og_git} |
| fi |
| fe=$(find_stable_git_id ${GIT_SHA_FULL} ${fixed_entry}) |
| if [[ "${fe}" == "" ]] ; then |
| fe=${GIT_SHA_SHORT} |
| fi |
| create_fix_pair ${vuln_entry} ${fixed_entry} "${ve}" "${fe}" |
| fi |
| create=1 |
| break |
| fi |
| done |
| |
| # If we haven't created anything yet, this must be it |
| if [[ ${create} == 0 ]] ; then |
| create_fix_pair ${og_vuln} ${fixed_entry} ${og_git} "${GIT_SHA_SHORT}" |
| default_status="affected" |
| fi |
| done |
| |
| dbg "We have found ${#fixed_pairs[@]} vulnerable:fixed kernel pairs" |
| if [[ "${#fixed_pairs[@]}" == "0" ]] ; then |
| echo "No vulnerable and then fixed pairs of kernels were found for commit ${GIT_SHA_SHORT}" |
| exit 1 |
| fi |
| for entry in "${fixed_pairs[@]}"; do |
| dbg " ${entry}" |
| done |
| dbg "default_status=${default_status}" |
| |
| # Generate the "vulnerable" kernel json and mbox information |
| vuln_array_json="" |
| vuln_array_mbox=() |
| url_array=() |
| url_string_json="" |
| git_array_json=() |
| if [[ "${default_status}" == "affected" ]]; then |
| # we need to now say what the "unaffected" range is, so loop |
| # through and find the "mainline" part to figure that out. |
| for entry in "${fixed_pairs[@]}"; do |
| x=(${entry//:/ }) |
| vuln=${x[0]} |
| fix=${x[1]} |
| vuln_git=${x[2]} |
| fix_git=${x[3]} |
| |
| if [[ "${vuln}" == "0" ]]; then |
| #FIXME this needs to handle this case |
| dbg "FIXME, vuln=${0}" |
| 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="custom" \ |
| ) " |
| fi |
| break |
| fi |
| done |
| fi |
| |
| for entry in "${fixed_pairs[@]}"; do |
| x=(${entry//:/ }) |
| vuln=${x[0]} |
| fix=${x[1]} |
| vuln_git=${x[2]} |
| fix_git=${x[3]} |
| |
| # 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="custom" \ |
| ) " |
| 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 |
| number_array=${fix} |
| 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="custom" \ |
| ) " |
| fi |
| # FIXME, I think we need more logic here, but I can't |
| # remember why... |
| |
| 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" \ |
| ) " |
| |
| # If this issue has always been there, just say when it was |
| # fixed, otherwise try to give a hint when it was introduced. |
| # The json file just wants 0 for "always been there", so no need |
| # to check it for the array. |
| if [[ "${vuln}" == "0" ]] ; then |
| vuln_array_mbox+=("Fixed in ${fix} with commit ${fix_git}") |
| else |
| vuln_array_mbox+=("Issue introduced in ${vuln} with commit ${vuln_git} and fixed in ${fix} with commit ${fix_git}") |
| fi |
| |
| # Add the git sha of the fix to the url array so we can print |
| # them later |
| long_id=$(git_full_id "${fix_git}") |
| url_array+=("https://git.kernel.org/stable/c/${long_id}") |
| url_string_json+="references[]=$(jo -- -s url="https://git.kernel.org/stable/c/${long_id}") " |
| |
| done |
| dbg "vuln_array_json=${vuln_array_json}" |
| dbg "git_array_json=${git_array_json}" |
| for entry in "${vuln_array_mbox[@]}"; do |
| dbg "vuln_array_mbox=${entry}" |
| done |
| for entry in "${url_string_json[@]}"; do |
| dbg "url_string_json=${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="" |
| for tag in $(cat "${DIR}"/tags); do |
| sed_script+="/^${tag}:/Id;" |
| done |
| #dbg "sed_script=${sed_script}" |
| sed_file=$(mktemp "${TMPDIR}"/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}" |
| |
| # 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="${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}) |
| |
| # FIXME: add program_files down below to a and ag in the proper place, |
| # but for now, the json is not validating on the CVE server side, so |
| # don't add it until that is figured out... |
| |
| # 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" \ |
| ${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" \ |
| ${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: Greg Kroah-Hartman <gregkh@linuxfoundation.org> |
| To: <linux-cve-announce@vger.kernel.org> |
| Reply-to: <cve@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 or 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 |