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

. $(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
    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)"
}

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"
    shift 2
    local references="$@"

    [ -z "$branch" ] && fail "check_branch_state: No branch provided"
    [ -z "$sha" ] && fail "check_branch_state: No sha provided"

    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" ] ; then
	local affected_by_git_fixes="$(affected_by_git_fixes "$branch" "$base" $sha_git_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)
	    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 cvss="${2%%.*}"
    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"

    grep "^$branch:" "$branch_state_file" | \
	while read line ; do
	    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

	    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=$(echo -n "$line" | sed -e "s|^$branch:|$merge_branch:|")

		# ignore the state when the same change is needed in a merge branch
		if grep -q "^$mb_line$" "$branch_state_file" ; then
		    merge_found=1
		fi

		mb_state=$(echo $mb_line | cut -d: -f3)

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

		    # No action is needed when the patch is backported
		    # and has all the references in the merge branch
		    if [ "$mb_state" == "ok" ] ; 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=

while getopts "hvrq" OPT
do
    case $OPT in
	h)
	    usage
	    exit
	    ;;
	v)
	    verbose_mode=1
	    ;;
	r)
	    refresh=1
	    ;;
	q)
	    quiet_mode=1
	    ;;
    esac
done

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
	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

cve=$(sha2cve $sha $refresh)
bsc=
if [ -n "$cve" ]
then
	bsc=$(cve2bugzilla $cve $refresh)
	cvss=$(cve2cvss $cve $refresh)
	echo "Security fix for $cve $bsc with CVSS ${cvss:-unknown}"
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
	cvss=0
fi
references="$cve $bsc"

branches_conf="$(fetch_branches $refresh)"

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

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

for_each_build_branch "$branches_conf" find_and_print_toplevel_actions $cvss

if [ ! -e "$actions_file" ] ; then
    echo "EVERYTHING IS OK!"
fi