Blob Blame History Raw
#!/bin/bash
# vim: sw=4:sts=4:et

# TODO: Error handling is really not great. Especially failures from nested shells
# sucks.

. $(dirname "$0")/common-functions

usage()
{
    echo "Check state of a kernel fix and eventually suggest needed actions"
    echo
    echo "Expect upstream kernel tree sha or CVE number as the parameter."
    echo "The script checks whether the commit is already in the upstream"
    echo "baseline or backported in kernel-source tree."
    echo "Requires LINUX_GIT pointing to Linus git tree clone."
    echo
    echo "If backported, checks for CVE/bsc references and recommends adding these"
    echo "if they are missing. (Requires VULNS_GIT pointing to"
    echo "https://git.kernel.org/pub/scm/linux/security/vulns.git tree."
    echo "This will also allow cve number instead of sha and it resolves proer"
    echo "upstream commit automatically."
    echo
    echo "Also the script looks for \"Fixes:\" tag of the given \"sha\"."
    echo "When defined, the script informs where the fix has to be backported."
    echo
    echo "The script also takes into account the hierarchy of branches."
    echo "It checks all branches. But the action is proposed only for"
    echo "the top level ones. The assumption is that the other branches"
    echo "will get the fix via a merge."
    echo
    echo "If the patch has CVE number with CVSS score associated then limits"
    echo "actions only to CVSS affected branches."
    echo
    echo "Usage: ${0##*/} [options] sha|CVE"
    echo
    echo "Parameters:"
    echo "	sha: sha of the upstream commit"
    echo "	cve: CVE-XXXX-YYYY of the upstream commit (requires VULNS_GIT)"
    echo
    echo "Options:"
    echo "	-h: help"
    echo "	-q: quiet mode (no progress)"
    echo "	-v: verbose mode: show state of each branch and even NOP actions"
    echo "	-r: refresh any cached data. Use if cve->sha or cve->cvss fails"
    echo "	    (git pull VULNS_GIT, cve, bsc medata)"
    echo "	-t: flat mode. Do not filter output based on cvss scoring or common"
    echo "	    ancestors."
    echo "	-s CVSS: override the CVSS score if known. This can be useful when"
    echo "	   the CVE->CVSS DB is not synced yet."
    echo "	-b bsc#NUMBER: override the bugzilla number if known"
    echo "	-f sha: provide explicit Fixes tag. Use when the one in the commit is"
    echo "	   unknown but you could figure it from the code inspection."
    echo "	   Specify repeteadly if more are required"
    echo "	-c CVE-XXXX-YYYY: provide CVE number for the given sha. Use for"
    echo "	   CVEs issued before the process has changed and VULNS_GIT doesn't"
    echo "	   recognize this CVE"
}

SUSE_GET_MAINTAINERS=/usr/bin/suse-get-maintainers
branch=
bprefix=
sha=
references=

cve=
top_level=

