diff --git a/security/vuxml/Makefile b/security/vuxml/Makefile --- a/security/vuxml/Makefile +++ b/security/vuxml/Makefile @@ -102,10 +102,6 @@ @${ECHO_CMD} 'Also, tags are usually wrong in ranges. Use where adequate.' @${ECHO_CMD} @${SH} ${FILESDIR}/newentry.sh "${VUXML_CURRENT_FILE}" "CVE_ID=${CVE_ID}" "SA_ID=${SA_ID}" - @${ECHO_CMD} - @${ECHO_CMD} 'Be sure to get versioning right for PORTEPOCH and remember possible linux-* ports!' - @${ECHO_CMD} 'Also, tags are usually wrong in ranges. Use where adequate.' - @${ECHO_CMD} .if defined(VID) && !empty(VID) html: work/${VID}.html diff --git a/security/vuxml/files/euvd_provider.sh b/security/vuxml/files/euvd_provider.sh new file mode 100644 --- /dev/null +++ b/security/vuxml/files/euvd_provider.sh @@ -0,0 +1,69 @@ +# Provider for the European Union Vulnerability Database +# https://euvd.enisa.europa.eu/ + +tmp_euvd="" + +init_euvd() { + tmp_euvd=$(mktemp "${TMPDIR:-/tmp}"/euvd_json_data.XXXXXXXXXX) || exit 1 + fetch -q -o "${tmp_euvd}" "https://euvdservices.enisa.europa.eu/api/enisaid?id=${CVE_ID}" || exit 1 +} + +cleanup_euvd() { + rm -f "${tmp_euvd}" 2>/dev/null +} + +get_cvename_from_euvd() { + # EUVD response includes "aliases" (CVE ID if available) + jq -r '.aliases // .id' "${tmp_euvd}" +} + +get_cveurl_from_euvd() { + echo "https://euvd.enisa.europa.eu/ui/vuln/${CVE_ID}" +} + +get_details_from_euvd() { + jq -r '.description // empty | @html' "${tmp_euvd}" | fmt -p -s | sed '1!s/^/\t/' +} + +get_discovery_date_from_euvd() { + raw=$(jq -r '.datePublished // empty' "${tmp_euvd}") + if [ -n "$raw" ]; then + trimmed=$(echo "$raw" | cut -d, -f1-2) + if date -d "$trimmed" "+%Y-%m-%d" >/dev/null 2>&1; then + date -d "$trimmed" "+%Y-%m-%d" + else + date -j -f "%b %d, %Y" "$trimmed" "+%Y-%m-%d" + fi + fi +} + +get_entry_date_from_euvd() { + echo "${entry_date}" +} + + +get_product_name_from_euvd() { + jq -r ' .enisaIdProduct[]?.product?.name ' "${tmp_euvd}" +} + +get_product_range_from_euvd() { + jq -r '.enisaIdProduct[]? | "\(.product_version? | gsub("<";"<") | gsub(">";">") | gsub("&";"&"))"' "${tmp_euvd}" +} + +get_package_name_from_euvd() { + jq -r '.enisaIdProduct[0]?.product?.name // empty' "${tmp_euvd}" +} + +get_references_from_euvd() { + jq -r '.references // empty | @html' "${tmp_euvd}" | tr " " "\n" +} + +get_source_from_euvd() { + jq -r '.assigner // empty | @html' "${tmp_euvd}" +} + +get_topic_from_euvd() { + # Use first sentence of description + jq -r '.description // empty' "${tmp_euvd}" | cut -f1 -d. +} + diff --git a/security/vuxml/files/mitre_provider.sh b/security/vuxml/files/mitre_provider.sh new file mode 100644 --- /dev/null +++ b/security/vuxml/files/mitre_provider.sh @@ -0,0 +1,61 @@ +# Provider for MITRE +# https://www.mitre.org/ + +tmp_mitre="" + +init_mitre() +{ + tmp_mitre=$(mktemp "${TMPDIR:-/tmp}"/mitre.XXXXXXXXXX) || exit 1 + fetch -q -o "${tmp_mitre}" https://cveawg.mitre.org/api/cve/"${CVE_ID}" +} + +cleanup_mitre() +{ + rm "${tmp_mitre}" 2>/dev/null +} + +get_cvename_from_mitre() +{ + cvename="${CVE_ID}" + echo "${cvename}" +} + +get_cveurl_from_mitre() { + echo https://cveawg.mitre.org/api/cve/"${CVE_ID}" +} + +get_details_from_mitre() { + jq -r '.containers?.cna?.descriptions[0]?.value' "${tmp_mitre}" | fmt -p -s +} + +get_discovery_date_from_mitre() { + jq -r '.cveMetadata?.datePublished?' "${tmp_mitre}" | cut -f1 -dT +} + +get_entry_date_from_mitre() { + echo "${entry_date}" +} + +get_product_name_from_mitre() { + jq -r '.containers?.cna?.affected[]?.product' "${tmp_mitre}" +} + +get_product_range_from_mitre() { + jq -r '.containers?.cna?.affected[]??.versions[0]?.lessThan' "${tmp_mitre}" +} + +get_package_name_from_mitre() { + jq -r '.containers?.cna?.affected[0]?.product' "${tmp_mitre}" +} + +get_references_from_mitre() { + jq -r '.containers?.cna?.references[0]?.url' "${tmp_mitre}" | fmt -p -s +} + +get_source_from_mitre() { + jq -r '.containers?.cna?.references[0]?.url' "${tmp_mitre}" +} + +get_topic_from_mitre() { + jq -r ".containers?.cna?.problemTypes[0]?.descriptions[0]?.description" "${tmp_mitre}" +} diff --git a/security/vuxml/files/newentry.sh b/security/vuxml/files/newentry.sh --- a/security/vuxml/files/newentry.sh +++ b/security/vuxml/files/newentry.sh @@ -15,6 +15,9 @@ show_usage fi +# ----------------- +# Process arguments +# ----------------- shift while [ $# -gt 0 ]; do case "$1" in @@ -34,27 +37,45 @@ esac done -tmp="`mktemp ${TMPDIR:-/tmp}/vuxml.XXXXXXXXXX`" || exit 1 +tmp=$(mktemp "${TMPDIR:-/tmp}"/vuxml.XXXXXXXXXX) || exit 1 tmp_fbsd_sa="" -tmp_mitre="" -tmp_nvd="" +# ------------------------------------- +# Define how to clean up temporal files +# ------------------------------------- +# doclean="yes" cleanup() { if [ "${doclean}" = "yes" ]; then - rm -f "${tmp}" "${tmp_fbsd_sa}" "${tmp_mitre}" "${tmp_nvd}" > /dev/null + rm -f "${tmp}" "${tmp_fbsd_sa}" > /dev/null fi + + # Call cleaners for providers + for provider in ${providers}; do + cleanup_"${provider}" + cleanup_"${provider}" + done } -trap cleanup EXIT 1 2 13 15 +trap cleanup EXIT HUP INT PIPE TERM -vid="`uuidgen | tr '[:upper:]' '[:lower:]'`" +# ----------------------------- +# Variables with default values +# ----------------------------- +vid="$(uuidgen | tr '[:upper:]' '[:lower:]')" [ -z "$vid" ] && exit 1 + +discovery_date="" cvename="INSERT CVE RECORD IF AVAILABLE" cveurl="INSERT BLOCKQUOTE URL HERE" details="." -discovery="`date -u '+%Y-%m'`-FIXME" || exit 1 -entry="`date -u '+%Y-%m-%d'`" || exit 1 +discovery_date="$(date -u '+%Y-%m')-FIXME" || exit 1 +entry_date="$(date -u '+%Y-%m-%d')" || exit 1 package_name="" +product_name="" +product_range="" +package_list=" + +" references="INSERT URL HERE" topic="" source="SO-AND-SO" @@ -67,38 +88,65 @@ " - -# Try to retrieve information if a CVE identifier was provided -if [ -n "${CVE_ID}" ]; then +# -------------------------------- +# Check we have everything we need +# -------------------------------- +check_dependencies() +{ if ! command -v jq > /dev/null; then echo textproc/jq is needed for CVE automatic entry fill exit 1 fi +} + +# ------------------------------------------ +# List of CVE providers sorted by preference +# ------------------------------------------ +providers="mitre nvd euvd" + +# ------------------------------------------ +# List of fields to query for every provider +# ------------------------------------------ +fields="cvename cveurl details discovery_date entry_date product_name product_range package_name references source topic" - # NVD database only accepts uppercase CVE ids, like CVE-2022-39282, NOT - # cve-2022-39282. - CVE_ID=$(echo "${CVE_ID}" | tr '[:lower:]' '[:upper:]') || exit 1 - - # Get information from the NVD database JSON format - tmp_nvd="`mktemp ${TMPDIR:-/tmp}/nvd_json_data.XXXXXXXXXX`" || exit 1 - fetch -q -o "${tmp_nvd}" https://services.nvd.nist.gov/rest/json/cves/2.0?cveId="${CVE_ID}" || exit 1 - # Get information from MITRE database (they provide a nice "topic") - tmp_mitre="`mktemp ${TMPDIR:-/tmp}/mitre.XXXXXXXXXX`" || exit 1 - fetch -q -o "${tmp_mitre}" https://cveawg.mitre.org/api/cve/"${CVE_ID}" - - # Create variables from input and online sources - cvename="${CVE_ID}" - cveurl=https://nvd.nist.gov/vuln/detail/${CVE_ID} - pref=.vulnerabilities[0].cve - details=$(jq -r "${pref}.descriptions[0].value|@html" "${tmp_nvd}" | fmt -p -s | sed '1!s/^/\t/') || exit 1 - discovery=$(jq -r "${pref}.published|@html" "${tmp_nvd}" | cut -f1 -dT) || exit 1 - pref=.vulnerabilities[0].cve.configurations[0].nodes[0].cpeMatch[0] - package_name=$(jq -r "${pref}.criteria|@html" "${tmp_nvd}" | cut -f4 -d:) || exit 1 - upstream_fix=$(jq -r "${pref}.versionEndExcluding|@html" "${tmp_nvd}") || exit 1 - pref=.vulnerabilities[0].cve.references[0] - references=$(jq -r "${pref}.url|@html" "${tmp_nvd}" | tr " " "\n") || exit 1 - source=$(jq -r "${pref}.source|@html" "${tmp_nvd}" | tr " " "\n") || exit 1 - topic=$(jq -r ".containers.cna.title|@html" "${tmp_mitre}" ) || exit 1 +# Some providers only allow for upper case identifiers +CVE_ID=$(echo "${CVE_ID}" | tr '[:lower:]' '[:upper:]') || exit 1 + +# ----------------------------------------------------------------------------- +# Generic resolver +# +# Gets a variable name and the list of providers and returns the value of the +# variable. If the first defined provider returns empty or nullm, it tries with +# the next one until one provider returns a value or we run out of providers +# ----------------------------------------------------------------------------- +resolve_field() { + field="${1}" + shift + providers="$@" + + for provider in $providers; do + func="get_${field}_from_${provider}" + if command -v "${func}" >/dev/null 2>&1; then + value="$($func)" + if [ -n "${value}" ] && [ "${value}" != "null" ] && [ "${value}" != "n/a" ]; then + echo "${value}" + return 0 + fi + else + echo "Warning: function ${func} not implemented in provider ${provider}" + fi + done + echo "null" +} + +# -------------------------------------------------- +# Fill global variables with data from CVE databases +# -------------------------------------------------- +get_cve_info() { + for field in ${fields}; do + value=$(resolve_field "${field}" ${providers}) + eval "${field}=\$value" + done DESC_BODY="

