|
| 1 | +require 'json' |
| 2 | +require 'csv' |
| 3 | +require 'heimdall_tools/hdf' |
| 4 | +require 'utilities/xml_to_hash' |
| 5 | + |
| 6 | +RESOURCE_DIR = Pathname.new(__FILE__).join('../../data') |
| 7 | + |
| 8 | +CWE_NIST_MAPPING_FILE = File.join(RESOURCE_DIR, 'cwe-nist-mapping.csv') |
| 9 | + |
| 10 | +IMPACT_MAPPING = { |
| 11 | + high: 0.7, |
| 12 | + medium: 0.5, |
| 13 | + low: 0.3, |
| 14 | +}.freeze |
| 15 | + |
| 16 | +SNYK_VERSION_REGEX = 'v(\d+.)(\d+.)(\d+)'.freeze |
| 17 | + |
| 18 | +DEFAULT_NIST_TAG = ["SA-11", "RA-5"].freeze |
| 19 | + |
| 20 | +# Loading spinner sign |
| 21 | +$spinner = Enumerator.new do |e| |
| 22 | + loop do |
| 23 | + e.yield '|' |
| 24 | + e.yield '/' |
| 25 | + e.yield '-' |
| 26 | + e.yield '\\' |
| 27 | + end |
| 28 | +end |
| 29 | + |
| 30 | +module HeimdallTools |
| 31 | + class SnykMapper |
| 32 | + def initialize(synk_json, name=nil, verbose = false) |
| 33 | + @synk_json = synk_json |
| 34 | + @verbose = verbose |
| 35 | + |
| 36 | + begin |
| 37 | + @cwe_nist_mapping = parse_mapper |
| 38 | + @projects = JSON.parse(synk_json) |
| 39 | + |
| 40 | + # Cover single and multi-project scan use cases. |
| 41 | + unless @projects.kind_of?(Array) |
| 42 | + @projects = [ @projects ] |
| 43 | + end |
| 44 | + |
| 45 | + rescue StandardError => e |
| 46 | + raise "Invalid Snyk JSON file provided Exception: #{e}" |
| 47 | + end |
| 48 | + end |
| 49 | + |
| 50 | + def extract_scaninfo(project) |
| 51 | + info = {} |
| 52 | + begin |
| 53 | + info['policy'] = project['policy'] |
| 54 | + reg = Regexp.new(SNYK_VERSION_REGEX, Regexp::IGNORECASE) |
| 55 | + info['version'] = info['policy'].scan(reg).join |
| 56 | + info['projectName'] = project['projectName'] |
| 57 | + info['summary'] = project['summary'] |
| 58 | + |
| 59 | + info |
| 60 | + rescue StandardError => e |
| 61 | + raise "Error extracting project info from Synk JSON file provided Exception: #{e}" |
| 62 | + end |
| 63 | + end |
| 64 | + |
| 65 | + def finding(vulnerability) |
| 66 | + finding = {} |
| 67 | + finding['status'] = 'failed' |
| 68 | + finding['code_desc'] = "From : [ #{vulnerability['from'].join(" , ").to_s } ]" |
| 69 | + finding['run_time'] = NA_FLOAT |
| 70 | + |
| 71 | + # Snyk results does not profile scan timestamp; using current time to satisfy HDF format |
| 72 | + finding['start_time'] = NA_STRING |
| 73 | + [finding] |
| 74 | + end |
| 75 | + |
| 76 | + def nist_tag(cweid) |
| 77 | + entries = @cwe_nist_mapping.select { |x| cweid.include? x[:cweid].to_s } |
| 78 | + tags = entries.map { |x| x[:nistid] } |
| 79 | + tags.empty? ? DEFAULT_NIST_TAG : tags.flatten.uniq |
| 80 | + end |
| 81 | + |
| 82 | + def parse_identifiers(vulnerability, ref) |
| 83 | + # Extracting id number from reference style CWE-297 |
| 84 | + vulnerability['identifiers'][ref].map { |e| e.split("#{ref}-")[1] } |
| 85 | + rescue |
| 86 | + return [] |
| 87 | + end |
| 88 | + |
| 89 | + def impact(severity) |
| 90 | + IMPACT_MAPPING[severity.to_sym] |
| 91 | + end |
| 92 | + |
| 93 | + def parse_mapper |
| 94 | + csv_data = CSV.read(CWE_NIST_MAPPING_FILE, **{ encoding: 'UTF-8', |
| 95 | + headers: true, |
| 96 | + header_converters: :symbol, |
| 97 | + converters: :all }) |
| 98 | + csv_data.map(&:to_hash) |
| 99 | + end |
| 100 | + |
| 101 | + def desc_tags(data, label) |
| 102 | + { "data": data || NA_STRING, "label": label || NA_STRING } |
| 103 | + end |
| 104 | + |
| 105 | + # Snyk report could have multiple vulnerability entries for multiple findings of same issue type. |
| 106 | + # The meta data is identical across entries |
| 107 | + # method collapse_duplicates return unique controls with applicable findings collapsed into it. |
| 108 | + def collapse_duplicates(controls) |
| 109 | + unique_controls = [] |
| 110 | + |
| 111 | + controls.map { |x| x['id'] }.uniq.each do |id| |
| 112 | + collapsed_results = controls.select { |x| x['id'].eql?(id) }.map {|x| x['results']} |
| 113 | + unique_control = controls.find { |x| x['id'].eql?(id) } |
| 114 | + unique_control['results'] = collapsed_results.flatten |
| 115 | + unique_controls << unique_control |
| 116 | + end |
| 117 | + unique_controls |
| 118 | + end |
| 119 | + |
| 120 | + |
| 121 | + def to_hdf |
| 122 | + project_results = {} |
| 123 | + @projects.each do | project | |
| 124 | + controls = [] |
| 125 | + project['vulnerabilities'].each do | vulnerability | |
| 126 | + printf("\rProcessing: %s", $spinner.next) |
| 127 | + |
| 128 | + item = {} |
| 129 | + item['tags'] = {} |
| 130 | + item['descriptions'] = [] |
| 131 | + item['refs'] = NA_ARRAY |
| 132 | + item['source_location'] = NA_HASH |
| 133 | + item['descriptions'] = NA_ARRAY |
| 134 | + |
| 135 | + item['title'] = vulnerability['title'].to_s |
| 136 | + item['id'] = vulnerability['id'].to_s |
| 137 | + item['desc'] = vulnerability['description'].to_s |
| 138 | + item['impact'] = impact(vulnerability['severity']) |
| 139 | + item['code'] = '' |
| 140 | + item['results'] = finding(vulnerability) |
| 141 | + item['tags']['nist'] = nist_tag( parse_identifiers( vulnerability, 'CWE') ) |
| 142 | + item['tags']['cweid'] = parse_identifiers( vulnerability, 'CWE') |
| 143 | + item['tags']['cveid'] = parse_identifiers( vulnerability, 'CVE') |
| 144 | + item['tags']['ghsaid'] = parse_identifiers( vulnerability, 'GHSA') |
| 145 | + |
| 146 | + controls << item |
| 147 | + end |
| 148 | + controls = collapse_duplicates(controls) |
| 149 | + scaninfo = extract_scaninfo(project) |
| 150 | + results = HeimdallDataFormat.new(profile_name: scaninfo['policy'], |
| 151 | + version: scaninfo['version'], |
| 152 | + title: "Snyk Project: #{scaninfo['projectName']}", |
| 153 | + summary: "Snyk Summary: #{scaninfo['summary']}", |
| 154 | + controls: controls, |
| 155 | + target_id: scaninfo['projectName']) |
| 156 | + project_results[scaninfo['projectName']] = results.to_hdf |
| 157 | + end |
| 158 | + project_results |
| 159 | + end |
| 160 | + end |
| 161 | +end |
0 commit comments