tmpdir=$(mktemp -d /tmp/${0##*/}.XXXXXX)
trap 'rm -rf "$tmpdir"' EXIT

branch_state_file="$tmpdir/branch-state"
patch_file="$tmpdir/patches"
actions_file="$tmpdir/actions"

print_branch_state()
{
    local msg="$@"

    if [ -n "$verbose_mode" ] ; then
	echo "$msg"
    elif [ -z "$quiet_mode" ] ; then
	# show progress
	echo -n "."
    fi

    echo "$msg" >> "$branch_state_file"
}

# Check state of the given branch and store
# The states are stored $tmpdir/branch-state are are the following:
#
#   + nope: branch not affected
#   + ok: branch has the fix and all references
#   + missing_references: all or some references were not found
#   + missing_patch: patch has to be backported
#   + maybe_missing_patch: patch is missing and it is not known which commit
#	introduced the bug
#
# When found, the name of the patch is stored into "$patch_file".
check_branch_state()
{
    local branch="$1"
    local sha="$2"
    local expl_fixes="$3"
    shift 3
    local references="$@"

    [ -z "$branch" ] && fail "check_branch_state: No branch provided"
    [ -z "$sha" ] && fail "check_branch_state: No sha provided"
    # FIXME ugh
    [ "$expl_fixes" = "none" ] && expl_fixes=""

    local patch=
    local base=
    local ref=
    local missing_references=
    local msg_prefix="$branch:$sha"


    base=$(branch_base_ver $branch)

    # Already merged upstream?
    if sha_merged_in_upstream_tag "$sha" "$base" ; then
	print_branch_state "$msg_prefix:nope"
	return
    fi

    # Does the patch exist?
    patch=$(sha_to_patch_in_branch "$sha" "$branch")

    if [ -n "$patch" ] ; then
	echo "$branch:$patch" >> "$patch_file"

	# Check references
	for ref in $references ; do
	    if ! patch_has_reference_in_branch "$patch" "$ref" "$branch" ; then
		[ -n "$missing_references" ] && missing_references="$missing_references "
		missing_references="$missing_references$ref"
	    fi
	done

	if [ -z "$missing_references" ] ; then
	    print_branch_state "$msg_prefix:ok"
	else
	    print_branch_state "$msg_prefix:missing_references:$missing_references"
	fi

	return
    fi

    # Sha is not backported
    # Do we need to backport it because of the Fixes tag?
    local sha_git_fixes="$(sha_get_upstream_git_fixes $sha)"
    if [ -n "$sha_git_fixes" -o -n "$expl_fixes" ] ; then
	local affected_by_git_fixes="$(affected_by_git_fixes "$branch" "$base" $sha_git_fixes $expl_fixes)"

	if [ -n "$affected_by_git_fixes" ] ; then
	    print_branch_state "$msg_prefix:missing_patch:$affected_by_git_fixes"
	else
	    print_branch_state "$msg_prefix:nope"
	fi

	return
    fi

    # missing git fixes
    print_branch_state "$msg_prefix:maybe_missing_patch:$ref"
}

print_action()
{
    local branch="$1"
    local sha="$2"
    local state="$3"
    shift 3
    local references="$@"

    [ -z "$branch" ] && fail "print_action: No branch provided"
    [ -z "$sha" ] && fail "print action: No sha provided"
    [ -z "$state" ] && fail "print action: No state provided"

    # make sure tha the file exists
    touch "$patch_file"

    patch=
    action=
    case "$state" in
	missing_patch)
	    action="$branch: MANUAL: backport $sha ($references)"
	    ;;

	maybe_missing_patch)
	    action="$branch: MANUAL: might need backport of $sha ($references)"
	    ;;

	missing_references)
	    [ -z $verbose_mode ] && return
	    patch=$(grep "^$branch:" "$patch_file" | cut -d : -f 2)
	    if [ -n "$patch" ] ; then
		ref_args=$(printf -- '-r %s ' $references)
		action="$branch: RUN: scripts/cve_tools/add-missing-reference $ref_args$patch"
	    else
		action="$branch: MANUAL: no patch has the references: $references"
	    fi
	    ;;

	nope)
	    [ -n "$verbose_mode" ] && action="$branch: NOPE: no problema for $sha $references"
	    ;;

	ok)
	    [ -n "$verbose_mode" ] && action="$branch: NOPE: up-to-date $sha $references"
	    ;;

	*)
	    echo "print_action: Unknown action: $action" >&2
	    echo "for $branch:$sha:$state:$references" >&2
	    exit 1
	    ;;
    esac

    if [ -n "$action" ] ; then
	if [ ! -e "$actions_file" ] ; then
	    # first action
	    echo "ACTION NEEDED!"
	    touch "$actions_file"
	fi

	echo "$action"
    fi
}

cvss_affects_branch()
{
	local branch="$1"
	local cvss="$2"

	local ret=1
	if [[ "$branch" =~ .*-EB.* ]]
	then
		[ $cvss -ge 9 ] && ret=0
	elif [[ "$branch" =~ .*-GA.* ]]
	then
		[ $cvss -ge 7 ] && ret=0
	elif [[ "$branch" =~ .*-LTSS.* ]]
	then
		[ $cvss -ge 7 ] && ret=0
	else
		ret=0
	fi
	return $ret
}

