diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..62a4eb5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,9 @@ +### Description + + +### Reason/Reference + \ No newline at end of file diff --git a/.github/workflows/standardrb.yml b/.github/workflows/standardrb.yml index d5d61c5..1ff8175 100644 --- a/.github/workflows/standardrb.yml +++ b/.github/workflows/standardrb.yml @@ -4,11 +4,6 @@ name: Standardrb # Controls when the action will run. on: - push: - paths-ignore: - - '.github/**' - - 'nginx/**' - branches: [main] pull_request: types: [opened, reopened, synchronize] merge_group: @@ -29,9 +24,9 @@ jobs: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 - # Disabled for now because of an issue installing ruby-debug-ide - # with: - # bundler-cache: true # runs 'bundle install' and caches installed gems automatically + with: + ruby-version: '3.3' # Not needed with a .ruby-version file + bundler-cache: true # runs 'bundle install' and caches installed gems automatically - run: | bundle install bundle exec standardrb diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..434527e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +# This is a basic workflow to help you get started with Actions + +name: Tests + +# Controls when the action will run. +on: + pull_request: + types: [opened, reopened, synchronize] + merge_group: + types: [checks_requested] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + env: + BUNDLE_DEPLOYMENT: false + BUNDLE_FROZEN: false + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' # Not needed with a .ruby-version file + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - run: | + bundle install + bundle exec rake test diff --git a/Gemfile.lock b/Gemfile.lock index 3b62534..0386c9c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - ghx (0.0.4) + ghx (0.2.1) faraday (~> 2.9.0) faraday-retry (~> 2.2.1) octokit diff --git a/Rakefile b/Rakefile index 2480d0e..cd4eb8e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,2 +1,6 @@ require "bundler" Bundler::GemHelper.install_tasks + +task :test do + Dir.glob("./test/*_test.rb").each { |file| require file } +end diff --git a/lib/ghx.rb b/lib/ghx.rb index 15ed9df..cf533ba 100644 --- a/lib/ghx.rb +++ b/lib/ghx.rb @@ -25,34 +25,52 @@ def symbolize_keys! # Extra classes to support more OO interfaces to the GitHub API. Wraps both the REST API and GraphQL API. Currently # incomplete. Functionality has been built for our existing use-cases, but nothing else. module GHX + # Defaults to $stdout + # @return [Logger] def self.logger @logger ||= Logger.new($stdout) end + # @param logger [Logger] def self.logger=(logger) @logger = logger end + # Internal octokit client. + # API Key defaults to ENV["GITHUB_TOKEN"] + # @return [Octokit::Client] def self.octokit @octokit ||= Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) end + # @param octokit [Octokit::Client] + # @return [Octokit::Client] def self.octokit=(octokit) @octokit = octokit end + # Internal graphql client. + # API Key defaults to ENV["GITHUB_TOKEN"] + # @return [GHX::GraphqlClient] def self.graphql @graphql ||= GHX::GraphqlClient.new(ENV["GITHUB_GRAPHQL_TOKEN"]) end + # @param graphql [GHX::GraphqlClient] + # @return [GHX::GraphqlClient] def self.graphql=(graphql) @graphql = graphql end + # Internal graphql client. + # API Key defaults to ENV["GITHUB_TOKEN"] + # @return [GHX::RestClient] def self.rest_client @rest_client ||= GHX::RestClient.new(ENV["GITHUB_TOKEN"]) end + # @param rest_client [GHX::RestClient] + # @return [GHX::RestClient] def self.rest_client=(rest_client) @rest_client = rest_client end diff --git a/lib/ghx/dependabot.rb b/lib/ghx/dependabot.rb index eba3708..81447c6 100644 --- a/lib/ghx/dependabot.rb +++ b/lib/ghx/dependabot.rb @@ -1,6 +1,15 @@ module GHX + # Module for Dependabot-related classes and methods module Dependabot + # Get Dependabot alerts for a given repository + # + # @note ONLY RETURNS THE FIRST 100 ALERTS + # @param owner [String] the owner of the repository + # @param repo [String] the repository name + # @return [Array] the alerts def self.get_alerts(owner:, repo:) + # TODO: Add pagination to get all alerts in one go + GHX.rest_client.get("repos/#{owner}/#{repo}/dependabot/alerts?state=open&per_page=100").map do |alert| GHX::Dependabot::Alert.new(alert) end diff --git a/lib/ghx/dependabot/alert.rb b/lib/ghx/dependabot/alert.rb index 7df3572..caa5c69 100644 --- a/lib/ghx/dependabot/alert.rb +++ b/lib/ghx/dependabot/alert.rb @@ -115,9 +115,11 @@ module GHX module Dependabot + # A Dependabot Alert class Alert attr_reader :number, :state, :dependency, :security_advisory, :security_vulnerability, :url, :html_url, :created_at, :updated_at + # @param json_data [Hash] The JSON data for the alert, from the API def initialize(json_data) @number = json_data["number"] @state = json_data["state"] diff --git a/lib/ghx/dependabot/package.rb b/lib/ghx/dependabot/package.rb index a4f6368..ca2cd08 100644 --- a/lib/ghx/dependabot/package.rb +++ b/lib/ghx/dependabot/package.rb @@ -1,5 +1,6 @@ module GHX module Dependabot + # A package is a dependency that is managed by a package manager. Referenced by a SecurityVulnerability. class Package attr_reader :ecosystem, :name diff --git a/lib/ghx/dependabot/security_vulnerability.rb b/lib/ghx/dependabot/security_vulnerability.rb index e59df6d..60464c7 100644 --- a/lib/ghx/dependabot/security_vulnerability.rb +++ b/lib/ghx/dependabot/security_vulnerability.rb @@ -1,5 +1,6 @@ module GHX module Dependabot + # A SecurityVulnerability is referenced by an Alert class SecurityVulnerability attr_reader :package, :severity, :vulnerable_version_range, :first_patched_version diff --git a/lib/ghx/graphql_client.rb b/lib/ghx/graphql_client.rb index de80c62..399f93f 100644 --- a/lib/ghx/graphql_client.rb +++ b/lib/ghx/graphql_client.rb @@ -1,9 +1,14 @@ module GHX + # Internal class to interact with the GitHub GraphQL API class GraphqlClient + # @param api_key [String] def initialize(api_key) @api_key = api_key end + # Perform a GraphQL Query and return the result + # @param query [String] GraphQL Query + # @return [Net::HTTPResponse] def query(query) uri = URI("https://api.github.com/graphql") req = Net::HTTP::Post.new(uri) @@ -15,54 +20,5 @@ def query(query) http.request(req) end end - - # @param [String] project_id - # @param [GithubProjectItem] project_item - # @param [DateTime] reported_at - def update_project_item_reported_at(project_item_id:, reported_at:, project_id: GithubProject::MAIN_GH_PROJECT_ID) - field_id = "PVTF_lADOALH_aM4Ac-_zzgSzAZs" # project_item.field_map["Reported At"] - - gql_query = <<~GQL - mutation { - updateProjectV2ItemFieldValue(input: { - fieldId: "#{field_id}", - itemId: "#{project_item_id}", - projectId: "#{project_id}", - value: { - date: "#{reported_at.to_date}" - } - }) { - projectV2Item { - id - } - } - } - GQL - - query(gql_query) - end - - def update_project_item_reported_by(project_item_id:, reported_by:, project_id: GithubProject::MAIN_GH_PROJECT_ID) - field_id = "PVTF_lADOALH_aM4Ac-_zzgSzBcc" # project_item.field_map["Reporter"] - - gql_query = <<~GQL - mutation { - updateProjectV2ItemFieldValue(input: { - fieldId: "#{field_id}", - itemId: "#{project_item_id}", - projectId: "#{project_id}", - value: { - text: "#{reported_by}" - } - }) { - projectV2Item { - id - } - } - } - GQL - - query(gql_query) - end end end diff --git a/lib/ghx/issue.rb b/lib/ghx/issue.rb index 5647be9..328703d 100644 --- a/lib/ghx/issue.rb +++ b/lib/ghx/issue.rb @@ -1,7 +1,13 @@ module GHX + # A GitHub Issue class Issue attr_accessor :owner, :repo, :number, :title, :body, :state, :state_reason, :author, :assignees, :labels, :milestone, :created_at, :updated_at, :closed_at + # Search for issues in a repository + # @param owner [String] the owner of the repository + # @param repo [String] the repository name + # @param query [String] the search query, using GitHub's search syntax + # @return [Array] the issues found def self.search(owner:, repo:, query:) data = GHX.rest_client.get("search/issues?q=#{URI.encode_www_form_component(query)}+is:issue+repo:#{owner}/#{repo}") data.fetch("items").to_a.map do |issue_data| @@ -13,17 +19,31 @@ def self.search(owner:, repo:, query:) [] end + # Find an issue by its number + # @param owner [String] the owner of the repository + # @param repo [String] the repository name + # @param number [Integer] the issue number + # @return [Issue] the issue found def self.find(owner:, repo:, number:) response_data = GHX.rest_client.get("repos/#{owner}/#{repo}/issues/#{number}") new(owner: owner, repo: repo, **response_data) end + # @param owner [String] the owner of the repository + # @param repo [String] the repository name + # @param **args [Hash] the attributes of the issue you wish to assign + # @return [Issue] the new issue def initialize(owner:, repo:, **args) @owner = owner @repo = repo update_attributes(args) end + # Save the issue to GitHub. Handles both creating and updating. + # + # If the issue has a number, it will be updated. Otherwise, it will be created. + # + # @return [Issue] the saved issue def save @number.nil? ? create : update end diff --git a/lib/ghx/project_item.rb b/lib/ghx/project_item.rb index 4da1a14..0e8e75d 100644 --- a/lib/ghx/project_item.rb +++ b/lib/ghx/project_item.rb @@ -1,7 +1,14 @@ module GHX + # A GitHub Project Item. This is a single item in a GitHub Project Board. + # + # ProjectsV2 are only available via the GraphQL API. This class wraps access to the API and provides a more OO interface. + # + # @note Access to ProjectItems should be done largely (if not always) through the Project class. class ProjectItem attr_accessor :id, :project_id, :issue_number, :issue_title, :issue_url, :issue_state, :field_values, :field_map + # @param field_configuration [Array] An array of field configurations. These are the fields that are available to the Project Item. These are provided via the Project itself. It's much easier to access ProjectItems through the project because of this. + # @param data [Hash] The data from the GraphQL API. def initialize(field_configuration:, data:) _setup_field_configuration(field_configuration) @@ -33,7 +40,11 @@ def initialize(field_configuration:, data:) end end - # Updates the given fields to the given values. Makes a GraphQL call per field to do the update. + # Updates the given fields to the given values. Makes a GraphQL call *per field* to do the update. + # + # The implementation wraps access to the various types for each field. Since GraphQL requires us to type match on all + # requests, this gives us convenience, especially for things like a select field. We can pass in the value, and the + # method will find the ID of the option and update the field for us. # # @param fields [Hash] A hash of field names to values. def update(**fields) @@ -138,23 +149,27 @@ def update_single_select_field(field_id, value) private + # Parse the field_configuration and set up the instance variables and accessors. + # + # Example field_configuration: + # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCno", :name=>"Title", :data_type=>"TITLE", :options=>nil} + # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCns", :name=>"Assignees", :data_type=>"ASSIGNEES", :options=>nil} + # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzAZs", :name=>"Reported At", :data_type=>"DATE", :options=>nil} + # {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgSxCnw", :name=>"Status", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"f971fb55", :name=>"To triage"}, {:id=>"856cdede", :name=>"Ready to Assign"}, {:id=>"f75ad846", :name=>"Assigned"}, {:id=>"47fc9ee4", :name=>"Fix In progress"}, {:id=>"5ef0dc97", :name=>"Additional Info Requested"}, {:id=>"98236657", :name=>"Done - Fixed"}, {:id=>"98aea6ad", :name=>"Done - Won't Fix"}, {:id=>"a3b4fc3a", :name=>"Duplicate"}, {:id=>"81377549", :name=>"Not a Vulnerability"}]} + # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn0", :name=>"Labels", :data_type=>"LABELS", :options=>nil} + # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn4", :name=>"Linked pull requests", :data_type=>"LINKED_PULL_REQUESTS", :options=>nil} + # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn8", :name=>"Milestone", :data_type=>"MILESTONE", :options=>nil} + # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCoA", :name=>"Repository", :data_type=>"REPOSITORY", :options=>nil} + # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCoM", :name=>"Reviewers", :data_type=>"REVIEWERS", :options=>nil} + # {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgSxCuA", :name=>"Severity", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"79628723", :name=>"Informational"}, {:id=>"153889c6", :name=>"Low"}, {:id=>"093709ee", :name=>"Medium"}, {:id=>"5a00bbe7", :name=>"High"}, {:id=>"00e0bbaf", :name=>"Critical"}, {:id=>"fd986bd9", :name=>"Duplicate"}]} + # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzBcc", :name=>"Reporter", :data_type=>"TEXT", :options=>nil} + # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzBho", :name=>"Resolve By", :data_type=>"DATE", :options=>nil} + # {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgTKjOw", :name=>"Payout Status", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"53c47c02", :name=>"Ready for Invoice"}, {:id=>"0b8a4629", :name=>"Payout in Process"}, {:id=>"5f356a58", :name=>"Payout Complete"}, {:id=>"368048ac", :name=>"Ineligible for Payout"}]} + # + # def _setup_field_configuration(field_configuration) @field_configuration = field_configuration.map { |fc| fc.merge({normalized_name: normalized_field_value_name(fc[:name])}) } - # Example field_configuration: - # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCno", :name=>"Title", :data_type=>"TITLE", :options=>nil} - # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCns", :name=>"Assignees", :data_type=>"ASSIGNEES", :options=>nil} - # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzAZs", :name=>"Reported At", :data_type=>"DATE", :options=>nil} - # {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgSxCnw", :name=>"Status", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"f971fb55", :name=>"To triage"}, {:id=>"856cdede", :name=>"Ready to Assign"}, {:id=>"f75ad846", :name=>"Assigned"}, {:id=>"47fc9ee4", :name=>"Fix In progress"}, {:id=>"5ef0dc97", :name=>"Additional Info Requested"}, {:id=>"98236657", :name=>"Done - Fixed"}, {:id=>"98aea6ad", :name=>"Done - Won't Fix"}, {:id=>"a3b4fc3a", :name=>"Duplicate"}, {:id=>"81377549", :name=>"Not a Vulnerability"}]} - # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn0", :name=>"Labels", :data_type=>"LABELS", :options=>nil} - # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn4", :name=>"Linked pull requests", :data_type=>"LINKED_PULL_REQUESTS", :options=>nil} - # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCn8", :name=>"Milestone", :data_type=>"MILESTONE", :options=>nil} - # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCoA", :name=>"Repository", :data_type=>"REPOSITORY", :options=>nil} - # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSxCoM", :name=>"Reviewers", :data_type=>"REVIEWERS", :options=>nil} - # {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgSxCuA", :name=>"Severity", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"79628723", :name=>"Informational"}, {:id=>"153889c6", :name=>"Low"}, {:id=>"093709ee", :name=>"Medium"}, {:id=>"5a00bbe7", :name=>"High"}, {:id=>"00e0bbaf", :name=>"Critical"}, {:id=>"fd986bd9", :name=>"Duplicate"}]} - # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzBcc", :name=>"Reporter", :data_type=>"TEXT", :options=>nil} - # {:id=>"PVTF_lADOALH_aM4Ac-_zzgSzBho", :name=>"Resolve By", :data_type=>"DATE", :options=>nil} - # {:id=>"PVTSSF_lADOALH_aM4Ac-_zzgTKjOw", :name=>"Payout Status", :data_type=>"SINGLE_SELECT", :options=>[{:id=>"53c47c02", :name=>"Ready for Invoice"}, {:id=>"0b8a4629", :name=>"Payout in Process"}, {:id=>"5f356a58", :name=>"Payout Complete"}, {:id=>"368048ac", :name=>"Ineligible for Payout"}]} @field_configuration.each do |field| next unless field[:name] next if field[:name].to_s.empty? diff --git a/lib/ghx/rest_client.rb b/lib/ghx/rest_client.rb index 694e4c0..b92195a 100644 --- a/lib/ghx/rest_client.rb +++ b/lib/ghx/rest_client.rb @@ -1,14 +1,20 @@ require "net/http" module GHX + # RestClient is a simple wrapper around Net::HTTP to make it easier to make API calls to the GitHub REST API. + # + # This is necessary because not all GitHub API endpoints are covered by Octokit. class RestClient attr_reader :api_key + # @param api_key [String] the GitHub API key def initialize(api_key) @api_key = api_key end - # @return [Hash] the JSON response + # Make a GET request to the given path + # @param path [String] the path to the API endpoint + # @return [Hash] the parsed JSON response def get(path) uri = URI.parse("https://api.github.com/#{path}") request = Net::HTTP::Get.new(uri) @@ -27,7 +33,10 @@ def get(path) JSON.parse(response.body) end - # @return [Hash] the JSON response + # Make a POST request to the given path with the given params + # @param path [String] the path to the API endpoint + # @param params [Hash] the request body + # @return [Hash] the parsed JSON response def post(path, params) uri = URI.parse("https://api.github.com/#{path}") request = Net::HTTP::Post.new(uri) @@ -48,7 +57,10 @@ def post(path, params) JSON.parse(response.body) end - # @return [Hash] the JSON response + # Make a PATCH request to the given path with the given params + # @param path [String] the path to the API endpoint + # @param params [Hash] the request body + # @return [Hash] the parsed JSON response def patch(path, params) uri = URI.parse("https://api.github.com/#{path}") request = Net::HTTP::Patch.new(uri) diff --git a/lib/version.rb b/lib/version.rb index c11cda7..e0902bb 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -1,3 +1,3 @@ module GHX - VERSION = "0.2.0" + VERSION = "0.2.1" end diff --git a/test/core_test.rb b/test/core_test.rb new file mode 100644 index 0000000..df17adb --- /dev/null +++ b/test/core_test.rb @@ -0,0 +1,30 @@ +require "minitest/autorun" +require "ghx" + +class CoreTest < Minitest::Test + # The most basic test to ensure that the code loads + def test_that_code_loads + GHX::Issue.new(owner: "test", repo: "test", title: "test") + GHX::Project.new("asdf1234") + + project_item_data = { + "content" => { + "id" => "asdf1234", + "number" => "1234", + "title" => "test", + "url" => "http://example.com", + "state" => "open" + }, + "project" => { + "id" => "asdf1234" + }, + "fieldValues" => { + "nodes" => [] + } + } + + GHX::ProjectItem.new(field_configuration: [], data: project_item_data) + + assert true + end +end diff --git a/test/dependabot_core_test.rb b/test/dependabot_core_test.rb new file mode 100644 index 0000000..e857a9d --- /dev/null +++ b/test/dependabot_core_test.rb @@ -0,0 +1,127 @@ +require "minitest/autorun" +require "ghx" + +class DependabotCoreTest < Minitest::Test + # The most basic test to ensure that the code loads + def test_that_code_loads + GHX::Dependabot::Alert.new(sample_dependabot_response) + + assert true + end + + def sample_dependabot_response + { + :number => 321, + :state => "open", + :dependency => { + package: { + ecosystem: "npm", + name: "react-pdf" + }, + manifest_path: "yarn.lock", + scope: "runtime" + }, + :security_advisory => { + ghsa_id: "GHSA-87hq-q4gp-9wr4", + cve_id: "CVE-2024-34342", + summary: "react-pdf vulnerable to arbitrary JavaScript execution upon opening a malicious PDF with PDF.js", + description: "### Summary\n\nIf PDF.js is used to load a malicious PDF, and PDF.js is configured with `isEvalSupported` set to `true` (which is the default value), unrestricted attacker-controlled JavaScript will be executed in the context of the hosting domain.\n\n### Patches\n[This patch](https://github.com/wojtekmaj/react-pdf/commit/671e6eaa2e373e404040c13cc6b668fe39839cad) forces `isEvalSupported` to `false`, removing the attack vector.\n\n### Workarounds\nSet `options.isEvalSupported` to `false`, where `options` is `Document` component prop.\n\n### References\n- [GHSA-wgrm-67xf-hhpq](https://github.com/mozilla/pdf.js/security/advisories/GHSA-wgrm-67xf-hhpq)\n- https://github.com/mozilla/pdf.js/pull/18015\n- https://github.com/mozilla/pdf.js/commit/85e64b5c16c9aaef738f421733c12911a441cec6\n- https://bugzilla.mozilla.org/show_bug.cgi?id=1893645", + severity: "high", + identifiers: [ + { + value: "GHSA-87hq-q4gp-9wr4", + type: "GHSA" + }, + { + value: "CVE-2024-34342", + type: "CVE" + } + ], + references: [ + { + url: "https://github.com/mozilla/pdf.js/security/advisories/GHSA-wgrm-67xf-hhpq" + }, + { + url: "https://github.com/wojtekmaj/react-pdf/security/advisories/GHSA-87hq-q4gp-9wr4" + }, + { + url: "https://nvd.nist.gov/vuln/detail/CVE-2024-34342" + }, + { + url: "https://github.com/mozilla/pdf.js/pull/18015" + }, + { + url: "https://github.com/mozilla/pdf.js/commit/85e64b5c16c9aaef738f421733c12911a441cec6" + }, + { + url: "https://github.com/wojtekmaj/react-pdf/commit/208f28dd47fe38c33ce4bac4205b2b0a0bb207fe" + }, + { + url: "https://github.com/wojtekmaj/react-pdf/commit/671e6eaa2e373e404040c13cc6b668fe39839cad" + }, + { + url: "https://github.com/advisories/GHSA-87hq-q4gp-9wr4" + } + ], + published_at: "2024-05-07T16:48:59Z", + updated_at: "2024-05-08T10:10:23Z", + withdrawn_at: nil, + vulnerabilities: [ + { + package: { + ecosystem: "npm", + name: "react-pdf" + }, + severity: "high", + vulnerable_version_range: "< 7.7.3", + first_patched_version: { + identifier: "7.7.3" + } + }, + { + package: { + ecosystem: "npm", + name: "react-pdf" + }, + severity: "high", + vulnerable_version_range: ">= 8.0.0, < 8.0.2", + first_patched_version: { + identifier: "8.0.2" + } + } + ], + cvss: { + vector_string: "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:L", + score: 7.1 + }, + cwes: [ + { + cwe_id: "CWE-79", + name: "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')" + } + ] + }, + "security_vulnerability" => { + "package" => { + ecosystem: "npm", + name: "react-pdf" + }, + :severity => "high", + :vulnerable_version_range => "< 7.7.3", + :first_patched_version => { + identifier: "7.7.3" + } + }, + :url => "https://api.github.com/repos/CompanyCam/Company-Cam-API/dependabot/alerts/321", + :html_url => "https://github.com/CompanyCam/Company-Cam-API/security/dependabot/321", + :created_at => "2024-05-07T16:54:48Z", + :updated_at => "2024-05-07T16:54:48Z", + :dismissed_at => nil, + :dismissed_by => nil, + :dismissed_reason => nil, + :dismissed_comment => nil, + :fixed_at => nil, + :auto_dismissed_at => nil + } + end +end