diff --git a/lib/http.rb b/lib/http.rb index 0e934255..c9c8c6a3 100644 --- a/lib/http.rb +++ b/lib/http.rb @@ -11,7 +11,30 @@ module Http Faraday::ConnectionFailed, Faraday::ClientError, Net::OpenTimeout, Errno::ECONNREFUSED, EOFError, Faraday::ServerError ] - def connection(verify_ssl = true, max_retries = 5, hmac_client: nil) + def connection(verify_ssl = true, max_retries = 5, hmac_client: nil, retry_options: nil) + retry_config = { + max: max_retries, + interval: 0.1, + max_interval: 30, + backoff_factor: 5, + methods: %i[get post], + exceptions: RETRY_EXCEPTIONS, + retry_statuses: [429, 500, 502, 503, 504], + retry_block: method(:log_retry), + exhausted_retries_block: method(:log_retries_exhausted) + } + + if retry_options + # Merge retry_options while preserving default exceptions and statuses (union, don't replace) + retry_config.merge!(retry_options) do |key, old_value, new_value| + if %i[exceptions retry_statuses].include?(key) + (old_value + new_value).uniq # Union and remove duplicates + else + new_value # For other keys, new value takes precedence + end + end + end + Faraday.new do |faraday| faraday.request :multipart faraday.request :json @@ -19,17 +42,7 @@ def connection(verify_ssl = true, max_retries = 5, hmac_client: nil) if @options && @options[:debug] == true faraday.response :logger # This logs to STDOUT by default end - faraday.request :retry, { - max: max_retries, - interval: 0.1, - max_interval: 30, - backoff_factor: 5, - methods: %i[get post], - exceptions: RETRY_EXCEPTIONS, - retry_statuses: [429, 500, 502, 503, 504], - retry_block: method(:log_retry), - exhausted_retries_block: method(:log_retries_exhausted) - } + faraday.request :retry, retry_config if hmac_client require_relative './faraday_middlewares/faraday_hmac_middleware' faraday.use FaradayHmac, hmac_client @@ -40,20 +53,20 @@ def connection(verify_ssl = true, max_retries = 5, hmac_client: nil) end end - def http_get(url, headers, max_retries = 5, verify_ssl = true, hmac_client: nil) - connection(verify_ssl, max_retries, hmac_client:).run_request(:get, url, nil, headers) + def http_get(url, headers, max_retries = 5, verify_ssl = true, hmac_client: nil, retry_options: nil) + connection(verify_ssl, max_retries, hmac_client:, retry_options:).run_request(:get, url, nil, headers) end - def http_post(url, headers, payload, max_retries = 5, verify_ssl = true, hmac_client: nil) - connection(verify_ssl, max_retries, hmac_client:).run_request(:post, url, payload, headers) + def http_post(url, headers, payload, max_retries = 5, verify_ssl = true, hmac_client: nil, retry_options: nil) + connection(verify_ssl, max_retries, hmac_client:, retry_options:).run_request(:post, url, payload, headers) end - def http_put(url, headers, payload, max_retries = 5, verify_ssl = true) - connection(verify_ssl, max_retries).run_request(:put, url, payload, headers) + def http_put(url, headers, payload, max_retries = 5, verify_ssl = true, retry_options: nil) + connection(verify_ssl, max_retries, retry_options:).run_request(:put, url, payload, headers) end - def http_delete(url, headers, max_retries = 5, verify_ssl = true) - connection(verify_ssl, max_retries).run_request(:delete, url, nil, headers) + def http_delete(url, headers, max_retries = 5, verify_ssl = true, retry_options: nil) + connection(verify_ssl, max_retries, retry_options:).run_request(:delete, url, nil, headers) end def log_retry(retry_count:, exception:, will_retry_in:, **_kwargs) diff --git a/spec/lib/http_spec.rb b/spec/lib/http_spec.rb new file mode 100644 index 00000000..ce77db0e --- /dev/null +++ b/spec/lib/http_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require "rspec_helper" +require_relative "../../lib/http" + +class TestHelper + include Kenna::Toolkit::Helpers::Http + + attr_reader :options + + def initialize(options = {}) + @options = options + end +end + +RSpec.describe Kenna::Toolkit::Helpers::Http do + subject(:helper) { TestHelper.new } + + describe "#connection" do + context "without retry_options" do + it "uses default retry configuration" do + conn = helper.connection + # Verify connection is created without errors + expect(conn).to be_a(Faraday::Connection) + end + end + + context "with custom retry_options" do + it "merges retry_statuses instead of replacing them" do + custom_options = { + retry_statuses: [504] + } + conn = helper.connection(true, 5, retry_options: custom_options) + expect(conn).to be_a(Faraday::Connection) + end + + it "preserves default exceptions while adding custom ones" do + custom_options = { + exceptions: [Faraday::TooManyRequestsError] + } + conn = helper.connection(true, 5, retry_options: custom_options) + expect(conn).to be_a(Faraday::Connection) + end + + it "allows overriding other retry options" do + custom_options = { + interval: 60, + max_interval: 60, + backoff_factor: 2 + } + conn = helper.connection(true, 5, retry_options: custom_options) + expect(conn).to be_a(Faraday::Connection) + end + end + + context "HMAC client support" do + it "creates connection with HMAC middleware when hmac_client is provided" do + mock_client = double("hmac_client") + conn = helper.connection(true, 5, hmac_client: mock_client) + expect(conn).to be_a(Faraday::Connection) + end + end + + context "SSL verification" do + it "disables SSL verification when verify_ssl is false" do + conn = helper.connection(false) + expect(conn).to be_a(Faraday::Connection) + end + end + end + + describe "#http_get" do + it "makes GET requests" do + stub_request(:get, "https://example.com/test") + .to_return(body: "success", status: 200) + + response = helper.http_get("https://example.com/test", {}) + expect(response.status).to eq(200) + expect(response.body).to eq("success") + end + + it "accepts retry_options parameter" do + stub_request(:get, "https://example.com/test") + .to_return(body: "success", status: 200) + + retry_opts = { retry_statuses: [504] } + response = helper.http_get("https://example.com/test", {}, 5, true, retry_options: retry_opts) + expect(response.status).to eq(200) + end + end + + describe "#http_post" do + it "makes POST requests with retry options" do + stub_request(:post, "https://example.com/test") + .to_return(body: "created", status: 201) + + retry_opts = { retry_statuses: [504] } + response = helper.http_post("https://example.com/test", {}, { foo: "bar" }, 5, true, retry_options: retry_opts) + expect(response.status).to eq(201) + expect(response.body).to eq("created") + end + end + + describe "#http_put" do + it "makes PUT requests with retry options" do + stub_request(:put, "https://example.com/test") + .to_return(body: "updated", status: 200) + + retry_opts = { retry_statuses: [504] } + response = helper.http_put("https://example.com/test", {}, { foo: "bar" }, 5, true, retry_options: retry_opts) + expect(response.status).to eq(200) + expect(response.body).to eq("updated") + end + end + + describe "#http_delete" do + it "makes DELETE requests with retry options" do + stub_request(:delete, "https://example.com/test") + .to_return(body: "deleted", status: 200) + + retry_opts = { retry_statuses: [504] } + response = helper.http_delete("https://example.com/test", {}, 5, true, retry_options: retry_opts) + expect(response.status).to eq(200) + expect(response.body).to eq("deleted") + end + end +end diff --git a/spec/tasks/connectors/veracode/veracode_client_spec.rb b/spec/tasks/connectors/veracode/veracode_client_spec.rb new file mode 100644 index 00000000..6a62da05 --- /dev/null +++ b/spec/tasks/connectors/veracode/veracode_client_spec.rb @@ -0,0 +1,253 @@ +# frozen_string_literal: true + +require "rspec_helper" +require_relative "../../../../tasks/connectors/veracode/lib/veracode_client" + +RSpec.describe Kenna::Toolkit::Veracode::Client do + subject(:client) { described_class.new("test_id", "test_key", 100) } + + let(:applications_response) do + { + "_embedded" => { + "applications" => [ + { + "guid" => "test-guid", + "profile" => { + "name" => "Test App", + "tags" => "tag1,tag2", + "business_unit" => { "name" => "Test BU" }, + "business_criticality" => "High", + "business_owners" => [{ "name" => "Test Owner" }], + "custom_fields" => [] + } + } + ] + }, + "_links" => {} + }.to_json + end + + let(:cwe_response) do + { + "_embedded" => { + "cwes" => [ + { + "id" => "CWE-79", + "recommendation" => "Sanitize input" + } + ] + }, + "_links" => {} + }.to_json + end + + let(:category_response) do + { + "_embedded" => { + "categories" => [ + { + "id" => "CAT-1", + "recommendation" => "Fix this" + } + ] + }, + "_links" => {} + }.to_json + end + + let(:findings_response) do + { + "_embedded" => { + "findings" => [] + }, + "_links" => {} + }.to_json + end + + describe "#initialize" do + it "initializes with required parameters" do + expect(client.instance_variable_get(:@id)).to eq("test_id") + expect(client.instance_variable_get(:@key)).to eq("test_key") + expect(client.instance_variable_get(:@page_size)).to eq(100) + end + end + + describe "#applications" do + context "successful response" do + before do + stub_request(:get, %r{https://api\.veracode\.com/appsec/v1/applications}) + .to_return(body: applications_response, status: 200) + end + + it "fetches applications from API" do + apps = client.applications + expect(apps).to be_an(Array) + expect(apps.first["guid"]).to eq("test-guid") + end + + it "includes guid, name, tags, and owner in app list" do + apps = client.applications + expect(apps.first).to have_key("guid") + expect(apps.first).to have_key("name") + expect(apps.first).to have_key("tags") + end + end + + context "with custom field filters" do + before do + stub_request(:get, %r{https://api\.veracode\.com/appsec/v1/applications}) + .to_return(body: applications_response, status: 200) + end + + it "applies custom field filter" do + apps = client.applications("custom_field", "filter_value") + expect(apps).to be_an(Array) + end + end + end + + describe "#cwe_recommendations" do + context "successful response" do + before do + stub_request(:get, %r{https://api\.veracode\.com/appsec/v1/cwes}) + .to_return(body: cwe_response, status: 200) + end + + it "fetches CWE recommendations from API" do + recommendations = client.cwe_recommendations + expect(recommendations).to be_an(Array) + end + + it "includes id and recommendation in results" do + recommendations = client.cwe_recommendations + expect(recommendations.first).to have_key("id") + expect(recommendations.first).to have_key("recommendation") + end + end + + context "when API returns no results" do + before do + stub_request(:get, %r{https://api\.veracode\.com/appsec/v1/cwes}) + .to_return(body: { "_embedded" => { "cwes" => [] }, "_links" => {} }.to_json, status: 200) + end + + it "returns empty array" do + recommendations = client.cwe_recommendations + expect(recommendations).to eq([]) + end + end + end + + describe "#category_recommendations" do + context "successful response" do + before do + stub_request(:get, %r{https://api\.veracode\.com/appsec/v1/categories}) + .to_return(body: category_response, status: 200) + end + + it "fetches category recommendations from API" do + recommendations = client.category_recommendations + expect(recommendations).to be_an(Array) + end + + it "includes id and recommendation in results" do + recommendations = client.category_recommendations + expect(recommendations.first).to have_key("id") + expect(recommendations.first).to have_key("recommendation") + end + end + end + + describe "#process_paged_findings" do + context "successful response" do + before do + stub_request(:get, %r{https://api\.veracode\.com/appsec/v2/applications}) + .to_return(body: findings_response, status: 200) + end + + it "yields findings to the block" do + expect { |b| client.process_paged_findings("test_guid", "STATIC", &b) }.to yield_with_args(Hash) + end + end + end + + describe "#get_paged_results" do + context "successful response with pagination" do + let(:first_page_body) do + { + "_embedded" => { "applications" => [{ "guid" => "app1" }] }, + "_links" => { "next" => { "href" => "https://api.veracode.com/page2" } } + }.to_json + end + + let(:second_page_body) do + { + "_embedded" => { "applications" => [{ "guid" => "app2" }] }, + "_links" => {} + }.to_json + end + + before do + stub_request(:get, "https://api.veracode.com/page1") + .to_return(body: first_page_body, status: 200) + stub_request(:get, "https://api.veracode.com/page2") + .to_return(body: second_page_body, status: 200) + end + + it "handles pagination correctly" do + results = [] + client.get_paged_results("https://api.veracode.com/page1") do |result| + results << result + end + expect(results.length).to eq(2) + end + end + + context "when API request fails" do + before do + allow(client).to receive(:http_get).and_return(nil) + end + + it "raises ApiError when response is nil" do + expect do + client.get_paged_results("https://api.veracode.com/page1") { |_result| } + end.to raise_error(Kenna::Toolkit::Veracode::Client::ApiError) + end + end + end + + describe "#hmac_auth_options" do + it "returns authorization header with HMAC signature" do + result = client.hmac_auth_options("/appsec/v1/applications") + expect(result).to have_key(:Authorization) + expect(result[:Authorization]).to match(/^VERACODE-HMAC-SHA-256/) + end + + it "handles query parameters in path" do + result = client.hmac_auth_options("/appsec/v1/applications?size=100") + expect(result).to have_key(:Authorization) + expect(result[:Authorization]).to match(/^VERACODE-HMAC-SHA-256/) + end + end + + describe "#veracode_signature" do + it "generates valid HMAC signature" do + signature = client.send(:veracode_signature, "/appsec/v1/applications") + expect(signature).to match(/^VERACODE-HMAC-SHA-256 id=test_id,ts=\d+,nonce=[a-f0-9]+,sig=[a-f0-9]+$/) + end + end + + describe "retry options merging" do + it "preserves default retry statuses when custom options are provided" do + allow(client).to receive(:http_get).and_call_original + stub_request(:get, %r{https://api\.veracode\.com/appsec/v1/applications}) + .to_return(body: applications_response, status: 200) + + # Call applications which uses retry options + client.applications + + # Verify http_get was called with retry options + expect(client).to have_received(:http_get) + end + end +end diff --git a/spec/tasks/connectors/veracode_asset_vulns/veracode_av_client_spec.rb b/spec/tasks/connectors/veracode_asset_vulns/veracode_av_client_spec.rb new file mode 100644 index 00000000..424f9aae --- /dev/null +++ b/spec/tasks/connectors/veracode_asset_vulns/veracode_av_client_spec.rb @@ -0,0 +1,356 @@ +# frozen_string_literal: true + +require "rspec_helper" +require_relative "../../../../tasks/connectors/veracode_asset_vulns/lib/veracode_av_client" + +RSpec.describe Kenna::Toolkit::VeracodeAV::Client do + subject(:client) do + described_class.new( + "test_id", + "test_key", + "/tmp", + "test_file.json", + "https://api.kennasecurity.com", + "12345", + "api_key", + "1-10,2-20,3-30,4-40,5-50" + ) + end + + let(:applications_response) do + { + "_embedded" => { + "applications" => [ + { + "guid" => "test-guid", + "profile" => { + "name" => "Test App", + "tags" => "tag1,tag2", + "business_unit" => { "name" => "Test BU" }, + "business_criticality" => "High", + "business_owners" => [{ "name" => "Test Owner" }], + "custom_fields" => [] + } + } + ] + }, + "_links" => {} + }.to_json + end + + let(:cwe_response) do + { + "_embedded" => { + "cwes" => [ + { + "id" => "CWE-79", + "recommendation" => "Sanitize input" + } + ] + }, + "_links" => {} + }.to_json + end + + let(:category_response) do + { + "_embedded" => { + "categories" => [ + { + "id" => "CAT-1", + "recommendation" => "Fix this" + } + ] + }, + "_links" => {} + }.to_json + end + + let(:static_findings_response) do + { + "_embedded" => { + "findings" => [ + { + "scan_type" => "STATIC", + "issue_id" => "issue-static-1", + "description" => "Static finding", + "violates_policy" => true, + "finding_details" => { + "file_name" => "app/models/user.rb", + "severity" => 5, + "finding_category" => { "id" => "CAT-1", "name" => "Category 1", "href" => "/categories/CAT-1" }, + "cwe" => { "id" => "CWE-79", "name" => "XSS", "href" => "/cwes/CWE-79" } + }, + "finding_status" => { + "status" => "OPEN", + "first_found_date" => "2024-01-01T00:00:00Z", + "last_seen_date" => "2024-01-02T00:00:00Z" + } + } + ] + }, + "_links" => {} + }.to_json + end + + let(:sca_findings_response) do + { + "_embedded" => { + "findings" => [ + { + "scan_type" => "SCA", + "description" => "SCA finding", + "violates_policy" => false, + "finding_details" => { + "component_filename" => "rack-2.2.8.gem", + "severity" => 4, + "cve" => { "name" => "CVE-2024-0001", "href" => "/cves/CVE-2024-0001" }, + "cwe" => { "id" => "CWE-89", "name" => "SQL Injection", "href" => "/cwes/CWE-89" } + }, + "finding_status" => { + "status" => "CLOSED", + "first_found_date" => "2024-02-01T00:00:00Z", + "last_seen_date" => "2024-02-02T00:00:00Z" + } + } + ] + }, + "_links" => {} + }.to_json + end + + describe "#initialize" do + it "initializes with required parameters" do + expect(client.instance_variable_get(:@id)).to eq("test_id") + expect(client.instance_variable_get(:@key)).to eq("test_key") + expect(client.instance_variable_get(:@output_dir)).to eq("/tmp") + expect(client.instance_variable_get(:@filename)).to eq("test_file.json") + expect(client.instance_variable_get(:@score_map)).to eq({ "1" => "10", "2" => "20", "3" => "30", "4" => "40", "5" => "50" }) + end + end + + describe "#build_score_map" do + it "builds a score map from comma-separated values" do + score_map = client.build_score_map("1-10,2-20,3-30") + expect(score_map).to eq({ "1" => "10", "2" => "20", "3" => "30" }) + end + + it "raises error for invalid score mapping with non-numeric score" do + expect do + client.build_score_map("1-abc,2-20") + end.to raise_error(SystemExit) + end + + it "raises error for score outside 0-100 range" do + expect do + client.build_score_map("1-101,2-20") + end.to raise_error(SystemExit) + end + end + + describe "#applications" do + context "successful response" do + before do + stub_request(:get, %r{https://api\.veracode\.com/appsec/v1/applications}) + .to_return(body: applications_response, status: 200) + end + + it "fetches applications from API" do + apps = client.applications(100) + expect(apps).to be_an(Array) + expect(apps.first["guid"]).to eq("test-guid") + end + + it "includes tags and owner information" do + apps = client.applications(100) + expect(apps.first["tags"]).to be_an(Array) + expect(apps.first["owner"]).to eq("Test Owner") + end + end + + context "when http_get returns nil" do + before do + allow(client).to receive(:http_get).and_return(nil) + end + + it "returns nil when response is nil" do + apps = client.applications(100) + expect(apps).to be_nil + end + end + + context "with custom field filters" do + before do + stub_request(:get, %r{https://api\.veracode\.com/appsec/v1/applications}) + .to_return(body: applications_response, status: 200) + end + + it "applies custom field filter" do + apps = client.applications(100, "custom_field", "filter_value") + expect(apps).to be_an(Array) + end + end + end + + describe "#cwe_recommendations" do + context "successful response" do + before do + stub_request(:get, %r{https://api\.veracode\.com/appsec/v1/cwes}) + .to_return(body: cwe_response, status: 200) + end + + it "fetches CWE recommendations from API" do + client.cwe_recommendations(100) + cwe_recs = client.instance_variable_get(:@cwe_recommendations) + expect(cwe_recs).to be_an(Array) + expect(cwe_recs.first["id"]).to eq("CWE-79") + end + end + + context "when http_get returns nil" do + before do + allow(client).to receive(:http_get).and_return(nil) + end + + it "returns nil when response is nil" do + result = client.cwe_recommendations(100) + expect(result).to be_nil + end + end + end + + describe "#category_recommendations" do + context "successful response" do + before do + stub_request(:get, %r{https://api\.veracode\.com/appsec/v1/categories}) + .to_return(body: category_response, status: 200) + end + + it "fetches category recommendations from API" do + client.category_recommendations(100) + cat_recs = client.instance_variable_get(:@category_recommendations) + expect(cat_recs).to be_an(Array) + expect(cat_recs.first["id"]).to eq("CAT-1") + end + end + + context "when http_get returns nil" do + before do + allow(client).to receive(:http_get).and_return(nil) + end + + it "returns nil when response is nil" do + result = client.category_recommendations(100) + expect(result).to be_nil + end + end + end + + describe "#hmac_auth_options" do + it "returns authorization header with HMAC signature" do + result = client.hmac_auth_options("/appsec/v1/applications") + expect(result).to have_key(:Authorization) + expect(result[:Authorization]).to match(/^VERACODE-HMAC-SHA-256/) + end + + it "handles query parameters in path" do + result = client.hmac_auth_options("/appsec/v1/applications?size=100") + expect(result).to have_key(:Authorization) + expect(result[:Authorization]).to match(/^VERACODE-HMAC-SHA-256/) + end + end + + describe "#veracode_signature" do + it "generates valid HMAC signature" do + signature = client.send(:veracode_signature, "/appsec/v1/applications") + expect(signature).to match(/^VERACODE-HMAC-SHA-256 id=test_id,ts=\d+,nonce=[a-f0-9]+,sig=[a-f0-9]+$/) + end + end + + describe "pagination handling" do + context "with multiple pages of applications" do + let(:first_page) do + { + "_embedded" => { + "applications" => [{ "guid" => "app1", "profile" => { "name" => "App 1", "tags" => nil, "business_unit" => { "name" => "BU1" }, "business_criticality" => nil, "business_owners" => [], "custom_fields" => [] } }] + }, + "_links" => { "next" => { "href" => "https://api.veracode.com/page2" } } + }.to_json + end + + let(:second_page) do + { + "_embedded" => { + "applications" => [{ "guid" => "app2", "profile" => { "name" => "App 2", "tags" => nil, "business_unit" => { "name" => "BU2" }, "business_criticality" => nil, "business_owners" => [], "custom_fields" => [] } }] + }, + "_links" => {} + }.to_json + end + + before do + stub_request(:get, "https://api.veracode.com/appsec/v1/applications?size=100") + .to_return(body: first_page, status: 200) + stub_request(:get, "https://api.veracode.com/page2") + .to_return(body: second_page, status: 200) + end + + it "handles paginated application results" do + apps = client.applications(100) + expect(apps.length).to eq(2) + expect(apps.map { |a| a["guid"] }).to contain_exactly("app1", "app2") + end + end + end + + describe "retry options" do + it "uses retry options for HTTP requests" do + allow(client).to receive(:http_get).and_call_original + stub_request(:get, %r{https://api\.veracode\.com/appsec/v1/cwes}) + .to_return(body: cwe_response, status: 200) + + client.cwe_recommendations(100) + expect(client).to have_received(:http_get) + end + end + + describe "#get_findings" do + it "uses the retry-enabled http_get call for static findings" do + client.instance_variable_set(:@category_recommendations, [{ "id" => "CAT-1", "recommendation" => "Fix this" }]) + client.instance_variable_set(:@cwe_recommendations, [{ "id" => "CWE-79", "recommendation" => "Sanitize input" }]) + + allow(client).to receive(:http_get).and_return(instance_double("Response", body: static_findings_response)) + allow(client).to receive(:create_kdi_asset_vuln) + allow(client).to receive(:create_kdi_vuln_def) + + client.get_findings("app-guid", "Test App", [], "Test Owner", 100, "STATIC") + + expect(client).to have_received(:http_get).with( + "https://api.veracode.com/appsec/v2/applications/app-guid/findings?size=100&scan_type=STATIC", + {}, + described_class::MAX_RETRIES_504_ONLY, + true, + hmac_client: client, + retry_options: described_class::RETRY_OPTIONS_504_ONLY + ) + end + end + + describe "#get_findings_sca" do + it "uses the retry-enabled http_get call for SCA findings" do + allow(client).to receive(:http_get).and_return(instance_double("Response", body: sca_findings_response)) + allow(client).to receive(:create_kdi_asset_vuln) + allow(client).to receive(:create_kdi_vuln_def) + + client.get_findings_sca("app-guid", "Test App", [], "Test Owner", 100) + + expect(client).to have_received(:http_get).with( + "https://api.veracode.com/appsec/v2/applications/app-guid/findings?size=100&scan_type=SCA", + {}, + described_class::MAX_RETRIES_504_ONLY, + true, + hmac_client: client, + retry_options: described_class::RETRY_OPTIONS_504_ONLY + ) + end + end +end diff --git a/tasks/connectors/veracode/lib/veracode_client.rb b/tasks/connectors/veracode/lib/veracode_client.rb index 36aefa1e..8e166d2d 100644 --- a/tasks/connectors/veracode/lib/veracode_client.rb +++ b/tasks/connectors/veracode/lib/veracode_client.rb @@ -12,6 +12,14 @@ class ApiError < StandardError; end CWE_PATH = "/appsec/v1/cwes" FINDING_PATH = "/appsec/v2/applications" REQUEST_VERSION = "vcode_request_version_1" + RETRY_OPTIONS_504_ONLY = { + interval: 30, + max_interval: 30, + backoff_factor: 1, + methods: %i[get], + retry_statuses: [504] + }.freeze + MAX_RETRIES_504_ONLY = 2 def initialize(id, key, page_size) @id = id @@ -82,7 +90,7 @@ def process_paged_findings(app_guid, scan_type, &) def get_paged_results(url) next_page = url until next_page.nil? - response = http_get(next_page, {}, hmac_client: self) + response = http_get(next_page, {}, MAX_RETRIES_504_ONLY, true, hmac_client: self, retry_options: RETRY_OPTIONS_504_ONLY) raise ApiError, "Unable to retrieve data for #{next_page}. Please, check credentials." unless response result = JSON.parse(response.body) diff --git a/tasks/connectors/veracode_asset_vulns/lib/veracode_av_client.rb b/tasks/connectors/veracode_asset_vulns/lib/veracode_av_client.rb index 4581fe7e..5e3df216 100644 --- a/tasks/connectors/veracode_asset_vulns/lib/veracode_av_client.rb +++ b/tasks/connectors/veracode_asset_vulns/lib/veracode_av_client.rb @@ -16,6 +16,14 @@ class Client FINDING_PATH = "/appsec/v2/applications" HOST = "api.veracode.com" REQUEST_VERSION = "vcode_request_version_1" + RETRY_OPTIONS_504_ONLY = { + interval: 30, + max_interval: 30, + backoff_factor: 1, + methods: %i[get], + retry_statuses: [504] + }.freeze + MAX_RETRIES_504_ONLY = 2 def initialize(id, key, output_dir, filename, kenna_api_host, kenna_connector_id, kenna_api_key, veracode_score_mapping) @id = id @@ -51,7 +59,7 @@ def applications(page_size, custom_field_filter_name = "", custom_field_filter_v url = "https://#{HOST}#{app_request}" app_list = [] until url.nil? - response = http_get(url, {}, hmac_client: self) + response = http_get(url, {}, MAX_RETRIES_504_ONLY, true, hmac_client: self, retry_options: RETRY_OPTIONS_504_ONLY) return unless response result = JSON.parse(response.body) @@ -84,7 +92,7 @@ def cwe_recommendations(page_size) url = "https://#{HOST}#{cwe_request}" cwe_rec_list = [] until url.nil? - response = http_get(url, {}, hmac_client: self) + response = http_get(url, {}, MAX_RETRIES_504_ONLY, true, hmac_client: self, retry_options: RETRY_OPTIONS_504_ONLY) return unless response result = JSON.parse(response.body) @@ -103,7 +111,7 @@ def category_recommendations(page_size) url = "https://#{HOST}#{cat_request}" cat_rec_list = [] until url.nil? - response = http_get(url, {}, hmac_client: self) + response = http_get(url, {}, MAX_RETRIES_504_ONLY, true, hmac_client: self, retry_options: RETRY_OPTIONS_504_ONLY) return unless response result = JSON.parse(response.body) @@ -123,7 +131,7 @@ def get_findings(app_guid, app_name, tags, owner, page_size, scan_type) app_request = "#{FINDING_PATH}/#{app_guid}/findings?size=#{page_size}&scan_type=#{scan_type}" url = "https://#{HOST}#{app_request}" until url.nil? - response = http_get(url, {}, hmac_client: self) + response = http_get(url, {}, MAX_RETRIES_504_ONLY, true, hmac_client: self, retry_options: RETRY_OPTIONS_504_ONLY) if response.nil? puts "Unable to retrieve data for #{app_name}. Continuing..." @@ -248,7 +256,7 @@ def get_findings_sca(app_guid, app_name, tags, owner, page_size) app_request = "#{FINDING_PATH}/#{app_guid}/findings?size=#{page_size}&scan_type=SCA" url = "https://#{HOST}#{app_request}" until url.nil? - response = http_get(url, {}, hmac_client: self) + response = http_get(url, {}, MAX_RETRIES_504_ONLY, true, hmac_client: self, retry_options: RETRY_OPTIONS_504_ONLY) if response.nil? puts "Unable to retrieve data for #{app_name}. Continuing..." diff --git a/tasks/connectors/veracode_findings/lib/veracode_client.rb b/tasks/connectors/veracode_findings/lib/veracode_client.rb index 10a21de4..c38a6e79 100644 --- a/tasks/connectors/veracode_findings/lib/veracode_client.rb +++ b/tasks/connectors/veracode_findings/lib/veracode_client.rb @@ -15,6 +15,14 @@ class FindingsClient FINDING_PATH = "/appsec/v2/applications" HOST = "api.veracode.com" REQUEST_VERSION = "vcode_request_version_1" + RETRY_OPTIONS_504_ONLY = { + interval: 30, + max_interval: 30, + backoff_factor: 1, + methods: %i[get], + retry_statuses: [504] + }.freeze + MAX_RETRIES_504_ONLY = 2 def initialize(id, key, output_dir, filename, kenna_api_host, kenna_connector_id, kenna_api_key) @id = id @@ -32,7 +40,7 @@ def applications(page_size) url = "https://#{HOST}#{app_request}" app_list = [] until url.nil? - response = http_get(url, {}, hmac_client: self) + response = http_get(url, {}, MAX_RETRIES_504_ONLY, true, hmac_client: self, retry_options: RETRY_OPTIONS_504_ONLY) return unless response result = JSON.parse(response.body) @@ -57,7 +65,7 @@ def category_recommendations(page_size) url = "https://#{HOST}#{cat_request}" cat_rec_list = [] until url.nil? - response = http_get(url, {}, hmac_client: self) + response = http_get(url, {}, MAX_RETRIES_504_ONLY, true, hmac_client: self, retry_options: RETRY_OPTIONS_504_ONLY) return unless response result = JSON.parse(response.body) @@ -77,7 +85,7 @@ def get_findings(app_guid, app_name, tags, page_size, omit_line_number) app_request = "#{FINDING_PATH}/#{app_guid}/findings?size=#{page_size}" url = "https://#{HOST}#{app_request}" until url.nil? - response = http_get(url, {}, hmac_client: self) + response = http_get(url, {}, MAX_RETRIES_504_ONLY, true, hmac_client: self, retry_options: RETRY_OPTIONS_504_ONLY) if response.nil? puts "Unable to retrieve data for #{app_name}. Continuing..." @@ -196,7 +204,7 @@ def get_findings_sca(app_guid, app_name, tags, page_size) url = "https://#{HOST}#{app_request}" until url.nil? - response = http_get(url, {}, hmac_client: self) + response = http_get(url, {}, MAX_RETRIES_504_ONLY, true, hmac_client: self, retry_options: RETRY_OPTIONS_504_ONLY) if response.nil? puts "Unable to retrieve data for #{app_name}. Continuing..."