${source} reports:

@@ -106,14 +154,17 @@

${details}

" -fi +} -if [ -n "${SA_ID}" ]; then +# ---------------------------------------------------------------- +# Fill global variables with data from FreeBSD Security Advisories +# ---------------------------------------------------------------- +get_sa_info() { SA_URL_BASE=https://www.freebsd.org/security/advisories/ # Get information from the Project's SA site - tmp_fbsd_sa="$(mktemp ${TMPDIR:-/tmp}/fbsd_sa_data.XXXXXXXXXX)" || exit 1 - fetch -q -o "${tmp_fbsd_sa}" ${SA_URL_BASE}${SA_ID} || exit 1 + tmp_fbsd_sa=$(mktemp "${TMPDIR:-/tmp}/fbsd_sa_data.XXXXXXXXXX") || exit 1 + fetch -q -o "${tmp_fbsd_sa}" "${SA_URL_BASE}${SA_ID}" || exit 1 # Create variables from SA note if grep -q 'CVE Name' "${tmp_fbsd_sa}"; then @@ -148,6 +199,40 @@

Impact:

${impact} " +} + +init_providers() { + for provider in files/*_provider.sh; do + provider_name=$(basename "${provider}" | cut -f1 -d_) + . "files/${provider_name}_provider.sh" + init_"${provider_name}" + done +} + +create_packages_list() { + tmp_prod=$(mktemp "${TMPDIR:-/tmp}"/vuxml.prod.XXXXXXXXXX) || exit 1 + tmp_ver=$(mktemp "${TMPDIR:-/tmp}"/vuxml.ver.XXXXXXXXXX) || exit 1 + printf "%s" "${product_name}" > "${tmp_prod}" + printf "%s" "${product_range}" > "${tmp_ver}" + + package_list=$(paste "${tmp_prod}" "${tmp_ver}" | sed \ + -e 's|\t|\n\t|g' \ + -e 's|^| \n\t|g' \ + -e 's|$|\n
|g') + + rm "${tmp_prod}" "${tmp_ver}" 2>/dev/null +} + +# Try to retrieve information if a CVE identifier was provided +if [ -n "${CVE_ID}" ]; then + check_dependencies + init_providers + get_cve_info "${CVE_ID}" + create_packages_list +fi + +if [ -n "${SA_ID}" ]; then + get_sa_info fi awk '/^<\?/,/^> "${tmp}" || exit 1 @@ -155,21 +240,18 @@ ${package_name} -- ${topic} - - ${package_name} - ${upstream_fix} - +${package_list} - ${DESC_BODY} + ${DESC_BODY} ${cvename} ${cveurl} - ${discovery} - ${entry} + ${discovery_date} + ${entry_date} diff --git a/security/vuxml/files/nvd_provider.sh b/security/vuxml/files/nvd_provider.sh new file mode 100644 --- /dev/null +++ b/security/vuxml/files/nvd_provider.sh @@ -0,0 +1,72 @@ +# Provider for the National Vulnerability Database +# https://nvd.nist.gov/ + +tmp_nvd="" + +init_nvd() +{ + tmp_nvd=$(mktemp "${TMPDIR:-/tmp}"/nvd_json_data.XXXXXXXXXX) || exit 1 + fetch -q -o "${tmp_nvd}" https://services.nvd.nist.gov/rest/json/cves/2.0?cveId="${CVE_ID}" || exit 1 +} + +cleanup_nvd() +{ + rm "${tmp_nvd}" 2>/dev/null +} + +get_cvename_from_nvd() +{ + cvename="${CVE_ID}" + echo "${cvename}" +} + +get_cveurl_from_nvd() { + cveurl=https://nvd.nist.gov/vuln/detail/${CVE_ID} + echo "${cveurl}" +} + +get_details_from_nvd() { + pref=".vulnerabilities[0]?.cve?" + jq -r "${pref}.descriptions[0]?.value|@html" "${tmp_nvd}" | fmt -p -s | sed '1!s/^/\t/' +} + +get_discovery_date_from_nvd() { + pref=".vulnerabilities[0]?.cve?" + jq -r "${pref}.published|@html" "${tmp_nvd}" | cut -f1 -dT +} + +get_entry_date_from_nvd() { + echo "${entry_date}" +} + +get_product_name_from_nvd() { + jq -r '.vulnerabilities[]?.cve?.configurations[]?.nodes[]?.cpeMatch[]? | + (.criteria | split(":")[4])' "${tmp_nvd}" +} + +get_product_range_from_nvd() { + jq -r '.vulnerabilities[]?.cve.configurations[]?.nodes[]?.cpeMatch[]?.versionEndExcluding ' "${tmp_nvd}" +} + +get_package_name_from_nvd() { + jq -r '.vulnerabilities[]?.cve?.configurations[]?.nodes[]?.cpeMatch[0]?.criteria' "${tmp_nvd}" | cut -f5 -d: +} + +get_references_from_nvd() { + pref=".vulnerabilities[0]?.cve?.references[0]?" + jq -r "${pref}.url|@html" "${tmp_nvd}" | tr " " "\n" +} + +get_source_from_nvd() +{ + pref=".vulnerabilities[0]?.cve?.references[0]?" + jq -r "${pref}.source|@html" "${tmp_nvd}" | tr " " "\n" +} + +get_topic_from_nvd() { + # NVD does not provide a nice summary. Let's use the first sentence from + # the details instead + pref=".vulnerabilities[0]?.cve?" + jq -r "${pref}.descriptions[0]?.value|@html" "${tmp_nvd}" | cut -f1 -d. +} +