diff --git a/security/vuxml/Makefile b/security/vuxml/Makefile index 9a3ef8b7a291..243b5cd5723e 100644 --- a/security/vuxml/Makefile +++ b/security/vuxml/Makefile @@ -1,120 +1,116 @@ PORTNAME= vuxml PORTVERSION= 1.1 PORTREVISION= 6 CATEGORIES= security textproc MASTER_SITES= http://www.vuxml.org/dtd/vuxml-1/ DISTFILES= vuxml-10.dtd vuxml-model-10.mod \ vuxml-11.dtd vuxml-model-11.mod \ xml1.dcl catalog catalog.xml DIST_SUBDIR= vuxml MAINTAINER= ports-secteam@FreeBSD.org COMMENT= Vulnerability and eXposure Markup Language DTD WWW= https://vuxml.freebsd.org/ LICENSE= BSD2CLAUSE RUN_DEPENDS= xmlcatmgr:textproc/xmlcatmgr \ xsltproc:textproc/libxslt \ ${LOCALBASE}/share/xml/dtd/xhtml-modularization/VERSION:textproc/xhtml-modularization \ ${LOCALBASE}/share/xml/dtd/xhtml-basic/xhtml-basic10.dtd:textproc/xhtml-basic USES= python:run NO_MTREE= yes NO_ARCH= yes NO_BUILD= yes WRKSRC= ${WRKDIR} dir_DTD= share/xml/dtd/vuxml .include VUXML_FILE?= ${PKGDIR}/vuln.xml VUXML_FLAT_NAME= vuln-flat.xml VUXML_FLAT_FILE?= ${PKGDIR}/${VUXML_FLAT_NAME} _YEAR!= date +%Y VUXML_CURRENT_FILE?= ${PKGDIR}/vuln/${_YEAR}.xml post-clean: @${RM} "${VUXML_FILE}.tidy" @${RM} "${VUXML_FLAT_FILE}" do-extract: @${RM} -r ${WRKDIR} @${MKDIR} ${WRKDIR} .for f in ${DISTFILES} ${CP} ${_DISTDIR}/${f} ${WRKDIR}/${f} .endfor do-install: @${MKDIR} ${STAGEDIR}${PREFIX}/${dir_DTD} .for f in ${DISTFILES} ${INSTALL_DATA} ${WRKSRC}/${f} ${STAGEDIR}${PREFIX}/${dir_DTD}/${f} .endfor do-test: @${MKDIR} ${WRKDIR}/test @${CP} -R ${.CURDIR}/vuln.xml ${.CURDIR}/vuln ${WRKDIR}/test @cd ${.CURDIR} && make validate PKGDIR=${WRKDIR}/test ${VUXML_FLAT_NAME}: ${VUXML_FILE} vuln/*.xml xmllint -noent ${.ALLSRC:[1]} > ${.TARGET} validate: tidy @${SH} ${FILESDIR}/validate.sh "${VUXML_FLAT_FILE}" @${ECHO_MSG} Checking if tidy differs... @if ${DIFF} -u "${VUXML_FLAT_FILE}" "${VUXML_FILE}.tidy"; \ then \ ${ECHO_MSG} ... seems okay; \ ${RM} "${VUXML_FILE}.tidy"; \ else \ return 1; \ fi @${ECHO_MSG} Checking for space/tab... @unexpand "${VUXML_FLAT_FILE}" | ${SED} -E 's,[[:space:]]*$$,,g' > "${VUXML_FILE}.unexpanded" @if ${DIFF} -u "${VUXML_FLAT_FILE}" "${VUXML_FILE}.unexpanded"; \ then \ ${ECHO_MSG} ... seems okay; \ ${RM} "${VUXML_FILE}.unexpanded"; \ else \ ${ECHO_MSG} ... see above; \ ${ECHO_CMD} Consider using ${VUXML_FILE}.unexpanded for final commit; \ return 1; \ fi ${PYTHON_CMD} ${FILESDIR}/extra-validation.py ${VUXML_FLAT_FILE} @${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} tidy: ${VUXML_FLAT_NAME} @if [ ! -e ${LOCALBASE}/share/xml/dtd/vuxml/catalog.xml ]; \ then \ echo "Please install the VuXML port prior to running make validate/tidy."; \ exit 1; \ fi ${SH} ${FILESDIR}/tidy.sh "${FILESDIR}/tidy.xsl" "${VUXML_FLAT_FILE}" > "${VUXML_FILE}.tidy" newentry: @${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} @${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 work/${VID}.html: ${FILESDIR}/html.xsl ${FILESDIR}/common.css ${VUXML_FILE} ${MKDIR} work xsltproc --stringparam vid "${VID}" \ --output ${.TARGET} \ ${FILESDIR}/html.xsl ${VUXML_FILE} ${INSTALL_DATA} ${FILESDIR}/common.css work .endif .include diff --git a/security/vuxml/files/euvd_provider.sh b/security/vuxml/files/euvd_provider.sh new file mode 100644 index 000000000000..821d2fcc06a2 --- /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 index 000000000000..c2b1f8ffa73e --- /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 index 0298a5376a9e..4c8b09636112 100644 --- a/security/vuxml/files/newentry.sh +++ b/security/vuxml/files/newentry.sh @@ -1,188 +1,270 @@ #! /bin/sh set -eu vuxml_file="$1" CVE_ID="" SA_ID="" show_usage() { exec >&2 echo "Usage: newentry.sh /path/to/vuxml/document [CVE_ID|SA_ID]" exit 1 } if [ -z "${vuxml_file}" ]; then show_usage fi +# ----------------- +# Process arguments +# ----------------- shift while [ $# -gt 0 ]; do case "$1" in CVE_ID=*) CVE_ID="${1#CVE_ID=}" shift ;; SA_ID=*) SA_ID="${1#SA_ID=}" shift ;; *) echo "Invalid argument: $1" show_usage exit 1 ;; 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" upstream_fix="" impact="" DESC_BODY="

