#!/bin/sh

set -ex

# The script relies too much on subshells, so we use trap to catch when a
# subshell errored and needs to exit.
trap "exit 1" 9

# check_go_dep_v_prefix() adds a duplicated subcomponent (dependency) only for
# Go dependencies. The duplicated entry doesn't have the the version's `v`
# prefix that Go uses after the `@`.
# Fix for https://github.com/rancher/image-scanning/issues/478 .
# The first param is the subcomponent.
check_go_dep_v_prefix() {
    local dep="${1}"
    if echo "${dep}" | grep -qE "^pkg:golang/.*@v"; then
        dep_without_v=$(echo "${dep}" | sed "s/@v/@/")
        echo "${dep},${dep_without_v}"
    else
        echo "${dep}"
    fi
}

# format_product_dir() formats a product identifier in PURL format into its
# directory related format for the VEX Hub index.json.
# The first param is the product identifier in PURL format.
# Examples:
# - Input:  pkg:golang/github.com/kubernetes-sigs/cri-tools
# - Output: pkg/golang/github.com/kubernetes-sigs/cri-tools
# - Input:  pkg:oci/system-agent?repository_url=registry.rancher.com%2Francher%2Fsystem-agent
# - Output: pkg/oci/registry.rancher.com/rancher/system-agent
format_product_dir() {
    local product="${1}"

    if echo "${product}" | grep -q "^pkg:golang"; then
        echo "${product}" | sed "s/@.*//; s/^pkg:/pkg\//"
    elif echo "${product}" | grep -q "^pkg:oci"; then
        echo "${product}" | sed "s/.*repository_url\?=//; s/%2F/\//g; s/^/pkg\/oci\//"
    else
        echo "[ERROR] Unknown product dir format - ${product}"
        fatal
    fi
}

# format_product_id() is the opposite of format_product_dir() and formats a
# directory entry for the VEX Hub index.json into its originating product
# identifier in PURL format.
# The first param is the product identifier in directory format.
# Examples:
# - Input:  pkg/golang/k8s.io/kubernetes/scan.openvex.json
# - Output: pkg:golang/k8s.io/kubernetes
# - Input:  pkg/oci/index.docker.io/rancher/system-agent/scan.openvex.json
# - Output: pkg:oci/system-agent?repository_url=index.docker.io%2Francher%2Fsystem-agent'
format_product_id() {
    local product="${1}"

    product=$(dirname "${product}")
    if echo "${product}" | grep -q "^pkg/golang"; then
        echo "${product}" | sed "s/^pkg\//pkg:/"
    elif echo "${product}" | grep -q "^pkg/oci"; then
        local image=$(basename "${product}")
        local registry_full_path=$(echo "${product}" | sed "s/^pkg\/oci\///; s/\//%2F/g")
        echo "pkg:oci/${image}?repository_url=${registry_full_path}"
    else
        echo "[ERROR] Unknown product id format - ${product}"
        fatal
    fi
}

help() {
    echo "The script must be called with either 'generate' or 'upload'"
}

# generate() is the main function that generates all the needed VEX files.
generate() {
    mkdir -p "${REPORTS_VEX_DIR}/vexhub/pkg"
    mkdir -p "${REPORTS_VEX_DIR}/vexhub/reports"

    merge_cve_files
    generate_vex_files
    generate_vexhub_index
    merge_vex_files

    mv pkg/ "${REPORTS_VEX_DIR}/vexhub/"
    mv index.json "${REPORTS_VEX_DIR}/vexhub/"
    mv "${CONSOLIDATED_VEX_REPORT_FILENAME}" "${REPORTS_VEX_DIR}/vexhub/"
}

# generate_vex_files() is responsible for generating the individual VEX files
# per product.
generate_vex_files() {
    grep -v "${VEX_CSV_HEADER}" "${VEX_CVES_CSV}" | while IFS= read line; do
        local vuln=$(echo $line | awk --csv '{ print $1 }')
        local vuln_aliases=$(echo $line | awk --csv '{ print $2 }')
        local product=$(echo $line | awk --csv '{ print $3 }')
        local subcomponent=$(echo $line | awk --csv '{ print $4 }')
        if echo ${subcomponent} | grep -q "@$"; then
            # Skip subcomponents that don't have a version tag, i.e., they end
            # with an '@' and lack the version.
            echo -e "[WARN] Skipping subcomponent ${subcomponent} from CVE VEX line\n${line}"
            continue
        fi
        subcomponent=$(check_go_dep_v_prefix "${subcomponent}")

        local status=$(echo $line | awk --csv '{ print $5 }')
        local status_notes=$(echo $line | awk --csv '{ print $6 }')
        local justification=$(echo $line | awk --csv '{ print $7 }')
        local impact_stmt=$(echo $line | awk --csv '{ print $8 }')
        local action_stmt=$(echo $line | awk --csv '{ print $9 }')

        local product_dir=$(format_product_dir "${product}")
        mkdir -p "${product_dir}"

        local scan_report="${product_dir}/${VEX_REPORT_FILENAME}"
        if ! [[ -e "${scan_report}" ]]; then
            cp "${TEMPLATE_VEX_REPORT}" "${scan_report}"
        fi

        "${BIN_VEXCTL}" add                        \
            --document "${scan_report}" --in-place \
            --product "${product}"                 \
            --subcomponents "${subcomponent}"      \
            --vuln "${vuln}"                       \
            --aliases "${vuln_aliases}"            \
            --status "${status}"                   \
            --status-note "${status_note}"         \
            --justification "${justification}"     \
            --impact-statement "${impact_stmt}"    \
            --action-statement "${action_stmt}"
    done
}

