diff --git a/lib/hackerone/client.rb b/lib/hackerone/client.rb index 444e607..e0e41b4 100644 --- a/lib/hackerone/client.rb +++ b/lib/hackerone/client.rb @@ -4,10 +4,13 @@ require "json" require "active_support" require "active_support/core_ext/numeric/time" +require "ostruct" require_relative "client/version" require_relative "client/report" require_relative "client/activity" require_relative "client/program" +require_relative "client/organization" +require_relative "client/asset" require_relative "client/reporter" require_relative "client/member" require_relative "client/user" diff --git a/lib/hackerone/client/asset.rb b/lib/hackerone/client/asset.rb new file mode 100644 index 0000000..966ba17 --- /dev/null +++ b/lib/hackerone/client/asset.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module HackerOne + module Client + class Asset + include ResourceHelper + + DELEGATES = [ + :asset_type, + :identifier, + :description, + :coverage, + :max_severity, + :confidentiality_requirement, + :integrity_requirement, + :availability_requirement, + :created_at, + :updated_at, + :archived_at, + :reference, + :state, + ] + + delegate *DELEGATES, to: :attributes + + attr_reader :organization + + def initialize(asset, organization) + @asset = asset + @organization = organization + end + + def id + @asset[:id] + end + + def update(attributes:) + body = { + type: "asset", + attributes: attributes + } + make_put_request("organizations/#{organization.id}/assets/#{id}", request_body: body) + end + + def programs + relationships.programs[:data].map { |p| Program.new(p) } + end + + private + + def relationships + OpenStruct.new(@asset[:relationships]) + end + + def attributes + OpenStruct.new(@asset[:attributes]) + end + end + end +end diff --git a/lib/hackerone/client/organization.rb b/lib/hackerone/client/organization.rb new file mode 100644 index 0000000..53d63c3 --- /dev/null +++ b/lib/hackerone/client/organization.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module HackerOne + module Client + class Organization + include ResourceHelper + + delegate :handle, :created_at, :updated_at, to: :attributes + + def initialize(org) + @organization = org + end + + def id + @organization[:id] + end + + def assets(page_number: 1, page_size: 100) + make_get_request( + "organizations/#{id}/assets", + params: { page: { number: page_number, size: page_size } } + ).map do |asset_data| + Asset.new(asset_data, self) + end + end + + private + + def attributes + OpenStruct.new(@organization[:attributes]) + end + end + end +end diff --git a/lib/hackerone/client/program.rb b/lib/hackerone/client/program.rb index ec8f3df..eb678ee 100644 --- a/lib/hackerone/client/program.rb +++ b/lib/hackerone/client/program.rb @@ -51,6 +51,15 @@ def find_group(groupname) groups.find { |group| group.name == groupname } end + def structured_scopes(page_number: 1, page_size: 100) + make_get_request( + "programs/#{id}/structured_scopes", + params: { page: { number: page_number, size: page_size } } + ).map do |data| + StructuredScope.new(data, self) + end + end + def update_policy(policy:) body = { type: "program-policy", @@ -83,8 +92,6 @@ def balance BillingBalance.new(response_body).balance end - private - def members @members ||= relationships.members[:data].map { |member_data| Member.new(member_data) } end @@ -93,6 +100,12 @@ def groups @groups ||= relationships.groups[:data].map { |group_data| Group.new(group_data) } end + def organization + @organization ||= Organization.new(relationships.organization[:data]) + end + + private + def relationships # Relationships are only included in the /programs/:id call, # which is why we need to do a separate call here. diff --git a/lib/hackerone/client/report.rb b/lib/hackerone/client/report.rb index 391fce9..57c0e58 100644 --- a/lib/hackerone/client/report.rb +++ b/lib/hackerone/client/report.rb @@ -108,7 +108,7 @@ def payment_total end def structured_scope - StructuredScope.new(relationships[:structured_scope].fetch(:data, {})) + StructuredScope.new(relationships[:structured_scope].fetch(:data, {}), program) end # Excludes reports where the payout amount is 0 indicating swag-only or no diff --git a/lib/hackerone/client/structured_scope.rb b/lib/hackerone/client/structured_scope.rb index c51ffda..6556b34 100644 --- a/lib/hackerone/client/structured_scope.rb +++ b/lib/hackerone/client/structured_scope.rb @@ -3,17 +3,27 @@ module HackerOne module Client class StructuredScope + include ResourceHelper + DELEGATES = [ :asset_identifier, :asset_type, + :availability_requirement, + :confidentiality_requirement, :eligible_for_bounty, :eligible_for_submission, - :instruction + :instruction, + :integrity_requirement, + :max_severity, + :reference ] delegate *DELEGATES, to: :attributes - def initialize(scope) + attr_reader :program + + def initialize(scope, program = nil) + @program = program @scope = scope end @@ -21,6 +31,14 @@ def id @scope[:id] end + def update(attributes:) + body = { + type: "structured-scope", + attributes: attributes + } + make_put_request("programs/#{program.id}/structured_scopes/#{id}", request_body: body) + end + private def attributes diff --git a/spec/hackerone/client/asset_spec.rb b/spec/hackerone/client/asset_spec.rb new file mode 100644 index 0000000..32ca878 --- /dev/null +++ b/spec/hackerone/client/asset_spec.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe HackerOne::Client::Asset do + before(:all) do + ENV["HACKERONE_TOKEN_NAME"] = "foo" + ENV["HACKERONE_TOKEN"] = "bar" + end + before(:each) do + stub_request(:get, "https://api.hackerone.com/v1/programs/18969"). + to_return(body: <<~JSON) +{ + "data": { + "id": "18969", + "type": "program", + "attributes": { + "handle": "github", + "created_at": "2016-02-02T04:05:06.000Z", + "updated_at": "2016-02-02T04:05:06.000Z" + }, + "relationships": { + "organization": { + "data": { + "id": "14", + "type": "organization", + "attributes": { + "handle": "api-example", + "created_at": "2016-02-02T04:05:06.000Z", + "updated_at": "2016-02-02T04:05:06.000Z" + } + } + } + } + } +} + JSON + + stub_request(:get, "https://api.hackerone.com/v1/organizations/14/assets?page%5Bnumber%5D=1&page%5Bsize%5D=100"). + to_return(body: <<~JSON2) +{ + "data": [ + { + "id": "2", + "type": "asset", + "attributes": { + "asset_type": "domain", + "domain_name": "hackerone.com", + "description": null, + "coverage": "untested", + "max_severity": "critical", + "confidentiality_requirement": "high", + "integrity_requirement": "high", + "availability_requirement": "high", + "created_at": "2016-02-02T04:05:06.000Z", + "updated_at": "2016-02-02T04:05:06.000Z", + "archived_at": "2017-02-02T04:05:06.000Z", + "reference": "reference", + "state": "confirmed" + }, + "relationships": { + "asset_tags": { + "data": [ + { + "id": "1", + "type": "asset-tag", + "attributes": { + "name": "test" + }, + "relationships": { + "asset_tag_category": { + "data": { + "id": "2", + "type": "asset-tag-category", + "attributes": { + "name": "test" + } + } + } + } + } + ] + }, + "programs": { + "data": [ + { + "id": "18969", + "type": "program", + "attributes": { + "handle": "github", + "name": "team name" + } + } + ] + }, + "attachments": { + "data": [ + { + "id": "1337", + "type": "attachment", + "attributes": { + "expiring_url": "https://attachments.s3.amazonaws.com/G74PuDP6qdEdN2rpKNLkVwZF", + "created_at": "2016-02-02T04:05:06.000Z", + "file_name": "example.png", + "content_type": "image/png", + "file_size": 16115 + } + } + ] + } + } + } + ], + "links": {} +} + JSON2 + end + + after(:each) do + # clear cached programs to prevent contaminatin between tests + HackerOne::Client::Program.instance_variable_set(:@my_programs, nil) + end + + let(:program) do + VCR.use_cassette(:programs) do + HackerOne::Client::Program.find("github") + end + end + + let(:organization) do + program.organization + end + + let(:assets) do + organization.assets + end + + let(:asset) { assets[0] } + + it "returns a collection" do + expect(assets).to be_kind_of(Array) + expect(assets.size).to eq(1) + end + + it "returns id" do + expect(asset.id).to be_present + expect(asset.id).to eq("2") + end + + it "returns organization" do + expect(asset.organization).to be_present + expect(asset.organization.id).to eq("14") + end + + it "returns programs" do + expect(asset.programs).to be_kind_of(Array) + expect(asset.programs.first.id).to eq("18969") + end + + it "updates the asset" do + req = stub_request(:put, "https://api.hackerone.com/v1/organizations/14/assets/2"). + with { |r| + r.body == <<~BODY.strip + {"data":{"type":"asset","attributes":{"description":"This is the new description"}}} + BODY + }. + to_return(body: "{}") # we are not using the response for now so not bothering to stub it properly + + asset.update( + attributes: { + description: "This is the new description" + } + ) + + expect(req).to have_been_requested + end +end diff --git a/spec/hackerone/client/program_spec.rb b/spec/hackerone/client/program_spec.rb index 21fff5e..529e19f 100644 --- a/spec/hackerone/client/program_spec.rb +++ b/spec/hackerone/client/program_spec.rb @@ -51,6 +51,39 @@ end end + describe "structured_scopes" do + it "returns a list of structured scopes" do + stub_request(:get, "https://api.hackerone.com/v1/programs/18969/structured_scopes?page%5Bnumber%5D=1&page%5Bsize%5D=100"). + to_return(body: <<~JSON) +{ + "data": [ + { + "id": "57", + "type": "structured-scope", + "attributes": { + "asset_identifier": "api.example.com", + "asset_type": "URL", + "confidentiality_requirement": "high", + "integrity_requirement": "high", + "availability_requirement": "high", + "max_severity": "critical", + "created_at": "2015-02-02T04:05:06.000Z", + "updated_at": "2016-05-02T04:05:06.000Z", + "instruction": null, + "eligible_for_bounty": true, + "eligible_for_submission": true, + "reference": "H001001" + } + } + ], + "links": {} +} + JSON + expect(program.structured_scopes).to be_a(Array) + expect(program.structured_scopes.first).to be_a(HackerOne::Client::StructuredScope) + end + end + describe ".incremental_activities" do it "can traverse through the activities of a program" do incremental_activities = program.incremental_activities(updated_at_after: DateTime.new(2017, 12, 4, 15, 38), page_size: 3) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index baf3328..5be3eb4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,6 +4,7 @@ require "hackerone/client" require "pry" require "vcr" +require "webmock/rspec" RSpec.configure do |config| config.example_status_persistence_file_path = ".rspec_status"