find_and_print_toplevel_actions()
{
    local branch="$1"
    local flat_mode=$2
    local cvss="${3%%.*}"
    local action_parameters=
    local merge_branch=
    local mb_line=
    local line=
    local merge_found=
    local state=
    local mb_state=

    [ -z "$branch" ] && fail "find_and_print_toplevel_actions: No branch provided"
    [ ! -f $branch_state_file ] && fail "Bailing out"

    grep "^$branch:" "$branch_state_file" | \
	while read line ; do
	    if [ "$flat_mode" -eq 1 ]
	    then
		    print_action ${line//:/ }
		    continue
	    fi

	    state=$(echo $line | cut -d: -f3)
	    # We only want to print branches which really need CVE fix backported
	    # CVSS 9+ EB branches
	    # CVSS 7+ LTSS branches
	    # Any CVSS for regular branch
	    # If we just need to add a reference then print everything
	    if [ -n "$cvss" -a "$state" != "missing_references" ]
	    then
		    if ! cvss_affects_branch "$branch" "$cvss"
		    then
			    continue
		    fi
	    fi

	    # EB branches do not really need to add missing references
	    if [[ "$branch" =~ .*-EB.* && "$state" = "missing_references" ]]
	    then
			continue
	    fi

	    for merge_branch in $(print_merge_branches $branches_conf $branch) ; do

		# Make sure merge_branches are in the same cvss scope
		if [ -n "$cvss" -a "$state" != "missing_references" ]
		then
		    if ! cvss_affects_branch "$merge_branch" "$cvss"
		    then
			    continue
		    fi
		fi

		# branch name might include '/', e.g. cve/linux-4.12
		mb_line=$(grep ^$merge_branch: $branch_state_file)
		mb_state=$(echo $mb_line | cut -d: -f3)

		# if the merge branch is in the same state then do not
		# bother
		if [ "$mb_state" == "$state" ]
		then
			merge_found=1
			break
		fi

		if [ "$state" == "missing_references" -o		\
		     "$state" == "missing_patch" -o			\
		     "$state" == "maybe_missing_patch" ] ; then

		    # No action is needed when the patch is backported
		    # even if it has references missing. Those will be
		    # reported for the merge origin
		    if [ "$mb_state" == "ok" -o "$mb_state" == "missing_references" ] ; then
			merge_found=1
		    fi
		fi

	    done

	    if [ -z "$merge_found" ] ; then
		# split line into parameters
		print_action ${line//:/ }
	    fi
	done
}

verbose_mode=
quiet_mode=
flat_mode=0
expl_fixes=""

while getopts "hvrqts:b:f:c:" OPT
do
    case $OPT in
	h)
	    usage
	    exit
	    ;;
	v)
	    verbose_mode=1
	    ;;
	r)
	    refresh=1
	    ;;
	q)
	    quiet_mode=1
	    ;;
	t)
	    flat_mode=1
	    ;;
	s)
	    cvss=$OPTARG
	    ;;
	b)
	    bsc=$OPTARG
	    ;;
	f)
	    expl_fixes="$expl_fixes $OPTARG"
	    ;;
	c)
	    cve=$OPTARG

    esac
done

# Fixme: Ugh but we have to send this as a parameter to a function and it
# cannot be the last (implicit one)
[ -z "$expl_fixes" ] && expl_fixes="none"

shift "$(($OPTIND-1))"

[ -n "$verbose_mode" ] && quiet_mode=

if [ -z "$1" ] ; then
    echo "No references provided"
    usage
    exit 1
fi

sha=$1
if ! sha_in_upstream "$1" ; then
	[ -n "$cve" ] && fail "SHA expected when -c CVE-XXXXXX-YYYY is used"
	sha=$(cve2sha $1 $refresh)
	if [ -z "$sha" ]
	then
		[ -z "$VULNS_GIT" ] && fail "VULNS_GIT not defined. It has to point https://git.kernel.org/pub/scm/linux/security/vulns.git tree clone."
		fail "Can find't sha in upstream: $1."
	fi
fi

print_upstream_sha_summary $sha "$expl_fixes"

[ -z "$cve" ] && cve=$(sha2cve $sha $refresh)
if [ -n "$cve" ]
then
	[ -z "$bsc" ] && bsc=$(cve2bugzilla $cve $refresh)
	[ -z "$cvss" ] && cvss=$(cve2cvss $cve $refresh)
	echo "Security fix for $cve ${bsc:-bsc unknown} with CVSS ${cvss:-unknown}"
	[ -z "$cvss" -o -z "$bsc" ] && fail "Incomplete references re-check with -r or use -s/-b to specify missing references"
else
	# emulate no CVE fix as CVSS==0. This will typically happen
	# for upstream commit with Fixes: which we want to target to
	# less conservative branches only
	[ -z "$cvss" ] && ccvss=0
fi

if [ -x $SUSE_GET_MAINTAINERS ]
then
	echo -n "Experts candidates: "
	$SUSE_GET_MAINTAINERS --sha $sha | tr "\n" " "
	echo
fi

references="$cve $bsc"

branches_conf="$(fetch_branches $refresh)"

# Check state of each branch
for_each_build_branch "$branches_conf" check_branch_state $sha "$expl_fixes" $references

# Newline after the dots showing progress
[ -z "$quiet_mode" ] && echo

for_each_build_branch "$branches_conf" find_and_print_toplevel_actions $flat_mode $cvss