# generate_vexhub_index() generates the VEX Hub index.json file.
generate_vexhub_index() {
    local vexhub_index=$(basename "${TEMPLATE_VEXHUB_INDEX}")
    local tmp_file=$(mktemp)

    cp "${TEMPLATE_VEXHUB_INDEX}" .

    for f in $(find pkg/ -type f -name "${VEX_REPORT_FILENAME}"); do
        local id=$(format_product_id "${f}")
        jq ".packages += [{\"id\": \"${id}\",\"location\": \"${f}\"}]" "${vexhub_index}" > "${tmp_file}"
        mv "${tmp_file}" "${vexhub_index}"
    done

    rm -rf "${tmp_file}"
}

git_clone_vexhub() {
    local dir="${1}"

    git clone "${VEX_HUB_REPO}" "${dir}"
}

# merge_vex_files() merges all individual VEX files into a single big file. This
# file is used for convenience in case a consumer wants to see all the VEXed
# entries in a single file.
merge_vex_files() {
    local tmp_file=$(mktemp)

    cp "${TEMPLATE_VEX_REPORT}" "${CONSOLIDATED_VEX_REPORT_FILENAME}"

    for f in $(find pkg/ -type f -name "${VEX_REPORT_FILENAME}"); do
        "${BIN_VEXCTL}" merge --author "${SEC_VEX_AUTHOR}" \
            "${CONSOLIDATED_VEX_REPORT_FILENAME}" "${f}" > "${tmp_file}"

        mv "${tmp_file}" "${CONSOLIDATED_VEX_REPORT_FILENAME}"
    done

    rm -rf "${tmp_file}"
}

# upload() the VEX files to the VEX Hub.
upload() {
    local vexhub_repo=$(echo "${VEX_HUB_REPO}" | sed "s,https://github.com/,,")
    local pr_title="${VEX_HUB_PR_TITLE} - $(date +'%Y-%m-%d')"
    local pr_number=$(gh pr list --repo "${vexhub_repo}" --label "${VEX_HUB_PR_LABEL}" --search "in:title ${VEX_HUB_PR_TITLE}" --author "${VEX_HUB_PR_AUTHOR}" --json number --jq ".[0].number")
    local pr_action="create --label ${VEX_HUB_PR_LABEL}"
    local commit=false

    git_clone_vexhub .

    rm -rf pkg
    mv "${REPORTS_VEX_DIR}/vexhub/pkg" .
    mv "${REPORTS_VEX_DIR}/vexhub/index.json" .

    mkdir -p reports
    mv "${REPORTS_VEX_DIR}/vex_cves.csv" reports/
    mv "${REPORTS_VEX_DIR}/vexhub/${CONSOLIDATED_VEX_REPORT_FILENAME}" reports/

    git checkout -b "${VEX_HUB_PR_BRANCH}"
    git add -N .

    # Only add the individual VEX reports if there are real changes. If the
    # changes are only related to timestamp and last_updated, then they are
    # ignored.
    for f in $(find pkg/ -type f); do
        if git status --porcelain | grep -q "${f}" && \
           git diff "${f}" | grep "^[+-] " | grep -vE " \"(last_updated|timestamp)\": "; then
            git add "${f}"
            commit=true
        else
            git restore "${f}"
        fi
    done

    # Same as the previous check, but now for the consolidated VEX report.
    if git status --porcelain | grep -q "reports/${CONSOLIDATED_VEX_REPORT_FILENAME}" && \
       git diff "reports/${CONSOLIDATED_VEX_REPORT_FILENAME}" | grep "^[+-] " | grep -vE " \"(last_updated|timestamp)\": "; then
        git add "reports/${CONSOLIDATED_VEX_REPORT_FILENAME}"
        commit=true
    else
        git restore "reports/${CONSOLIDATED_VEX_REPORT_FILENAME}"
    fi

    if git status --porcelain | grep -qE "index.json|reports/vex_cves.csv"; then
        commit=true
    fi

    if "${commit}"; then
        echo "[INFO] New VEX Hub reports created or updated"

        git add .
        git commit -m "${VEX_HUB_PR_BODY_COMMIT}"
        git push -f origin "${VEX_HUB_PR_BRANCH}"

        if [ "${pr_number}" != "" ]; then
            pr_action="edit ${pr_number} --add-label ${VEX_HUB_PR_LABEL}"
        fi
        gh pr ${pr_action} --body "${VEX_HUB_PR_BODY_COMMIT}" --title "${pr_title}"
    else
        echo "[INFO] No reports created or changed (timestamp related changes are ignored)"
    fi
}

main() {
    local param="${1}"

    source "${WORKING_DIR}/env.sh"
    source "${WORKING_DIR}/helper.sh"

    local tmp_vexhub_dir=$(mktemp -d)
    cd "${tmp_vexhub_dir}"

    case "${param}" in
        "generate")
            generate
            ;;
        "upload")
            upload
            ;;
        *)
            help
            ;;
    esac

    cd "${WORKING_DIR}"
    rm -rf "${tmp_vexhub_dir}"
}

WORKING_DIR=$(dirname $(realpath ${0}))
cd "${WORKING_DIR}"
main "${1}"

