DescriptionEcosystemMapper.java

  1. /*
  2.  * This file is part of dependency-check-core.
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *     http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  *
  16.  * Copyright (c) 2020 The OWASP Foundation. All Rights Reserved.
  17.  */
  18. package org.owasp.dependencycheck.data.nvd.ecosystem;

  19. import org.apache.commons.lang3.StringUtils;
  20. import io.github.jeremylong.openvulnerability.client.nvd.DefCveItem;

  21. import java.util.HashMap;
  22. import java.util.Map;
  23. import java.util.Map.Entry;
  24. import java.util.TreeMap;

  25. /**
  26.  * Helper utility for mapping CVEs to their ecosystems based on the description.
  27.  *
  28.  * @author skjolber
  29.  */
  30. public class DescriptionEcosystemMapper {

  31.     // static fields for thread-safe + hardcoded functionality
  32.     /**
  33.      * The array of ecosystems.
  34.      */
  35.     private static final String[] ECOSYSTEMS;
  36.     /**
  37.      * A helper map to retrieve the index of an ecosystem.
  38.      */
  39.     private static final int[] HINT_TO_ECOSYSTEM_LOOKUP;
  40.     /**
  41.      * Map of strings to ecosystems.
  42.      */
  43.     private static final TreeMap<String, EcosystemHint> ECOSYSTEM_MAP; // thread safe for reading

  44.     static {
  45.         ECOSYSTEM_MAP = new TreeMap<>();

  46.         for (FileExtensionHint fileExtensionHint : FileExtensionHint.values()) {
  47.             ECOSYSTEM_MAP.put(fileExtensionHint.getValue(), fileExtensionHint);
  48.         }
  49.         for (DescriptionKeywordHint descriptionKeywordHint : DescriptionKeywordHint.values()) {
  50.             ECOSYSTEM_MAP.put(descriptionKeywordHint.getValue(), descriptionKeywordHint);
  51.         }

  52.         final Map<String, Integer> ecosystemIndexes = new HashMap<>();

  53.         HINT_TO_ECOSYSTEM_LOOKUP = new int[ECOSYSTEM_MAP.size()];

  54.         int index = 0;
  55.         for (Entry<String, EcosystemHint> entry : ECOSYSTEM_MAP.entrySet()) {
  56.             final EcosystemHint ecosystemHint = entry.getValue();

  57.             Integer ecosystemIndex = ecosystemIndexes.get(ecosystemHint.getEcosystem());
  58.             if (ecosystemIndex == null) {
  59.                 ecosystemIndex = ecosystemIndexes.size();

  60.                 ecosystemIndexes.put(ecosystemHint.getEcosystem(), ecosystemIndex);
  61.             }

  62.             HINT_TO_ECOSYSTEM_LOOKUP[index] = ecosystemIndex;

  63.             index++;
  64.         }

  65.         ECOSYSTEMS = new String[ecosystemIndexes.size()];
  66.         ecosystemIndexes.forEach((key, value) -> ECOSYSTEMS[value] = key);
  67.     }

  68.     // take advantage of chars also being numbers
  69.     /**
  70.      * Prefix prefix for matching ecosystems.
  71.      */
  72.     private final boolean[] keywordPrefixes = getPrefixesFor(" -(\"'");
  73.     /**
  74.      * Postfix prefix for matching ecosystems.
  75.      */
  76.     private final boolean[] keywordPostfixes = getPrefixesFor(" -)\"',.:;");
  77.     /**
  78.      * Aho Corasick double array trie used for parsing and matching ecosystems.
  79.      */
  80.     private final StringAhoCorasickDoubleArrayTrie<EcosystemHint> ahoCorasickDoubleArrayTrie;

  81.     /**
  82.      * Constructs a new description ecosystem mapper.
  83.      */
  84.     public DescriptionEcosystemMapper() {
  85.         ahoCorasickDoubleArrayTrie = toAhoCorasickDoubleArrayTrie();
  86.     }

  87.     protected static boolean[] getPrefixesFor(String str) {
  88.         int max = -1;
  89.         for (int i = 0; i < str.length(); i++) {
  90.             if (max < str.charAt(i)) {
  91.                 max = str.charAt(i);
  92.             }
  93.         }

  94.         final boolean[] delimiters = new boolean[max + 1];
  95.         for (int i = 0; i < str.length(); i++) {
  96.             delimiters[str.charAt(i)] = true;
  97.         }
  98.         return delimiters;
  99.     }

  100.     protected static StringAhoCorasickDoubleArrayTrie<EcosystemHint> toAhoCorasickDoubleArrayTrie() {
  101.         final StringAhoCorasickDoubleArrayTrie<EcosystemHint> exact = new StringAhoCorasickDoubleArrayTrie<>();
  102.         exact.build(ECOSYSTEM_MAP);
  103.         return exact;
  104.     }

  105.     protected static boolean isExtension(String str, int begin, int end) {
  106.         if (str.length() != end && Character.isLetterOrDigit(str.charAt(end))) {
  107.             return false;
  108.         }

  109.         return isLowercaseAscii(str, begin + 1, end);
  110.     }

  111.     protected static boolean isLowercaseAscii(String multicase, int start, int end) {
  112.         for (int i = start; i < end; i++) {
  113.             final char c = multicase.charAt(i);

  114.             if (c < 'a' || c > 'z') {
  115.                 return false;
  116.             }
  117.         }
  118.         return true;
  119.     }

  120.     /**
  121.      * Tests if the string is a URL by looking for '://'.
  122.      *
  123.      * @param c the text to test.
  124.      * @param begin the position in the string to begin searching; note the
  125.      * search is decreasing to 0
  126.      * @return <code>true</code> if `://` is found; otherwise <code>false</code>
  127.      */
  128.     public static boolean isURL(String c, int begin) {
  129.         int pos = begin - 2;

  130.         while (pos > 2) {
  131.             pos--;

  132.             if (c.charAt(pos) == ' ') {
  133.                 return false;
  134.             }
  135.             if (c.charAt(pos) == ':') {
  136.                 return c.charAt(pos + 1) == '/' && c.charAt(pos + 2) == '/';
  137.             }
  138.         }

  139.         return false;
  140.     }

  141.     protected void increment(int i, int[] ecosystemMap) {
  142.         ecosystemMap[HINT_TO_ECOSYSTEM_LOOKUP[i]]++;
  143.     }

  144.     /**
  145.      * Returns the ecosystem if identified by English description from the CVE
  146.      * data.
  147.      *
  148.      * @param cve the CVE data
  149.      * @return the ecosystem if identified
  150.      */
  151.     public String getEcosystem(DefCveItem cve) {
  152.         final int[] ecosystemMap = new int[ECOSYSTEMS.length];
  153.         cve.getCve().getDescriptions().stream()
  154.                 .filter((langString) -> (langString.getLang().equals("en")))
  155.                 .forEachOrdered((langString) -> search(langString.getValue(), ecosystemMap));
  156.         return getResult(ecosystemMap);
  157.     }

  158.     /**
  159.      * Determines the ecosystem for the given string.
  160.      *
  161.      * @param multicase the string to test
  162.      * @return the ecosystem
  163.      */
  164.     public String getEcosystem(String multicase) {
  165.         final int[] ecosystemMap = new int[ECOSYSTEMS.length];
  166.         search(multicase, ecosystemMap);
  167.         return getResult(ecosystemMap);
  168.     }

  169.     private void search(String multicase, int[] ecosystemMap) {
  170.         final String c = multicase.toLowerCase();
  171.         ahoCorasickDoubleArrayTrie.parseText(c, (begin, end, value, index) -> {
  172.             if (value.getNature() == EcosystemHintNature.FILE_EXTENSION) {
  173.                 if (!isExtension(multicase, begin, end)) {
  174.                     return;
  175.                 }

  176.                 final String ecosystem = value.getEcosystem();
  177.                 // real extension, if not part of url
  178.                 if (Ecosystem.PHP.equals(ecosystem) && c.regionMatches(begin, ".php", 0, 4)) {
  179.                     if (isURL(c, begin)) {
  180.                         return;
  181.                     }
  182.                 } else if (Ecosystem.JAVA.equals(ecosystem) && c.regionMatches(begin, ".jsp", 0, 4)) {
  183.                     if (isURL(c, begin)) {
  184.                         return;
  185.                     }
  186.                 }
  187.             } else { // keyword

  188.                 // check if full word, i.e. typically space first and then space or dot after
  189.                 if (begin != 0) {
  190.                     final char startChar = c.charAt(begin - 1);
  191.                     if (startChar >= keywordPrefixes.length || !keywordPrefixes[startChar]) {
  192.                         return;
  193.                     }
  194.                 }
  195.                 if (end != c.length()) {
  196.                     final char endChar = c.charAt(end);
  197.                     if (endChar >= keywordPostfixes.length || !keywordPostfixes[endChar]) {
  198.                         return;
  199.                     }
  200.                 }

  201.                 final String ecosystem = value.getEcosystem();
  202.                 if (Ecosystem.NATIVE.equals(ecosystem)) { // TODO could be checked afterwards
  203.                     if (StringUtils.contains(c, "android")) {
  204.                         return;
  205.                     }
  206.                 }
  207.             }
  208.             increment(index, ecosystemMap);
  209.         });
  210.     }

  211.     private String getResult(int[] values) {
  212.         final int best = getBestScore(values);
  213.         if (best != -1) {
  214.             return ECOSYSTEMS[best];
  215.         }
  216.         return null;
  217.     }

  218.     private int getBestScore(int[] values) {
  219.         int bestIndex = -1;
  220.         int bestScore = -1;
  221.         for (int i = 0; i < values.length; i++) {
  222.             if (values[i] > 0) {
  223.                 if (values[i] > bestScore) {
  224.                     bestIndex = i;
  225.                     bestScore = values[i];
  226.                 }
  227.                 values[i] = 0;
  228.             }
  229.         }
  230.         return bestIndex;
  231.     }
  232. }