diff --git a/Gemfile.lock b/Gemfile.lock index 2589b5bf32..2d07ff3358 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -325,7 +325,7 @@ GEM rb-fsevent (0.9.8) rb-inotify (0.10.1) ffi (~> 1.0) - rbtrace (0.4.12) + rbtrace (0.4.14) ffi (>= 1.0.6) msgpack (>= 0.4.3) optimist (>= 3.0.0) diff --git a/lib/travis/api/v3/models/scan_result.rb b/lib/travis/api/v3/models/scan_result.rb new file mode 100644 index 0000000000..5690da3ef2 --- /dev/null +++ b/lib/travis/api/v3/models/scan_result.rb @@ -0,0 +1,30 @@ + +module Travis::API::V3 + class Models::ScanResult + attr_reader :id, :log_id, :job_id, :owner_id, :owner_type, :created_at, :formatted_content, :issues_found, :archived, :purged_at, :token, :token_created_at, + :job_number, :build_id, :build_number, :job_finished_at, :commit_sha, :commit_compare_url, :commit_branch, :repository_id + + def initialize(attributes = {}) + @id = attributes.fetch('id') + @log_id = attributes.fetch('log_id') + @job_id = attributes.fetch('job_id') + @owner_id = attributes.fetch('owner_id') + @owner_type = attributes.fetch('owner_type') + @created_at = attributes.fetch('created_at') + @formatted_content = attributes.fetch('formatted_content') + @issues_found = attributes.fetch('issues_found') + @archived = attributes.fetch('archived') + @purged_at = attributes.fetch('purged_at') + @token = attributes.fetch('token') + @token_created_at = attributes.fetch('token_created_at') + @job_number = attributes.fetch('job_number') + @build_id = attributes.fetch('build_id') + @build_number = attributes.fetch('build_number') + @job_finished_at = attributes.fetch('job_finished_at') + @commit_sha = attributes.fetch('commit_sha') + @commit_compare_url = attributes.fetch('commit_compare_url') + @commit_branch = attributes.fetch('commit_branch') + @repository_id = attributes.fetch('repository_id') + end + end +end diff --git a/lib/travis/api/v3/models/scanner_collection.rb b/lib/travis/api/v3/models/scanner_collection.rb new file mode 100644 index 0000000000..dcc9ae5346 --- /dev/null +++ b/lib/travis/api/v3/models/scanner_collection.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Travis::API::V3 + class Models::ScannerCollection + def initialize(collection, total_count) + @collection = collection + @total_count = total_count + end + + def count(*) + @total_count + end + + def limit(*) + self + end + + def offset(*) + self + end + + def map + return @collection.map unless block_given? + + @collection.map { |x| yield x } + end + + def to_sql + "scanner_query:#{Time.now.to_i}" + end + end +end diff --git a/lib/travis/api/v3/permissions/repository.rb b/lib/travis/api/v3/permissions/repository.rb index 10099b5fe4..b75d5b6514 100644 --- a/lib/travis/api/v3/permissions/repository.rb +++ b/lib/travis/api/v3/permissions/repository.rb @@ -42,6 +42,10 @@ def create_request? write? end + def check_scan_results? + write? + end + def admin? access_control.adminable? object end diff --git a/lib/travis/api/v3/queries/scan_result.rb b/lib/travis/api/v3/queries/scan_result.rb new file mode 100644 index 0000000000..a091d258f3 --- /dev/null +++ b/lib/travis/api/v3/queries/scan_result.rb @@ -0,0 +1,15 @@ +module Travis::API::V3 + class Queries::ScanResult < RemoteQuery + params :id + + def find + scanner_client.get_scan_result(id) + end + + private + + def scanner_client + @_scanner_client ||= ScannerClient.new(nil) + end + end +end diff --git a/lib/travis/api/v3/queries/scan_results.rb b/lib/travis/api/v3/queries/scan_results.rb new file mode 100644 index 0000000000..a877aa572b --- /dev/null +++ b/lib/travis/api/v3/queries/scan_results.rb @@ -0,0 +1,22 @@ +module Travis::API::V3 + class Queries::ScanResults < Query + params :repository_id, :offset, :limit + + def all + # Reset the scan status on viewing the reports + Repository.find(repository_id).update!(scan_failed_at: nil) + + page = (offset.to_i / limit.to_i) + 1 + scanner_client(repository_id).scan_results( + page.to_s, + limit + ) + end + + private + + def scanner_client(repository_id) + @_scanner_client ||= ScannerClient.new(repository_id) + end + end +end diff --git a/lib/travis/api/v3/renderer/repository.rb b/lib/travis/api/v3/renderer/repository.rb index 775fa6ca5d..e183cc2b50 100644 --- a/lib/travis/api/v3/renderer/repository.rb +++ b/lib/travis/api/v3/renderer/repository.rb @@ -1,9 +1,9 @@ module Travis::API::V3 class Renderer::Repository < ModelRenderer representation(:minimal, :id, :name, :slug) - representation(:standard, :id, :name, :slug, :description, :github_id, :vcs_id, :vcs_type, :github_language, :active, :private, :owner, :owner_name, :vcs_name, :default_branch, :starred, :managed_by_installation, :active_on_org, :migration_status, :history_migration_status, :shared, :config_validation, :server_type) - representation(:experimental, :id, :name, :slug, :description, :vcs_id, :vcs_type, :github_id, :github_language, :active, :private, :owner, :default_branch, :starred, :current_build, :last_started_build, :next_build_number, :server_type) - representation(:internal, :id, :name, :slug, :github_id, :vcs_id, :vcs_type, :active, :private, :owner, :default_branch, :private_key, :token, :user_settings, :server_type) + representation(:standard, :id, :name, :slug, :description, :github_id, :vcs_id, :vcs_type, :github_language, :active, :private, :owner, :owner_name, :vcs_name, :default_branch, :starred, :managed_by_installation, :active_on_org, :migration_status, :history_migration_status, :shared, :config_validation, :server_type, :scan_failed_at) + representation(:experimental, :id, :name, :slug, :description, :vcs_id, :vcs_type, :github_id, :github_language, :active, :private, :owner, :default_branch, :starred, :current_build, :last_started_build, :next_build_number, :server_type, :scan_failed_at) + representation(:internal, :id, :name, :slug, :github_id, :vcs_id, :vcs_type, :active, :private, :owner, :default_branch, :private_key, :token, :user_settings, :server_type, :scan_failed_at) representation(:additional, :allow_migration) hidden_representations(:experimental, :internal) diff --git a/lib/travis/api/v3/renderer/scan_result.rb b/lib/travis/api/v3/renderer/scan_result.rb new file mode 100644 index 0000000000..c11330c148 --- /dev/null +++ b/lib/travis/api/v3/renderer/scan_result.rb @@ -0,0 +1,27 @@ +module Travis::API::V3 + class Renderer::ScanResult < ModelRenderer + representation(:minimal, :id, :created_at, :formatted_content, :issues_found, :job_id, :build_id, :job_number, :build_number, :job_finished_at, + :commit_sha, :commit_compare_url, :commit_branch, :build_created_by) + representation(:standard, *representations[:minimal]) + + def build_created_by + job = Travis::API::V3::Models::Job.find(model.job_id) + build = Travis::API::V3::Models::Build.find(job.source_id) + return nil unless creator = build.sender + { + '@type' => build.sender_type.downcase, + '@href' => created_by_href(creator), + '@representation' => 'minimal'.freeze, + 'id' => creator.id, + 'login' => creator.login + } + end + + private def created_by_href(creator) + case creator + when V3::Models::Organization then Renderer.href(:organization, script_name: script_name, id: creator.id) + when V3::Models::User then Renderer.href(:user, script_name: script_name, id: creator.id) + end + end + end +end diff --git a/lib/travis/api/v3/renderer/scan_results.rb b/lib/travis/api/v3/renderer/scan_results.rb new file mode 100644 index 0000000000..839dbddab0 --- /dev/null +++ b/lib/travis/api/v3/renderer/scan_results.rb @@ -0,0 +1,6 @@ +module Travis::API::V3 + class Renderer::ScanResults < CollectionRenderer + type :scan_results + collection_key :scan_results + end +end diff --git a/lib/travis/api/v3/routes.rb b/lib/travis/api/v3/routes.rb index 083c5ef00a..c577aed06a 100644 --- a/lib/travis/api/v3/routes.rb +++ b/lib/travis/api/v3/routes.rb @@ -283,6 +283,16 @@ module Routes end end + resource :scan_results do + route '/scan_results' + get :all + end + + resource :scan_result do + route '/scan_result/{scan_result.id}' + get :find + end + resource :user do capture id: :digit route '/user/{user.id}' diff --git a/lib/travis/api/v3/scanner_client.rb b/lib/travis/api/v3/scanner_client.rb new file mode 100644 index 0000000000..2606a17f5e --- /dev/null +++ b/lib/travis/api/v3/scanner_client.rb @@ -0,0 +1,84 @@ + +# frozen_string_literal: true + +module Travis::API::V3 + class ScannerClient + class ConfigurationError < StandardError; end + + def initialize(repository_id) + @repository_id = repository_id + end + + def scan_results(page, limit) + query_string = query_string_from_params( + repository_id: @repository_id, + limit: limit, + page: page || '1', + ) + response = connection.get("/scan_results?#{query_string}") + + handle_errors_and_respond(response) do |body| + scan_results = body['scan_results'].map do |scan_result| + Travis::API::V3::Models::ScanResult.new(scan_result) + end + + Travis::API::V3::Models::ScannerCollection.new(scan_results, body.fetch('total_count', 0)) + end + end + + def get_scan_result(id) + response = connection.get("/scan_results/#{id}") + handle_errors_and_respond(response) do |body| + Travis::API::V3::Models::ScanResult.new(body.fetch('scan_result')) + end + end + + private + + def handle_errors_and_respond(response) + case response.status + when 200, 201 + yield(response.body) if block_given? + when 202 + true + when 204 + true + when 400 + raise Travis::API::V3::ClientError, response.body&.fetch('error', '') + when 403 + raise Travis::API::V3::InsufficientAccess, response.body&.fetch('rejection_code', '') + when 404 + raise Travis::API::V3::NotFound, response.body&.fetch('error', '') + when 422 + raise Travis::API::V3::UnprocessableEntity, response.body&.fetch('error', '') + else + raise Travis::API::V3::ServerError, 'Scanner API failed' + end + end + + def connection(timeout: 20) + @connection ||= Faraday.new(url: scanner_url, ssl: { ca_path: '/usr/lib/ssl/certs' }) do |conn| + conn.headers[:Authorization] = "Token token=\"#{scanner_token}\"" + conn.headers['Content-Type'] = 'application/json' + conn.request :json + conn.response :json + conn.options[:open_timeout] = timeout + conn.options[:timeout] = timeout + conn.use OpenCensus::Trace::Integrations::FaradayMiddleware if Travis::Api::App::Middleware::OpenCensus.enabled? + conn.adapter :net_http + end + end + + def scanner_url + Travis.config.scanner.url || raise(ConfigurationError, 'No Scanner API URL configured!') + end + + def scanner_token + Travis.config.scanner.token || raise(ConfigurationError, 'No Scanner Auth Token configured!') + end + + def query_string_from_params(params) + params.delete_if { |_, v| v.nil? || v.empty? }.to_query + end + end +end diff --git a/lib/travis/api/v3/services.rb b/lib/travis/api/v3/services.rb index 90b850d571..06790974ce 100644 --- a/lib/travis/api/v3/services.rb +++ b/lib/travis/api/v3/services.rb @@ -39,6 +39,8 @@ module Services Leads = Module.new { extend Services } Lint = Module.new { extend Services } Log = Module.new { extend Services } + ScanResult = Module.new { extend Services } + ScanResults = Module.new { extend Services } Messages = Module.new { extend Services } Organization = Module.new { extend Services } Organizations = Module.new { extend Services } diff --git a/lib/travis/api/v3/services/scan_result/find.rb b/lib/travis/api/v3/services/scan_result/find.rb new file mode 100644 index 0000000000..ee924c201e --- /dev/null +++ b/lib/travis/api/v3/services/scan_result/find.rb @@ -0,0 +1,19 @@ +module Travis::API::V3 + class Services::ScanResult::Find < Service + params :id + + def run! + raise LoginRequired unless access_control.full_access_or_logged_in? + scan_result = query(:scan_result).find + + repository = Travis::API::V3::Models::Repository.find(scan_result.repository_id) + check_access(repository) + + result scan_result + end + + def check_access(repository) + access_control.permissions(repository).check_scan_results! + end + end +end diff --git a/lib/travis/api/v3/services/scan_results/all.rb b/lib/travis/api/v3/services/scan_results/all.rb new file mode 100644 index 0000000000..514b02ee77 --- /dev/null +++ b/lib/travis/api/v3/services/scan_results/all.rb @@ -0,0 +1,19 @@ +module Travis::API::V3 + class Services::ScanResults::All < Service + params :repository_id + paginate + + def run! + raise LoginRequired unless access_control.full_access_or_logged_in? + + repository = Travis::API::V3::Models::Repository.find(params['repository_id']) + check_access(repository) + + result query(:scan_results).all + end + + def check_access(repository) + access_control.permissions(repository).check_scan_results! + end + end +end diff --git a/lib/travis/config/defaults.rb b/lib/travis/config/defaults.rb index 463e86dec2..3e6a50ed5d 100644 --- a/lib/travis/config/defaults.rb +++ b/lib/travis/config/defaults.rb @@ -41,13 +41,9 @@ def fallback_logs_api_auth_token auth: { target_origin: nil }, assets: { host: HOSTS[Travis.env.to_sym] }, amqp: { username: 'guest', password: 'guest', host: 'localhost', prefetch: 1 }, - billing: {}, closeio: { key: 'key' }, gdpr: {}, - insights: { endpoint: 'https://insights.travis-ci.dev/', auth_token: 'secret' }, database: { adapter: 'postgresql', database: "travis_#{Travis.env}", encoding: 'unicode', min_messages: 'warning', variables: { statement_timeout: 10_000 } }, - fallback_logs_api: { url: fallback_logs_api_auth_url, token: fallback_logs_api_auth_token }, - logs_api: { url: logs_api_url, token: logs_api_auth_token }, db: { max_statement_timeout_in_seconds: 15, slow_host_max_statement_timeout_in_seconds: 60}, log_options: { s3: { access_key_id: '', secret_access_key: ''}}, s3: { access_key_id: '', secret_access_key: ''}, @@ -86,10 +82,15 @@ def fallback_logs_api_auth_token build_backup_options: ENV['GCE_BUILD_BACKUP_OPTIONS'] ? JSON.parse(ENV['GCE_BUILD_BACKUP_OPTIONS']) : { gcs: { bucket_name: 'fillme', json_key: JSON.parse("{\n \"type\": \"service_account\",\n \"project_id\": \"fillme\",\n \"private_key_id\": \"b1c57117b4a0b8ae2af2f45b19a1cf9727bc6caf\",\n \"private_key\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDRaHA5z0vXNVSr\\nlLVd/smJpNkpzk4BoHq+zcuuzvKTf1ZY1LnrAhldUbDTKY67c06eOYwVrQc3tEIp\\nJCNhIDNY1lRfGJag6t2v5C710WY+X7qVnRhSpgthvWFX/Rm9KIv3be8AJvUTMUDQ\\nAL10eYrIWJOI/J59khuKvr7khIlysASwGoZc8UgcufxGuwCziNyEIfH1nxiBKILR\\nM1+LdYi/Avyb4bQth5x03THVEmdRDjV7Yoo2c17XElIXtkl03nUce4w8BCu+T1G2\\nKRkApHcQ8R0BDTjjBzaQuXTtTpLvkmZzJ1i0/kPNdnT70lv6N2nI1AN2DVkXDCkG\\nNlZ283W7AgMBAAECggEAC/W6CyMywqzSFCafISovGoRmvsOAowkmWYVpb6d0JUZt\\niQ9FOw3YowLKZZUHCN+yCslgndBPDDhoWu8schyjshwzn2bJG5GubaBLqlB2VXOk\\nNW1OeVHwbnmheKQE90+8hropn0maT6lNeVPBfkh+y6h7bKR47NUOa6MvRd/n9bvL\\nT7pP5ZAHoPoTcbUftOX0gDq0u+uRULe/rduxB0S2EHDEtZEH+ioUOP9AomnaRDSy\\n0spH1s2FUZxIbKBQzsrqMCai4MSjeUrJMTR3ZlpfXePirettvilSWqEXDLvvwaak\\ngehELuM5lH4T49wf4PmEYZ8Jqkh9ku+oNYJdJvGR2QKBgQD7KjhJx9usl0afrIH3\\nw7saHELluWGqHNa6j+TJDpY7N5lLLIym/br9d+cuLTF5CBEHJ502coDR9cyrLVZX\\na05CGmEfSVrSrLUyAU+mHHdsn8n6CCATmlgtPyzzt2c29J7dHUZL94zW/yG6Btg6\\nm+nY4eBKreLpj0+3KbhI/q0q1wKBgQDVcG8Ek3Kt2buOrpDBqxcwB31QljntT+7+\\nYcTZctYL/y7Lm2VcTjserNa3AjG59Z5iaQjKFPhbAvMHfppklyiVSVBRfn4bLTcx\\nSM9I+lntODtGI/BiHVE7hfoYKzwz/3Aj3npiOO9xnOfAgEubGn9DrOzLXPsvWN7E\\nz+/iSr4zvQKBgHFVB7kjGZizWgbKzIqEI3UQs479K3ibMrlUHKQslNV7rQwiugTQ\\nEQQ2inZnph866JQV5/adjEsxYn0LJB6mKNXjGVgIvZa6n7hEpzAJQEoff//2kqLF\\nzmv8SchfRY+iqdyUTRgSR9broMhUNlWb7NUUdyS7edxx8kJv7NvjLzhZAoGAMre9\\n2bOD26XSeKwof6y9HM+ayox4BVkqLE5lLVqpXD5uCznI0y9PwxFFEEW4NT0VPsNA\\nsGxdO5suzsgZve9hWGAMcuEA7EpJRC/N+cRrm//xrdAabeYTiHZkoFudualoJ03V\\nfQOUekXTmB2kWZ3pQdaUihp1IaIXhWL32Kj0G20CgYAfGInHUZf5mqxRK/id8B5Q\\nYmpnLDXsNu1I4qFKeaBo4dF2SGByNW7fVbK/BdCSg5Ov3Ui6m3QWBbJXh1a8QVYA\\nICvwJWqm53bNpocrFPAeXLy9xL5/5CEeVGQNcxFvUF3QgaPVmjbTsZk8vjbidUZk\\nOU8bArrUjGTxNJOe7GebhA==\\n-----END PRIVATE KEY-----\\n\",\n \"client_email\": \"service@fillme.iam.gserviceaccount.com\",\n \"client_id\": \"100937792194965642651\",\n \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n \"token_uri\": \"https://oauth2.googleapis.com/token\",\n \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/service%40fillme.iam.gserviceaccount.com\"\n}\n") } }, merge: { auth_token: 'merge-auth-token', api_url: 'https://merge.localhost' }, force_authentication: false, - yml: { url: 'https://yml.travis-ci.org', token: 'secret', auth_key: 'abc123' }, read_only: ENV['READ_ONLY'] || false, - vcs: {}, - job_log_access_permissions: { time_based_limit: false, access_based_limit: false, older_than_days: 365, max_days_value: 730, min_days_value: 30 } + job_log_access_permissions: { time_based_limit: false, access_based_limit: false, older_than_days: 365, max_days_value: 730, min_days_value: 30 }, + billing: {}, + vcs: {}, + yml: { url: 'https://yml.travis-ci.org', token: 'secret', auth_key: 'abc123' }, + logs_api: { url: logs_api_url, token: logs_api_auth_token }, + fallback_logs_api: { url: fallback_logs_api_auth_url, token: fallback_logs_api_auth_token }, + scanner: {}, + insights: { endpoint: 'https://insights.travis-ci.dev/', auth_token: 'secret' } default :_access => [:key] diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f384c29916..e1f48ca335 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -31,6 +31,7 @@ require 'auth/helpers' require 'support/active_record' require 'support/billing_spec_helper' +require 'support/scanner_spec_helper' require 'support/env' require 'support/formats' require 'support/gcs' @@ -79,6 +80,7 @@ def parsed_body c.include Support::Env c.include Support::AuthHelpers, auth_helpers: true c.include Support::BillingSpecHelper, billing_spec_helper: true + c.include Support::ScannerSpecHelper, scanner_spec_helper: true c.include Support::GdprSpecHelper, gdpr_spec_helper: true # for auth tests against staging, how the hell does this work, if at all diff --git a/spec/support/scanner_spec_helper.rb b/spec/support/scanner_spec_helper.rb new file mode 100644 index 0000000000..712be873dd --- /dev/null +++ b/spec/support/scanner_spec_helper.rb @@ -0,0 +1,132 @@ +module Support + module ScannerSpecHelper + def stub_scanner_request(method, path, query: '', auth_key:) + url = URI(scanner_url).tap do |url| + url.path = path + url.query = query + end.to_s + stub_request(method, url).with(headers: { 'Authorization' => "Token token=\"#{auth_key}\"" }) + end + + def scanner_scan_results_response(job_id) + { + "scan_results" => [ + { + "id"=>1, + "log_id"=>1, + "job_id"=>job_id, + "owner_id"=>9830, + "owner_type"=>"User", + "created_at"=>"2022-10-20T08:55:20.522Z", + "content"=>{ + "1"=>[ + { + "size"=>20, + "column"=>9, + "plugin_name"=>"trivy", + "finding_name"=>"AWS Access Key ID" + }, + { + "size"=>-1, + "column"=>-1, + "plugin_name"=>"detect_secrets", + "finding_name"=>"AWS Access Key" + } + ] + }, + "issues_found"=>1, + "archived"=>nil, + "purged_at"=>nil, + "token"=>"123", + "token_created_at"=>nil, + "job_number"=>"1", + "build_id"=>nil, + "build_number"=>"1", + "job_finished_at"=>nil, + "commit_sha"=>"SHA", + "commit_compare_url"=>nil, + "commit_branch"=>nil, + "repository_id"=>4, + "formatted_content"=> + "travis_fold:start:trivy\r\e[0K\e[33;1mIn line 1 of your build job log trivy found\e[0m\n" + + "AWS Access Key ID\n" + + "travis_fold:end:trivy\n" + + "\n" + + "\n" + + "travis_fold:start:detect_secrets\r\e[0K\e[33;1mIn line 1 of your build job log detect_secrets found\e[0m\n" + + "AWS Access Key\n" + + "travis_fold:end:detect_secrets\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "Our backend build job log monitoring uses:\n" + + " • trivy\n" + + " • detect_secrets\n" + + "Called via command line and under respective permissive licenses." + } + ], + "total_count"=>1 + } + end + + def scanner_scan_result_response(job_id, repo_id) + { + "scan_result" => { + "id"=>1, + "log_id"=>1, + "job_id"=>job_id, + "owner_id"=>9830, + "owner_type"=>"User", + "created_at"=>"2022-10-20T08:55:20.522Z", + "content"=>{ + "1"=>[ + { + "size"=>20, + "column"=>9, + "plugin_name"=>"trivy", + "finding_name"=>"AWS Access Key ID" + }, + { + "size"=>-1, + "column"=>-1, + "plugin_name"=>"detect_secrets", + "finding_name"=>"AWS Access Key" + } + ] + }, + "issues_found"=>1, + "archived"=>nil, + "purged_at"=>nil, + "token"=>"123", + "token_created_at"=>nil, + "job_number"=>"1", + "build_id"=>nil, + "build_number"=>"1", + "job_finished_at"=>nil, + "commit_sha"=>"SHA", + "commit_compare_url"=>nil, + "commit_branch"=>nil, + "repository_id"=>repo_id, + "formatted_content"=> + "travis_fold:start:trivy\r\e[0K\e[33;1mIn line 1 of your build job log trivy found\e[0m\n" + + "AWS Access Key ID\n" + + "travis_fold:end:trivy\n" + + "\n" + + "\n" + + "travis_fold:start:detect_secrets\r\e[0K\e[33;1mIn line 1 of your build job log detect_secrets found\e[0m\n" + + "AWS Access Key\n" + + "travis_fold:end:detect_secrets\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "Our backend build job log monitoring uses:\n" + + " • trivy\n" + + " • detect_secrets\n" + + "Called via command line and under respective permissive licenses." + } + } + end + end +end diff --git a/spec/v3/models/scanner_collection_spec.rb b/spec/v3/models/scanner_collection_spec.rb new file mode 100644 index 0000000000..9a77fc544d --- /dev/null +++ b/spec/v3/models/scanner_collection_spec.rb @@ -0,0 +1,36 @@ +describe Travis::API::V3::Models::ScannerCollection do + let(:collection) { [ { id: 1, name: 'test1' }, { id: 2, name: 'test2' }, { id: 3, name: 'test3' } ] } + subject { Travis::API::V3::Models::ScannerCollection.new(collection, collection.count) } + + describe "#count" do + it 'returns total_count' do + expect(subject.count).to eq(collection.count) + end + end + + describe "#limit" do + it 'returns self' do + expect(subject.limit).to eq(subject) + end + end + + describe "#offset" do + it 'returns self' do + expect(subject.offset).to eq(subject) + end + end + + describe "#map" do + it 'returns map on collection' do + expect(subject.map { |e| e[:id] }).to eq([1, 2, 3]) + end + end + + describe "#to_sql" do + before { Timecop.freeze(Time.now.utc) } + + it 'returns placeholder string' do + expect(subject.to_sql).to eq("scanner_query:#{Time.now.to_i}") + end + end +end diff --git a/spec/v3/scanner_client_spec.rb b/spec/v3/scanner_client_spec.rb new file mode 100644 index 0000000000..fa209cb2fa --- /dev/null +++ b/spec/v3/scanner_client_spec.rb @@ -0,0 +1,99 @@ +describe Travis::API::V3::ScannerClient, scanner_spec_helper: true do + let(:scanner_client) { described_class.new(repository_id.to_s) } + let(:repository_id) { rand(999) } + let(:scanner_url) { 'https://scanner.travis-ci.com/' } + let(:auth_key) { 'supersecret' } + + before do + Travis.config.scanner.url = scanner_url + Travis.config.scanner.token = auth_key + end + + describe '#scan_results' do + let(:page) { '1' } + let(:limit) { '25' } + + subject { scanner_client.scan_results(page, limit) } + + it 'requests user notifications with specified query' do + stub_scanner_request(:get, '/scan_results', query: "repository_id=#{repository_id}&page=#{page}&limit=#{limit}", auth_key: auth_key) + .to_return(body: JSON.dump(scanner_scan_results_response(1))) + expect(subject).to be_a(Travis::API::V3::Models::ScannerCollection) + expect(subject.map { |e| e }.first).to be_a(Travis::API::V3::Models::ScanResult) + expect(subject.map { |e| e }.size).to eq(scanner_scan_results_response(1)['scan_results'].size) + expect(subject.count).to eq(scanner_scan_results_response(1)['total_count']) + end + end + + describe '#get_scan_result' do + let(:scan_result_id) { rand(999) } + + subject { scanner_client.get_scan_result(scan_result_id) } + + it 'requests the creation and returns the representation' do + stubbed_request = stub_scanner_request(:get, "/scan_results/#{scan_result_id}", auth_key: auth_key) + .to_return(status: 201, body: JSON.dump(scanner_scan_result_response(1, 1))) + + expect(subject).to be_a(Travis::API::V3::Models::ScanResult) + expect(subject.issues_found).to eq(1) + expect(stubbed_request).to have_been_made + end + end + + describe 'error handling' do + let(:scan_result_id) { rand(999) } + + subject { scanner_client.get_scan_result(scan_result_id) } + + it 'returns true when 202' do + stubbed_request = stub_scanner_request(:get, "/scan_results/#{scan_result_id}", auth_key: auth_key) + .to_return(status: 202) + + expect(subject).to be_truthy + expect(stubbed_request).to have_been_made + end + + it 'returns true when 204' do + stubbed_request = stub_scanner_request(:get, "/scan_results/#{scan_result_id}", auth_key: auth_key) + .to_return(status: 204) + + expect(subject).to be_truthy + expect(stubbed_request).to have_been_made + end + + it 'raises error when 400' do + stubbed_request = stub_scanner_request(:get, "/scan_results/#{scan_result_id}", auth_key: auth_key) + .to_return(status: 400, body: JSON.dump({error: 'error text'})) + + expect { subject }.to raise_error(Travis::API::V3::ClientError) + end + + it 'raises error when 403' do + stubbed_request = stub_scanner_request(:get, "/scan_results/#{scan_result_id}", auth_key: auth_key) + .to_return(status: 403, body: JSON.dump({rejection_code: 'error text'})) + + expect { subject }.to raise_error(Travis::API::V3::InsufficientAccess) + end + + it 'raises error when 404' do + stubbed_request = stub_scanner_request(:get, "/scan_results/#{scan_result_id}", auth_key: auth_key) + .to_return(status: 404, body: JSON.dump({error: 'error text'})) + + expect { subject }.to raise_error(Travis::API::V3::NotFound) + end + + it 'raises error when 422' do + stubbed_request = stub_scanner_request(:get, "/scan_results/#{scan_result_id}", auth_key: auth_key) + .to_return(status: 422, body: JSON.dump({error: 'error text'})) + + expect { subject }.to raise_error(Travis::API::V3::UnprocessableEntity) + end + + it 'raises error when 500' do + stubbed_request = stub_scanner_request(:get, "/scan_results/#{scan_result_id}", auth_key: auth_key) + .to_return(status: 500) + + expect { subject }.to raise_error(Travis::API::V3::ServerError) + end + end +end diff --git a/spec/v3/services/owner/find_spec.rb b/spec/v3/services/owner/find_spec.rb index ffc531639d..720f19afa6 100644 --- a/spec/v3/services/owner/find_spec.rb +++ b/spec/v3/services/owner/find_spec.rb @@ -28,7 +28,8 @@ "@type" => "allowance", "@representation" => "minimal", "id" => org.id - } + }, + "custom_keys" => [] }} end @@ -54,7 +55,8 @@ "@type" => "allowance", "@representation" => "minimal", "id" => org.id - } + }, + "custom_keys" => [] }} end @@ -86,6 +88,7 @@ "@representation" => "minimal", "id" => org.id }, + "custom_keys" => [], "repositories" => [{ "@type" => "repository", "@href" => "/v3/repo/#{repo.id}", @@ -102,6 +105,7 @@ "create_env_var" => false, "create_key_pair" => false, "delete_key_pair" => false, + "check_scan_results" => false, "admin" => false }, "id" => repo.id, @@ -118,6 +122,7 @@ "private" => false, "server_type" => 'git', "shared" => false, + "scan_failed_at" => nil, "owner" => { "@href"=> "/v3/org/#{org.id}" }, "default_branch" => { "@type" => "branch", @@ -162,6 +167,7 @@ "@representation" => "minimal", "id" => org.id }, + "custom_keys" => [], "repositories" => [{ "@type" => "repository", "@href" => "/v3/repo/#{repo.id}", @@ -178,6 +184,7 @@ "create_env_var" => false, "create_key_pair" => false, "delete_key_pair" => false, + "check_scan_results" => false, "admin" => false }, "id" => repo.id, @@ -194,6 +201,7 @@ "private" => false, "server_type" => 'git', "shared" => false, + "scan_failed_at" => nil, "owner" => { "@href"=> "/v3/org/#{org.id}" }, "default_branch" => { "@type" => "branch", @@ -232,7 +240,8 @@ "@type" => "allowance", "@representation" => "minimal", "id" => org.id - } + }, + "custom_keys" => [] }} end @@ -263,6 +272,7 @@ "@representation" => "minimal", "id" => org.id }, + "custom_keys" => [], "@warnings" => [{ "@type" => "warning", "message" => "query parameter organization.id not safelisted, ignored", @@ -302,6 +312,7 @@ "@representation" => "minimal", "id" => user.id }, + "custom_keys" => [], "recently_signed_up"=>false, "secure_user_hash" => nil, "ro_mode" => false, @@ -334,6 +345,7 @@ "@representation" => "minimal", "id" => user.id }, + "custom_keys" => [], "recently_signed_up"=>false, "secure_user_hash" => nil, "ro_mode" => false, @@ -366,6 +378,7 @@ "@representation" => "minimal", "id" => user.id }, + "custom_keys" => [], "recently_signed_up"=>false, "secure_user_hash" => nil, "ro_mode" => false, @@ -402,6 +415,7 @@ "@representation" => "minimal", "id" => user.id }, + "custom_keys" => [], "recently_signed_up"=>false, "secure_user_hash" => nil, "ro_mode" => false, diff --git a/spec/v3/services/repositories/for_current_user_spec.rb b/spec/v3/services/repositories/for_current_user_spec.rb index fbf9b9c504..6dcbced394 100644 --- a/spec/v3/services/repositories/for_current_user_spec.rb +++ b/spec/v3/services/repositories/for_current_user_spec.rb @@ -49,6 +49,7 @@ "create_env_var" => true, "create_key_pair" => true, "delete_key_pair" => true, + "check_scan_results" => true, "admin" => true }, "id" => repo.id, @@ -65,6 +66,7 @@ "private" => true, "server_type" => 'git', "shared" => false, + "scan_failed_at" => nil, "owner" => { "@type" => "user", "@href" => "/v3/user/#{repo.owner_id}", diff --git a/spec/v3/services/repositories/for_owner_spec.rb b/spec/v3/services/repositories/for_owner_spec.rb index e113017806..89d286a84b 100644 --- a/spec/v3/services/repositories/for_owner_spec.rb +++ b/spec/v3/services/repositories/for_owner_spec.rb @@ -76,6 +76,7 @@ "create_env_var" => false, "create_key_pair" => false, "delete_key_pair" => false, + "check_scan_results" => false, "admin" => false }, "id" => repo.id, @@ -92,6 +93,7 @@ "private" => true, "server_type" => 'git', "shared" => false, + "scan_failed_at" => nil, "owner" => { "@type" => "user", "id" => repo.owner_id, @@ -132,6 +134,7 @@ "create_env_var" =>false, "create_key_pair" =>false, "delete_key_pair" =>false, + "check_scan_results" => false, "create_request" =>false}, "id" =>repo.id, "name" =>"minimal", @@ -147,6 +150,7 @@ "private" =>true, "server_type" => 'git', "shared" =>false, + "scan_failed_at" => nil, "owner" =>{ "@type" =>"user", "id" =>1, @@ -249,6 +253,7 @@ "create_env_var" => false, "create_key_pair" => false, "delete_key_pair" => false, + "check_scan_results" => false, "create_request" => false}, "id" => repo.id, "name" => "minimal", @@ -264,6 +269,7 @@ "private" => true, "server_type" => 'git', "shared" => false, + "scan_failed_at" => nil, "owner" => { "@type" => "user", "id" => 1, @@ -411,6 +417,7 @@ "create_env_var" => false, "create_key_pair"=> false, "delete_key_pair"=> false, + "check_scan_results" => false, "create_request"=> false }, "id" => 1, @@ -426,6 +433,7 @@ "active" => true, "private" => true, "server_type" => 'git', + "scan_failed_at" => nil, "owner" => { "@type" => "user", "id" => 1, @@ -459,6 +467,7 @@ "create_env_var" => false, "create_key_pair" => false, "delete_key_pair" => false, + "check_scan_results" => false, "create_request"=> false }, "id" => repo2.id, @@ -475,6 +484,7 @@ "private" => false, "server_type" => 'git', "shared" => false, + "scan_failed_at" => nil, "owner" => { "@type" => "user", "id" => 1, @@ -533,6 +543,7 @@ "create_env_var" => true, "create_key_pair" => true, "delete_key_pair" => true, + "check_scan_results" => true, "admin" => false }, "id" => sharedrepo.id, @@ -549,6 +560,7 @@ "private" => false, "server_type" => 'git', "shared" => true, + "scan_failed_at" => nil, "owner" => { "@type" => "user", "id" => sharedrepo.owner_id, diff --git a/spec/v3/services/repository/find_spec.rb b/spec/v3/services/repository/find_spec.rb index 5af584539f..9ee04d1727 100644 --- a/spec/v3/services/repository/find_spec.rb +++ b/spec/v3/services/repository/find_spec.rb @@ -19,6 +19,7 @@ "create_env_var" => true, "create_key_pair" => true, "delete_key_pair" => true, + "check_scan_results" => true, "admin" => true }, full_access: { @@ -33,6 +34,7 @@ "create_env_var" => true, "create_key_pair" => true, "delete_key_pair" => true, + "check_scan_results" => true, "admin" => false }, read_and_star: { @@ -47,6 +49,7 @@ "create_env_var" => false, "create_key_pair" => false, "delete_key_pair" => false, + "check_scan_results" => false, "admin" => false }, read: { @@ -61,6 +64,7 @@ "create_env_var" => false, "create_key_pair" => false, "delete_key_pair" => false, + "check_scan_results" => false, "admin" => false } } @@ -87,6 +91,7 @@ "private" => opts[:private], "server_type" => 'git', "shared" => false, + "scan_failed_at" => nil, "owner" => { "id" => repo.owner_id, "login" => "svenfuchs", diff --git a/spec/v3/services/scan_result/find_spec.rb b/spec/v3/services/scan_result/find_spec.rb new file mode 100644 index 0000000000..69ebfcfcab --- /dev/null +++ b/spec/v3/services/scan_result/find_spec.rb @@ -0,0 +1,98 @@ +describe Travis::API::V3::Services::ScanResult::Find, set_app: true, scanner_spec_helper: true do + let(:parsed_body) { JSON.load(last_response.body) } + let(:scanner_url) { 'http://scanner.travis-ci.com' } + let(:scanner_auth_key) { 'secret' } + let(:scan_result_id) { 1 } + + before do + Travis.config.scanner.url = scanner_url + Travis.config.scanner.token = scanner_auth_key + end + + context 'unauthenticated' do + it 'responds 403' do + get('/v3/scan_results') + + expect(last_response.status).to eq(403) + end + end + + context 'authenticated' do + let(:user) { FactoryBot.create(:user) } + let(:repository) { FactoryBot.create(:repository) } + let(:job) { FactoryBot.create(:job) } + let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } + let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }} + let(:offset) { '0' } + let(:limit) { '25' } + let(:expected_json) do + { + "@type"=>"scan_result", + "@representation"=>"standard", + "id"=>1, + "created_at"=>"2022-10-20T08:55:20.522Z", + "formatted_content"=> + "travis_fold:start:trivy\r\e[0K\e[33;1mIn line 1 of your build job log trivy found\e[0m\n" + + "AWS Access Key ID\n" + + "travis_fold:end:trivy\n" + + "\n" + + "\n" + + "travis_fold:start:detect_secrets\r\e[0K\e[33;1mIn line 1 of your build job log detect_secrets found\e[0m\n" + + "AWS Access Key\n" + + "travis_fold:end:detect_secrets\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "Our backend build job log monitoring uses:\n" + + " \u2022 trivy\n" + + " \u2022 detect_secrets\n" + + "Called via command line and under respective permissive licenses.", + "issues_found"=>1, + "job_id"=>job.id, + "build_id"=>nil, + "job_number"=>"1", + "build_number"=>"1", + "job_finished_at"=>nil, + "commit_sha"=>"SHA", + "commit_compare_url"=>nil, + "commit_branch"=>nil, + "build_created_by"=>nil + } + end + + before do + stubbed_request = stub_scanner_request(:get, "/scan_results/#{scan_result_id}", auth_key: scanner_auth_key) + .to_return(status: 201, body: JSON.dump(scanner_scan_result_response(job.id, repository.id))) + end + + context 'with push access to repository' do + before { repository.permissions.create(user: user, push: true) } + + it 'responds with list of plugins' do + get("/v3/scan_result/#{scan_result_id}", {}, headers) + expect(last_response.status).to eq(200) + expect(parsed_body).to eql_json(expected_json) + end + end + + context 'without push access to repository' do + before { repository.permissions.create(user: user, push: false) } + + it 'responds with list of plugins' do + get("/v3/scan_result/#{scan_result_id}", {}, headers) + expect(last_response.status).to eq(403) + expect(JSON.load(body).to_s).to include( + "@type", + "error_type", + "insufficient_access", + "error_message", + "operation requires check_scan_results access to repository", + "resource_type", + "repository", + "permission", + "check_scan_results") + end + end + end +end diff --git a/spec/v3/services/scan_results/all_spec.rb b/spec/v3/services/scan_results/all_spec.rb new file mode 100644 index 0000000000..551dd50bee --- /dev/null +++ b/spec/v3/services/scan_results/all_spec.rb @@ -0,0 +1,124 @@ +describe Travis::API::V3::Services::ScanResults::All, set_app: true, scanner_spec_helper: true do + let(:parsed_body) { JSON.load(last_response.body) } + let(:scanner_url) { 'http://scanner.travis-ci.com' } + let(:scanner_auth_key) { 'secret' } + + before do + Travis.config.scanner.url = scanner_url + Travis.config.scanner.token = scanner_auth_key + end + + context 'unauthenticated' do + it 'responds 403' do + get('/v3/scan_results') + + expect(last_response.status).to eq(403) + end + end + + context 'authenticated' do + let(:user) { FactoryBot.create(:user) } + let(:repository) { FactoryBot.create(:repository) } + let(:job) { FactoryBot.create(:job) } + let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } + let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}" }} + let(:offset) { '0' } + let(:limit) { '25' } + let(:expected_json) do + { + "@type"=>"scan_results", + "@href"=>"/v3/scan_results?repository_id=4&offset=0&limit=25", + "@representation"=>"standard", + "@pagination"=> + { + "limit"=>25, + "offset"=>0, + "count"=>1, + "is_first"=>true, + "is_last"=>true, + "next"=>nil, + "prev"=>nil, + "first"=>{ + "@href"=>"/v3/scan_results?repository_id=4&offset=0&limit=25", + "offset"=>0, + "limit"=>25 + }, + "last"=>{ + "@href"=>"/v3/scan_results?repository_id=4&offset=0&limit=25", + "offset"=>0, + "limit"=>25 + } + }, + "scan_results"=>[ + { + "@type"=>"scan_result", + "@representation"=>"standard", + "id"=>1, + "created_at"=>"2022-10-20T08:55:20.522Z", + "formatted_content"=> + "travis_fold:start:trivy\r\e[0K\e[33;1mIn line 1 of your build job log trivy found\e[0m\n" + + "AWS Access Key ID\n" + + "travis_fold:end:trivy\n" + + "\n" + + "\n" + + "travis_fold:start:detect_secrets\r\e[0K\e[33;1mIn line 1 of your build job log detect_secrets found\e[0m\n" + + "AWS Access Key\n" + + "travis_fold:end:detect_secrets\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "Our backend build job log monitoring uses:\n" + + " \u2022 trivy\n" + + " \u2022 detect_secrets\n" + + "Called via command line and under respective permissive licenses.", + "issues_found"=>1, + "job_id"=>job.id, + "build_id"=>nil, + "job_number"=>"1", + "build_number"=>"1", + "job_finished_at"=>nil, + "commit_sha"=>"SHA", + "commit_compare_url"=>nil, + "commit_branch"=>nil, + "build_created_by"=>nil + } + ] + } + end + + before do + stub_scanner_request(:get, '/scan_results', query: "repository_id=#{repository.id}&page=#{(offset.to_i / limit.to_i) + 1}&limit=#{limit}", auth_key: scanner_auth_key) + .to_return(body: JSON.dump(scanner_scan_results_response(job.id))) + end + + context 'with push access to repository' do + before { repository.permissions.create(user: user, push: true) } + + it 'responds with list of plugins' do + get('/v3/scan_results', { repository_id: repository.id, offset: offset, limit: limit }, headers) + expect(last_response.status).to eq(200) + expect(parsed_body).to eql_json(expected_json) + end + end + + context 'without push access to repository' do + before { repository.permissions.create(user: user, push: false) } + + it 'responds with list of plugins' do + get('/v3/scan_results', { repository_id: repository.id, offset: offset, limit: limit }, headers) + expect(last_response.status).to eq(403) + expect(JSON.load(body).to_s).to include( + "@type", + "error_type", + "insufficient_access", + "error_message", + "operation requires check_scan_results access to repository", + "resource_type", + "repository", + "permission", + "check_scan_results") + end + end + end +end