${source} reports:

${details}

" - -# 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:

${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 cve_tmp=$(grep 'CVE Name' "${tmp_fbsd_sa}" | cut -f2 -d:) || exit 1 cvename="${cve_tmp#"${cve_tmp%%[![:space:]]*}"}" # NVD database only accepts uppercase CVE ids, like CVE-2022-39282, NOT # cve-2022-39282. cvename=$(echo "${cvename}" | tr '[:lower:]' '[:upper:]') || exit 1 cveurl="https://nvd.nist.gov/vuln/detail/${cvename}" fi details=$(awk '/II. Problem Description/ {f=1;next;next} /III. Impact/ {f=0} (f==1) {print}' "${tmp_fbsd_sa}" ) || exit 1 details=$(echo "

${details}

" | fmt -p -s | sed -e 's/

/

/' | sed '1!s/^/\t/') impact=$(awk '/III. Impact/ {f=1;next;next} /IV. Workaround/ {f=0} (f==1) {print}' "${tmp_fbsd_sa}") || exit 1 impact=$(echo "

${impact}

" | fmt -p -s | sed -e 's/

/

/' | sed '1!s/^/\t/') package_name="FreeBSD" if grep -Eq 'Module:.*kernel' "${tmp_fbsd_sa}"; then package_name="${package_name}-kernel" fi upstream_fix="FIXME" references="${SA_URL_BASE}${SA_ID}" source="The FreeBSD Project" topic_tmp=$(grep 'Topic:' "${tmp_fbsd_sa}" | cut -f2 -d:) || exit 1 topic="${topic_tmp#"${topic_tmp%%[![:space:]]*}"}" DESC_BODY="

Problem Description:

${details}

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 cat << EOF >> "${tmp}" || exit 1 ${package_name} -- ${topic} - - ${package_name} - ${upstream_fix} - +${package_list} - ${DESC_BODY} + ${DESC_BODY} ${cvename} ${cveurl} - ${discovery} - ${entry} + ${discovery_date} + ${entry_date} EOF awk '/^[[:space:]]+> "${tmp}" || exit 1 if cp "${tmp}" "${vuxml_file}"; then exec ${EDITOR:-vi} "${vuxml_file}" else doclean="no" exec >&2 echo "Could not overwrite \`${vuxml_file}'." echo "Results are left in \`${tmp}'." exit 1 fi diff --git a/security/vuxml/files/nvd_provider.sh b/security/vuxml/files/nvd_provider.sh new file mode 100644 index 000000000000..8a383a7d6752 --- /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. +} +