diff --git a/.env.test b/.env.test index 7269ea7dc..bc78088e5 100644 --- a/.env.test +++ b/.env.test @@ -38,6 +38,8 @@ JWT_SECRET_API_KEY='116016cca2a9f3eed660a65a78ba88091a73b330' SUPPRESS_JASMINE_DEPRECATION = 1 +COVERITY_SCAN_URL = 'http://vcrlocalhost.org:5008' + KB_API_AUTH_KEY = 'test' KB_AUTH_API = 'https://vcrlocalhost/auth' BDSA_VULNERABILITY_API = 'https://vcrlocalhost/bdsa/BDSA_ID' diff --git a/app/controllers/api/v1/projects_controller.rb b/app/controllers/api/v1/projects_controller.rb index 38fc11de1..ce1bd6dd2 100644 --- a/app/controllers/api/v1/projects_controller.rb +++ b/app/controllers/api/v1/projects_controller.rb @@ -6,6 +6,7 @@ class Api::V1::ProjectsController < ApplicationController skip_before_action :verify_authenticity_token before_action :authenticate_jwt + before_action :set_project_or_fail, only: [:create_scan_project] def create @project = build_project @@ -18,6 +19,14 @@ def create end end + def create_scan_project + response = get_scan_api_data(params[:url], 'api/projects') + return unless response && response['scan_project_id'] + + CodeLocationScan.where(code_location_id: @project.enlistments.first.code_location_id, + scan_project_id: response['scan_project_id']).first_or_create + end + private def project_params @@ -63,4 +72,13 @@ def code_location_branch(url) out, _err, _status = Open3.capture3("git ls-remote --symref #{url} HEAD | head -1 | awk '{print $2}'") out.strip.sub(/refs\/heads\//, '') end + + def get_scan_api_data(url, path) + return unless @project + + language = @project&.best_analysis&.main_language&.nice_name + data = { name: @project&.name, repo_url: url, user_id: params[:user_id], + language: scan_oh_language_mapping(language) } + ScanCoverityApi.save(path, data) + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 36a0b73d5..a733c1acf 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -127,5 +127,15 @@ def project_activity_level(project) def project_description_size_breached?(project) project.description && project.description.size > 800 end + + def scan_oh_language_mapping(language) + case language + when 'C++', 'C/C++', 'C' then 'CXX' + when 'Java' then 'JAVA' + when 'C#' then 'CSHARP' + when 'JavaScript' then 'JAVASCRIPT' + when 'Ruby', 'Python', 'PHP' then 'OTHER' + end + end end # rubocop: enable Metrics/ModuleLength diff --git a/app/lib/scan_coverity/scan_coverity_api.rb b/app/lib/scan_coverity/scan_coverity_api.rb new file mode 100644 index 000000000..edd579b02 --- /dev/null +++ b/app/lib/scan_coverity/scan_coverity_api.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class ScanCoverityApi + URL = ENV['COVERITY_SCAN_URL'] + + class << self + def resource_uri(path = nil, _query = {}) + URI("#{URL}/#{path}.json") + end + + def get_response(path = nil, query = {}) + uri = resource_uri(path, query) + response = Net::HTTP.get_response(uri) + handle_errors(response) { JSON.parse(response.body) } + end + + def save(path = nil, query = {}) + uri = resource_uri(path, query) + response = Net::HTTP.post_form(uri, query) + handle_errors(response) do + hsh = JSON.parse(response.body) + set_attributes_or_errors(response, hsh) + end + rescue JSON::ParserError + response.body + end + + private + + def handle_errors(response) + case response + when Net::HTTPServerError + raise ScanCoverityApiError, "#{response.message} => #{response.body}" + else + yield + end + end + + def save_success?(response) + response.is_a?(Net::HTTPSuccess) + end + + def set_errors(hsh) + @errors = hsh.key?('error') ? hsh['error'].with_indifferent_access : hsh + false + end + + def set_attributes(hsh) + @attributes = hsh + hsh.each do |key, value| + instance_variable_set("@#{key}", value) + end + end + + def set_attributes_or_errors(response, hsh) + save_success?(response) ? set_attributes(hsh) : set_errors(hsh) + end + end +end diff --git a/app/lib/scan_coverity/scan_coverity_api_error.rb b/app/lib/scan_coverity/scan_coverity_api_error.rb new file mode 100644 index 000000000..39b835d62 --- /dev/null +++ b/app/lib/scan_coverity/scan_coverity_api_error.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class ScanCoverityApiError < StandardError +end diff --git a/config/routes.rb b/config/routes.rb index 5f93b1738..ab4420894 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -493,7 +493,11 @@ post 'enlist' end resources :jwt, only: [:create] - resources :projects, only: [:create] + resources :projects, only: [:create] do + member do + get :create_scan_project + end + end end end diff --git a/fixtures/vcr_cassettes/CreateProjectFromMatchURL_record_none.yml b/fixtures/vcr_cassettes/CreateProjectFromMatchURL_record_none.yml index a4582c391..14eb87439 100644 --- a/fixtures/vcr_cassettes/CreateProjectFromMatchURL_record_none.yml +++ b/fixtures/vcr_cassettes/CreateProjectFromMatchURL_record_none.yml @@ -566,4 +566,59 @@ http_interactions: on Rails","fork":false,"url":"https://api.github.com/repos/rails/rails","forks_url":"https://api.github.com/repos/rails/rails/forks","keys_url":"https://api.github.com/repos/rails/rails/keys{/key_id}","collaborators_url":"https://api.github.com/repos/rails/rails/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/rails/rails/teams","hooks_url":"https://api.github.com/repos/rails/rails/hooks","issue_events_url":"https://api.github.com/repos/rails/rails/issues/events{/number}","events_url":"https://api.github.com/repos/rails/rails/events","assignees_url":"https://api.github.com/repos/rails/rails/assignees{/user}","branches_url":"https://api.github.com/repos/rails/rails/branches{/branch}","tags_url":"https://api.github.com/repos/rails/rails/tags","blobs_url":"https://api.github.com/repos/rails/rails/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/rails/rails/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/rails/rails/git/refs{/sha}","trees_url":"https://api.github.com/repos/rails/rails/git/trees{/sha}","statuses_url":"https://api.github.com/repos/rails/rails/statuses/{sha}","languages_url":"https://api.github.com/repos/rails/rails/languages","stargazers_url":"https://api.github.com/repos/rails/rails/stargazers","contributors_url":"https://api.github.com/repos/rails/rails/contributors","subscribers_url":"https://api.github.com/repos/rails/rails/subscribers","subscription_url":"https://api.github.com/repos/rails/rails/subscription","commits_url":"https://api.github.com/repos/rails/rails/commits{/sha}","git_commits_url":"https://api.github.com/repos/rails/rails/git/commits{/sha}","comments_url":"https://api.github.com/repos/rails/rails/comments{/number}","issue_comment_url":"https://api.github.com/repos/rails/rails/issues/comments{/number}","contents_url":"https://api.github.com/repos/rails/rails/contents/{+path}","compare_url":"https://api.github.com/repos/rails/rails/compare/{base}...{head}","merges_url":"https://api.github.com/repos/rails/rails/merges","archive_url":"https://api.github.com/repos/rails/rails/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/rails/rails/downloads","issues_url":"https://api.github.com/repos/rails/rails/issues{/number}","pulls_url":"https://api.github.com/repos/rails/rails/pulls{/number}","milestones_url":"https://api.github.com/repos/rails/rails/milestones{/number}","notifications_url":"https://api.github.com/repos/rails/rails/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/rails/rails/labels{/name}","releases_url":"https://api.github.com/repos/rails/rails/releases{/id}","deployments_url":"https://api.github.com/repos/rails/rails/deployments","created_at":"2008-04-11T02:19:47Z","updated_at":"2022-09-20T12:14:03Z","pushed_at":"2022-09-20T12:18:30Z","git_url":"git://github.com/rails/rails.git","ssh_url":"git@github.com:rails/rails.git","clone_url":"https://github.com/rails/rails.git","svn_url":"https://github.com/rails/rails","homepage":"https://rubyonrails.org","size":250233,"stargazers_count":51449,"watchers_count":51449,"language":"Ruby","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":20618,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":654,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["activejob","activerecord","framework","html","mvc","rails","ruby"],"visibility":"public","forks":20618,"open_issues":654,"watchers":51449,"default_branch":"main","temp_clone_token":null,"organization":{"login":"rails","id":4223,"node_id":"MDEyOk9yZ2FuaXphdGlvbjQyMjM=","avatar_url":"https://avatars.githubusercontent.com/u/4223?v=4","gravatar_id":"","url":"https://api.github.com/users/rails","html_url":"https://github.com/rails","followers_url":"https://api.github.com/users/rails/followers","following_url":"https://api.github.com/users/rails/following{/other_user}","gists_url":"https://api.github.com/users/rails/gists{/gist_id}","starred_url":"https://api.github.com/users/rails/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rails/subscriptions","organizations_url":"https://api.github.com/users/rails/orgs","repos_url":"https://api.github.com/users/rails/repos","events_url":"https://api.github.com/users/rails/events{/privacy}","received_events_url":"https://api.github.com/users/rails/received_events","type":"Organization","site_admin":false},"network_count":20618,"subscribers_count":2387}' recorded_at: Tue, 21 Sep 2022 17:25:35 GMT +- request: + method: post + uri: http://vcrlocalhost.org:5008/api/projects.json + body: + encoding: US-ASCII + string: name=Dummytestdata&repo_url=https%3A%2F%2Fgithub.com%2Frails%2Frails&user_id=e1dc08285095f4ff99199c3436532768&language=JAVA + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Host: + - vcrlocalhost.org:5008 + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 201 + message: success + headers: + Date: + - Tue, 14 Mar 2023 11:09:01 GMT + Content-Type: + - text/plain + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Request-Id: + - 83ba289fe76f4ed9a882a2a823be6d87 + X-Runtime: + - '0.006584' + X-Powered-By: + - Phusion Passenger 5.0.30 + Status: + - 201 + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Set-Cookie: + - incap_ses_1559_941207=QCvHB5yXehqCVRkBiK6iFc1VEGQAAAAA7G/+9TvEcxaAWzmLbgQZBg==; + path=/; Domain=.coverity.com + - nlbi_941207=bJ7MAhEsdhG1ODi7vBlnAwAAAAA3GPUJcWfVnLBF6Cb2d22Z; path=/; Domain=.coverity.com + - visid_incap_941207=QJfhtfo9QgiltzVrch2GzcxVEGQAAAAAQUIPAAAAAAD7Vbp21tN9wMYyJIC789MH; + expires=Tue, 12 Mar 2024 15:15:25 GMT; HttpOnly; path=/; Domain=.coverity.com + X-Cdn: + - Imperva + X-Iinfo: + - 12-49168636-49168647 NNYN CT(229 230 0) RT(1678792140667 100) q(0 0 5 5) r(7 + 7) U24 + body: + encoding: ASCII-8BIT + string: '{"scan_project_id": 1 }' + recorded_at: Tue, 14 Mar 2023 11:09:02 GMT recorded_with: VCR 6.0.0 diff --git a/fixtures/vcr_cassettes/scan_projects.yml b/fixtures/vcr_cassettes/scan_projects.yml new file mode 100644 index 000000000..6ff7878e9 --- /dev/null +++ b/fixtures/vcr_cassettes/scan_projects.yml @@ -0,0 +1,116 @@ +--- +http_interactions: +- request: + method: post + uri: http://vcrlocalhost.org:5008/api/projects.json + body: + encoding: US-ASCII + string: name=Dummytestdata&repo_url=https%3A%2F%2Fgithub.com%2Frails%2Frails&user_id=e1dc08285095f4ff99199c3436532768&language=JAVA + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Host: + - vcrlocalhost.org:5008 + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 201 + message: success + headers: + Date: + - Tue, 14 Mar 2023 11:09:01 GMT + Content-Type: + - text/plain + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Request-Id: + - 83ba289fe76f4ed9a882a2a823be6d87 + X-Runtime: + - '0.006584' + X-Powered-By: + - Phusion Passenger 5.0.30 + Status: + - 201 + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Set-Cookie: + - incap_ses_1559_941207=QCvHB5yXehqCVRkBiK6iFc1VEGQAAAAA7G/+9TvEcxaAWzmLbgQZBg==; + path=/; Domain=.coverity.com + - nlbi_941207=bJ7MAhEsdhG1ODi7vBlnAwAAAAA3GPUJcWfVnLBF6Cb2d22Z; path=/; Domain=.coverity.com + - visid_incap_941207=QJfhtfo9QgiltzVrch2GzcxVEGQAAAAAQUIPAAAAAAD7Vbp21tN9wMYyJIC789MH; + expires=Tue, 12 Mar 2024 15:15:25 GMT; HttpOnly; path=/; Domain=.coverity.com + X-Cdn: + - Imperva + X-Iinfo: + - 12-49168636-49168647 NNYN CT(229 230 0) RT(1678792140667 100) q(0 0 5 5) r(7 + 7) U24 + body: + encoding: ASCII-8BIT + string: '{"scan_project_id": 1 }' + recorded_at: Tue, 14 Mar 2023 11:09:02 GMT +--- +http_interactions: +- request: + method: post + uri: http://vcrlocalhost.org:5008/api/projects.json + body: + encoding: US-ASCII + string: name=&repo_url=https%3A%2F%2Fgithub.com%2Frails%2Frails&user_id=d1224324214 + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Host: + - vcrlocalhost.org:5008 + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 401 + message: unauthorized + headers: + Date: + - Tue, 14 Mar 2023 11:09:01 GMT + Content-Type: + - text/plain + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Request-Id: + - 83ba289fe76f4ed9a882a2a823be6d87 + X-Runtime: + - '0.006584' + X-Powered-By: + - Phusion Passenger 5.0.30 + Status: + - 401 + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Set-Cookie: + - incap_ses_1559_941207=QCvHB5yXehqCVRkBiK6iFc1VEGQAAAAA7G/+9TvEcxaAWzmLbgQZBg==; + path=/; Domain=.coverity.com + - nlbi_941207=bJ7MAhEsdhG1ODi7vBlnAwAAAAA3GPUJcWfVnLBF6Cb2d22Z; path=/; Domain=.coverity.com + - visid_incap_941207=QJfhtfo9QgiltzVrch2GzcxVEGQAAAAAQUIPAAAAAAD7Vbp21tN9wMYyJIC789MH; + expires=Tue, 12 Mar 2024 15:15:25 GMT; HttpOnly; path=/; Domain=.coverity.com + X-Cdn: + - Imperva + X-Iinfo: + - 12-49168636-49168647 NNYN CT(229 230 0) RT(1678792140667 100) q(0 0 5 5) r(7 + 7) U24 + body: + encoding: ASCII-8BIT + string: '{"message": "unauthorized"}' + recorded_at: Tue, 14 Mar 2023 11:09:02 GMT +recorded_with: VCR 6.0.0 + diff --git a/fixtures/vcr_cassettes/scan_projects_error.yml b/fixtures/vcr_cassettes/scan_projects_error.yml new file mode 100644 index 000000000..f90a399e7 --- /dev/null +++ b/fixtures/vcr_cassettes/scan_projects_error.yml @@ -0,0 +1,59 @@ +--- +http_interactions: +- request: + method: post + uri: http://vcrlocalhost.org:5008/api/projects.json + body: + encoding: US-ASCII + string: name=&repo_url=https%3A%2F%2Fgithub.com%2Frails%2Frails&user_id=d1224324214 + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Host: + - vcrlocalhost.org:5008 + Content-Type: + - application/x-www-form-urlencoded + response: + status: + code: 400 + message: bad_request + headers: + Date: + - Tue, 14 Mar 2023 11:09:01 GMT + Content-Type: + - text/plain + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Request-Id: + - 83ba289fe76f4ed9a882a2a823be6d87 + X-Runtime: + - '0.006584' + X-Powered-By: + - Phusion Passenger 5.0.30 + Status: + - 400 + Strict-Transport-Security: + - max-age=15724800; includeSubDomains + Set-Cookie: + - incap_ses_1559_941207=QCvHB5yXehqCVRkBiK6iFc1VEGQAAAAA7G/+9TvEcxaAWzmLbgQZBg==; + path=/; Domain=.coverity.com + - nlbi_941207=bJ7MAhEsdhG1ODi7vBlnAwAAAAA3GPUJcWfVnLBF6Cb2d22Z; path=/; Domain=.coverity.com + - visid_incap_941207=QJfhtfo9QgiltzVrch2GzcxVEGQAAAAAQUIPAAAAAAD7Vbp21tN9wMYyJIC789MH; + expires=Tue, 12 Mar 2024 15:15:25 GMT; HttpOnly; path=/; Domain=.coverity.com + X-Cdn: + - Imperva + X-Iinfo: + - 12-49168636-49168647 NNYN CT(229 230 0) RT(1678792140667 100) q(0 0 5 5) r(7 + 7) U24 + body: + encoding: ASCII-8BIT + string: '{"message": "Language cant be blank"}' + recorded_at: Tue, 14 Mar 2023 11:09:02 GMT +recorded_with: VCR 6.0.0 + diff --git a/test/controllers/api_v1_projects_controller_test.rb b/test/controllers/api_v1_projects_controller_test.rb index 8510a25b6..a2ef148a7 100644 --- a/test/controllers/api_v1_projects_controller_test.rb +++ b/test/controllers/api_v1_projects_controller_test.rb @@ -87,4 +87,17 @@ class Api::V1::ProjectsControllerTest < ActionController::TestCase end end end + + describe 'create_scan_project' do + it 'it create a scan project if not found' do + VCR.use_cassette('CreateProjectFromMatchURL, :record => :none') do + url = 'https://github.com/rails/rails' + project = create(:project, name: 'rails', description: 'Ruby on Rails', vanity_url: 'rails') + create(:enlistment, project: project, code_location_id: 1) + params = { id: project.vanity_url, JWT: @jwt, url: url, user_id: 'e1dc08285095f4ff99199c3436532768' } + get :create_scan_project, params: params, format: :json + assert_response 204 + end + end + end end diff --git a/test/helpers/projects_helper_test.rb b/test/helpers/projects_helper_test.rb index c0f1ecdc8..61ec886cc 100644 --- a/test/helpers/projects_helper_test.rb +++ b/test/helpers/projects_helper_test.rb @@ -156,4 +156,20 @@ class ProjectsHelperTest < ActionView::TestCase _(project_description_size_breached?(@project)).must_equal false end end + + describe 'scan_oh_language_mapping' do + it 'should return matching value' do + _(scan_oh_language_mapping('Java')).must_equal 'JAVA' + _(scan_oh_language_mapping('C/C++')).must_equal 'CXX' + _(scan_oh_language_mapping('C#')).must_equal 'CSHARP' + _(scan_oh_language_mapping('JavaScript')).must_equal 'JAVASCRIPT' + _(scan_oh_language_mapping('Ruby')).must_equal 'OTHER' + end + end + + describe 'project_separator_text' do + it 'should return seperation text' do + _(project_separator_text).must_equal ' | ' + end + end end diff --git a/test/lib/scan_coverity_api_test.rb b/test/lib/scan_coverity_api_test.rb new file mode 100644 index 000000000..425b9e1a3 --- /dev/null +++ b/test/lib/scan_coverity_api_test.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ScanCoverityApiTest < ActiveSupport::TestCase + it 'must return true when attributes are valid' do + path = 'api/projects' + data = { name: 'Dummytestdata', repo_url: 'https://github.com/rails/rails', + user_id: 'e1dc08285095f4ff99199c3436532768', language: 'JAVA' } + VCR.use_cassette('scan_projects', match_requests_on: [:path]) do + _(ScanCoverityApi.save(path, data)).wont_be_empty + end + end + + it 'must return false when attributes are invalid' do + path = 'api/projects' + data = { name: 'Dummytestdata', repo_url: 'https://github.com/rails/rails', + user_id: 'e1dc08285095f4ff99199c3436532768' } + VCR.use_cassette('scan_projects_error', match_requests_on: [:path]) do + _(ScanCoverityApi.save(path, data)).must_equal false + end + end + + it 'must raise error when the api throws an exception' do + Net::HTTP.stubs(:get_response).returns(Net::HTTPServerError.new('1', '503', 'error')) + Net::HTTPServerError.any_instance.stubs(:body).returns('') + path = 'api/projects' + _(-> { ScanCoverityApi.get_response(path) }).must_raise(ScanCoverityApiError) + end +end