diff --git a/tools/dev/find_release_notes.rb b/tools/dev/find_release_notes.rb new file mode 100644 index 0000000000..fbd073a5ae --- /dev/null +++ b/tools/dev/find_release_notes.rb @@ -0,0 +1,159 @@ +#!/usr/bin/env ruby + +require 'net/http' +require 'nokogiri' +require 'thread' + +module ReleaseNotesFinder + # This finds the release notes information based on either: + # 1. A PR number. In release notes, PR numbers are for bug fixes and notable changes. + # 2. A module short name. For example: ms08_067_netapi + class Client + attr_accessor :release_notes + + RELEASE_NOTES_PAGE = 'https://community.rapid7.com/docs/DOC-2918'.freeze + + def initialize + init_release_notes + @mutex = Mutex.new + end + + def add_release_notes_entry(row) + td = row.search('td') + release_notes_link = td[0] && td[0].at('a') ? td[0].at('a').attributes['href'].value : '' + release_notes_num = td[0] && td[0].at('a') ? td[0].at('a').text.scan(/\d{10}/).flatten.first || '' : '' + highlights = td[1] ? (td[1].search('span') || []).map { |e| e.text } * " " : '' + update_link = td[2] && td[2].at('a') ? td[2].at('a').attributes['href'].value : '' + + @release_notes << { + release_notes_link: release_notes_link, + release_notes_num: release_notes_num, + highlights: highlights, + update_link: update_link, + pull_requests: [], + new_modules: [] + } + end + + def init_release_notes + self.release_notes = [] + + html = send_http_request(RELEASE_NOTES_PAGE) + table_rows_pattern = 'div[@id="jive-body-main"]//div//section//div//div[@class="j-rte-table"]//table//tbody//tr' + rows = html.search(table_rows_pattern) + rows.each do |row| + add_release_notes_entry(row) + end + end + + def update_pr_list(n, text) + pr_num, desc = text.scan(/#(\d+).\x20*(.+)/).flatten + return unless pr_num + n[:pull_requests] << { id: pr_num, description: desc } + end + + def update_module_list(n, li) + li.search('a').each do |a| + next if a.attributes['href'].nil? + n[:new_modules] << { link: a.attributes['href'].value } + end + end + + def update_release_notes_entry(n) + html = send_http_request(n[:release_notes_link]) + pattern = '//div[@class="jive-rendered-content"]//ul//li' + html.search(pattern).each do |li| + @mutex.synchronize do + update_pr_list(n, li.text) + update_module_list(n, li) + end + end + end + + def get_release_notes(input) + release_notes.each do |n| + if n[:pull_requests].empty? + update_release_notes_entry(n) + end + + input_type = guess_input_type(input) + + case input_type + when :pr + m = get_release_notes_from_pr(n, input) + when :module_name + m = get_release_notes_from_module_name(n, input) + end + + return m if m + end + + nil + end + + def guess_input_type(input) + input =~ /^\d+/ ? :pr : :module_name + end + + def get_release_notes_from_module_name(n, input) + n[:new_modules].each do |m| + return n if m[:link] && m[:link].include?(input) + end + + nil + end + + def get_release_notes_from_pr(n, pr) + n[:pull_requests].each do |p| + return n if p[:id] && pr == p[:id] + end + + nil + end + + def send_http_request(uri) + url = URI.parse(uri) + cli = Net::HTTP.new(url.host, url.port) + cli.use_ssl = true + req = Net::HTTP::Get.new(url.request_uri) + res = cli.request(req) + Nokogiri::HTML(res.body) + end + end +end + +def main + inputs = [] + + ARGV.length.times { inputs << ARGV.shift } + puts "[*] Enumerating release notes..." + cli = ReleaseNotesFinder::Client.new + puts "[*] Finding release notes for items: #{inputs * ', '}" + threads = [] + begin + inputs.each do |input| + t = Thread.new do + n = cli.get_release_notes(input) + puts "\n" + + if n + puts "[*] Found release notes for: #{input}" + puts "Release Notes Number: #{n[:release_notes_num]}" + puts "Release Notes Link: #{n[:release_notes_link] || 'N/A'}" + puts "Update Link: #{n[:update_link] || 'N/A'}" + puts "Highlights:\n#{n[:highlights]}" + else + puts "[*] Unable to find release notes for: #{input}" + end + end + threads << t + end + threads.each { |t| t.join } + ensure + threads.each { |t| t.kill } + end +end + +if __FILE__ == $PROGRAM_NAME + main +end