diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c7860b9774d..c63eb7d7abf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,11 +6,16 @@ Dangerfile @department-of-veterans-affairs/backend-review-group Dockerfile @department-of-veterans-affairs/backend-review-group Dockerfile-k8s @department-of-veterans-affairs/backend-review-group -docker-compose* @department-of-veterans-affairs/backend-review-group +docker-compose.yml @department-of-veterans-affairs/backend-review-group +docker-compose-clamav.yml @department-of-veterans-affairs/backend-review-group +docker-compose-deps.yml @department-of-veterans-affairs/backend-review-group +docker-compose.review.yml @department-of-veterans-affairs/backend-review-group +docker-compose.test.yml @department-of-veterans-affairs/backend-review-group Gemfile @department-of-veterans-affairs/backend-review-group Gemfile.lock @department-of-veterans-affairs/backend-review-group Jenkinsfile @department-of-veterans-affairs/backend-review-group Makefile @department-of-veterans-affairs/backend-review-group +Procfile @department-of-veterans-affairs/backend-review-group .devcontainer @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/cto-engineers app/controllers/appeals_base_controller.rb @department-of-veterans-affairs/backend-review-group app/controllers/appeals_base_controller_v1.rb @department-of-veterans-affairs/backend-review-group @@ -640,13 +645,13 @@ app/sidekiq/vbms @department-of-veterans-affairs/benefits-dependents-management app/sidekiq/vre/create_ch31_submissions_report_job.rb @department-of-veterans-affairs/benefits-non-disability @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/sidekiq/vre/submit1900_job.rb @department-of-veterans-affairs/Benefits-Team-1 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group app/sidekiq/webhooks @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -bin/fake_clamdscan @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group bin/git_blame @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group bin/rails @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group bin/rake @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group bin/rspec @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group bin/setup @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group bin/sidekiq_quiet @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +clamav_tmp @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group config/application.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group config/betamocks @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group config/betamocks/services_config.yml @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -707,7 +712,7 @@ config/initializers/backtrace_silencers.rb @department-of-veterans-affairs/va-ap config/initializers/betamocks.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group config/initializers/bgs.rb @department-of-veterans-affairs/Benefits-Team-1 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group config/initializers/breakers.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group -config/initializers/clamscan.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +config/initializers/clamav.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group config/initializers/config.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group config/initializers/cookie_rotation.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group config/initializers/covid_vaccine_facilities.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/long-covid @@ -804,6 +809,7 @@ lib/caseflow @department-of-veterans-affairs/lighthouse-banana-peels @department lib/central_mail @department-of-veterans-affairs/lighthouse-banana-peels @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/chip @department-of-veterans-affairs/vsa-healthcare-health-quest-1-backend @department-of-veterans-affairs/patient-check-in @department-of-veterans-affairs/backend-review-group lib/claim_letters @department-of-veterans-affairs/benefits-management-tools-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +lib/clamav @department-of-veterans-affairs/backend-review-group lib/common/client/base.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/common/client/concerns/mhv_fhir_session_client.rb @department-of-veterans-affairs/vfs-mhv-medical-records @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/common/client/concerns/mhv_jwt_session_client.rb @department-of-veterans-affairs/vfs-mhv-medical-records @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -815,6 +821,7 @@ lib/common/client/middleware/request/remove_cookies.rb @department-of-veterans-a lib/common/client/middleware/response/soap_parser.rb @department-of-veterans-affairs/backend-review-group lib/common/exceptions/open_id_service_error.rb @department-of-veterans-affairs/lighthouse-pivot lib/common/file_helpers.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +lib/common/virus_scan.rb @department-of-veterans-affairs/backend-review-group lib/debt_management_center @department-of-veterans-affairs/vsa-debt-resolution @department-of-veterans-affairs/backend-review-group lib/decision_review @department-of-veterans-affairs/Benefits-Team-1 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/decision_review_v1 @department-of-veterans-affairs/Benefits-Team-1 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @@ -921,6 +928,7 @@ lib/search @department-of-veterans-affairs/va-api-engineers @department-of-veter lib/sentry @department-of-veterans-affairs/backend-review-group lib/sentry_logging.rb @department-of-veterans-affairs/backend-review-group lib/sftp_writer @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers +lib/shrine @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers lib/sidekiq/attr_package.rb @department-of-veterans-affairs/octo-identity @department-of-veterans-affairs/backend-review-group lib/sidekiq/error_tag.rb @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers lib/sidekiq/form526_backup_submission_process @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/va-api-engineers @@ -1388,6 +1396,7 @@ spec/lib/sentry @department-of-veterans-affairs/va-api-engineers @department-of- spec/lib/sftp_writer @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/sftp_writer/factory_spec.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/sftp_writer/remote_spec.rb @department-of-veterans-affairs/backend-review-group +spec/lib/shrine @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/sidekiq/attr_package_spec.rb @department-of-veterans-affairs/octo-identity @department-of-veterans-affairs/backend-review-group spec/lib/sidekiq/error_tag_spec.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group spec/lib/sidekiq/form526_backup_submission_process @department-of-veterans-affairs/Disability-Experience @department-of-veterans-affairs/dbex-trex @department-of-veterans-affairs/benefits-disability-2 @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group diff --git a/.github/workflows/audit_service_tags.yml b/.github/workflows/audit_service_tags.yml index b81aad522b3..9391b69f875 100644 --- a/.github/workflows/audit_service_tags.yml +++ b/.github/workflows/audit_service_tags.yml @@ -36,10 +36,9 @@ jobs: uses: docker/build-push-action@v5 with: build-args: | - sidekiq_license=${{ env.BUNDLE_ENTERPRISE__CONTRIBSYS__COM }} - userid=${{ env.VETS_API_USER_ID }} + BUNDLE_ENTERPRISE__CONTRIBSYS__COM=${{ env.BUNDLE_ENTERPRISE__CONTRIBSYS__COM }} + USER_ID=${{ env.VETS_API_USER_ID }} context: . - target: builder push: false load: true tags: vets-api @@ -48,8 +47,8 @@ jobs: - name: Setup Database run: | - docker-compose -f docker-compose.test.yml run vets-api bash \ - -c "CI=true RAILS_ENV=test DISABLE_BOOTSNAP=true parallel_test -n 13 -e 'bin/rails db:reset'" + docker-compose -f docker-compose.test.yml run web bash \ + -c "CI=true RAILS_ENV=test DISABLE_BOOTSNAP=true bundle exec parallel_test -n 13 -e 'bin/rails db:reset'" - name: Get changed files run: | @@ -60,6 +59,6 @@ jobs: - name: Run service tags audit controllers task run: | - docker-compose -f docker-compose.test.yml run -e CHANGED_FILES=${{ env.CHANGED_FILES }} vets-api bash \ + docker-compose -f docker-compose.test.yml run -e CHANGED_FILES=${{ env.CHANGED_FILES }} web bash \ -c "CI=true DISABLE_BOOTSNAP=true bundle exec rake service_tags:audit_controllers_ci" diff --git a/.gitignore b/.gitignore index b8d480faeb8..38c53974184 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,7 @@ node_modules # Ignore public folder (used for local document uploads) public +# Ignore any files within clamav_tmp + +clamav_tmp/* +!/clamav_tmp/.keep diff --git a/Dockerfile b/Dockerfile index e5795637709..2d403bc889e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.2.4-slim-bullseye AS rubyimg +FROM ruby:3.2.4-slim-bookworm AS rubyimg FROM rubyimg AS modules WORKDIR /tmp @@ -23,10 +23,9 @@ RUN groupadd --gid $USER_ID nonroot \ WORKDIR /app -RUN echo "deb http://ftp.debian.org/debian testing main contrib non-free" >> /etc/apt/sources.list -RUN apt-get update -RUN apt-get install -y -t testing poppler-utils build-essential libpq-dev git curl wget ca-certificates-java file -RUN dpkg --configure -a && apt-get install -y -t bullseye imagemagick pdftk \ +RUN apt-get update --fix-missing +RUN apt-get install -y poppler-utils build-essential libpq-dev git curl wget ca-certificates-java file \ + imagemagick pdftk \ && apt-get clean \ && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* /tmp/* /var/tmp/* @@ -69,4 +68,4 @@ EXPOSE 3000 USER nonroot -CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"] \ No newline at end of file +CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"] diff --git a/Gemfile b/Gemfile index f3a4bf79ce5..ce4bd571ea4 100644 --- a/Gemfile +++ b/Gemfile @@ -55,7 +55,7 @@ gem 'bootsnap', require: false gem 'breakers' gem 'carrierwave' gem 'carrierwave-aws' -gem 'clam_scan' +gem 'clamav-client', require: 'clamav/client' gem 'combine_pdf' gem 'config' gem 'connect_vbms', git: 'https://github.com/adhocteam/connect_vbms', tag: 'v2.0.0.rc', require: 'vbms' diff --git a/Gemfile.lock b/Gemfile.lock index 465033cee09..e3cf8f240a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -301,7 +301,7 @@ GEM cork nap open4 (~> 1.3) - clam_scan (0.0.2) + clamav-client (3.2.0) cliver (0.3.2) coderay (1.1.3) coercible (1.0.0) @@ -596,9 +596,12 @@ GEM kramdown (~> 2.0) language_server-protocol (3.17.0.3) libdatadog (5.0.0.1.0) + libdatadog (5.0.0.1.0-aarch64-linux) libdatadog (5.0.0.1.0-x86_64-linux) libddwaf (1.14.0.0.0) ffi (~> 1.0) + libddwaf (1.14.0.0.0-aarch64-linux) + ffi (~> 1.0) libddwaf (1.14.0.0.0-java) ffi (~> 1.0) libddwaf (1.14.0.0.0-x86_64-linux) @@ -1076,6 +1079,7 @@ GEM zeitwerk (2.6.13) PLATFORMS + aarch64-linux java ruby x64-mingw32 @@ -1111,7 +1115,7 @@ DEPENDENCIES carrierwave-aws check_in! claims_api! - clam_scan + clamav-client combine_pdf config connect_vbms! diff --git a/Makefile b/Makefile index 16c7d952307..cac52538ac9 100644 --- a/Makefile +++ b/Makefile @@ -7,16 +7,9 @@ else ENV_ARG := dev endif -ifdef clam - FOREMAN_ARG := all=1 -else - FOREMAN_ARG := all=1,clamd=0,freshclam=0 -endif - - COMPOSE_DEV := docker-compose COMPOSE_TEST := docker-compose -f docker-compose.test.yml -BASH := run --rm --service-ports vets-api bash +BASH := run --rm --service-ports web bash BASH_DEV := $(COMPOSE_DEV) $(BASH) -c BASH_TEST := $(COMPOSE_TEST) $(BASH) --login -c SPEC_PATH := spec/ modules/ @@ -117,9 +110,9 @@ spec: ## Runs spec tests .PHONY: spec_parallel_setup spec_parallel_setup: ## Setup the parallel test dbs. This resets the current test db, as well as the parallel test dbs ifeq ($(ENV_ARG), dev) - @$(BASH_DEV) "RAILS_ENV=test DISABLE_BOOTSNAP=true parallel_test -e 'bundle exec rake db:reset'" + @$(BASH_DEV) "RAILS_ENV=test DISABLE_BOOTSNAP=true bundle exec parallel_test -e 'bundle exec rake db:reset db:migrate'" else - @$(COMPOSE_TEST) $(BASH) -c "RAILS_ENV=test DISABLE_BOOTSNAP=true parallel_test -e 'bundle exec rake db:reset'" + @$(COMPOSE_TEST) $(BASH) -c "RAILS_ENV=test DISABLE_BOOTSNAP=true parallel_test -e 'bundle exec rake db:reset db:migrate'" endif .PHONY: spec_parallel @@ -131,14 +124,14 @@ else endif .PHONY: up -up: db ## Starts the server and associated services with docker-compose, use `clam=1 make up` to run ClamAV - @$(BASH_DEV) "rm -f tmp/pids/server.pid && foreman start -m ${FOREMAN_ARG}" +up: db ## Starts the server and associated services with docker-compose + @$(BASH_DEV) "rm -f tmp/pids/server.pid && foreman start -m all=1" # NATIVE COMMANDS .PHONY: native-up native-up: bundle install - foreman start -m ${FOREMAN_ARG} + foreman start -m all=1 .PHONY: native-lint native-lint: diff --git a/Procfile b/Procfile index 074c2758193..9af216e5db4 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,2 @@ web: bundle exec puma -p 3000 -C ./config/puma.rb job: bundle exec sidekiq -q critical,4 -q tasker,3 -q default,2 -q low,1 -freshclam: /usr/bin/freshclam -d --config-file=config/freshclam.conf -clamd: /usr/sbin/clamd -c config/clamd.conf diff --git a/app/uploaders/uploader_virus_scan.rb b/app/uploaders/uploader_virus_scan.rb index 4bd293a83d7..846adc6d175 100644 --- a/app/uploaders/uploader_virus_scan.rb +++ b/app/uploaders/uploader_virus_scan.rb @@ -16,13 +16,14 @@ class VirusFoundError < StandardError def validate_virus_free(file) return unless Rails.env.production? - temp_file_path = Common::FileHelpers.generate_temp_file(file.read) + temp_file_path = Common::FileHelpers.generate_clamav_temp_file(file.read) result = Common::VirusScan.scan(temp_file_path) File.delete(temp_file_path) - unless result.safe? + # Common::VirusScan result will return true or false + unless result # unless safe file.delete - raise VirusFoundError, result.body + raise VirusFoundError, "Virus Found + #{temp_file_path}" end end end diff --git a/bin/fake_clamdscan b/bin/fake_clamdscan deleted file mode 100755 index 63a31f0b72c..00000000000 --- a/bin/fake_clamdscan +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -# frozen_string_literal: true - -echo 'Fake scanning file with fake_clamdscan script' -exit 0 diff --git a/clamav_tmp/.keep b/clamav_tmp/.keep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/config/clamd.conf b/config/clamd.conf index 52595bda7b5..1c12245cf51 100755 --- a/config/clamd.conf +++ b/config/clamd.conf @@ -1,5 +1,7 @@ Foreground yes -DatabaseDirectory /srv/vets-api/clamav/database -LocalSocket /srv/vets-api/clamav/clamd.ctl TCPSocket 3310 -TCPAddr 127.0.0.1 \ No newline at end of file +TCPAddr 127.0.0.1 + +LogSyslog yes +LogVerbose yes +ExtendedDetectionInfo yes diff --git a/config/freshclam.conf b/config/freshclam.conf index fa18e68d88b..eae285e35fa 100644 --- a/config/freshclam.conf +++ b/config/freshclam.conf @@ -1,7 +1,7 @@ Foreground yes -PidFile /srv/vets-api/clamav/freshclam.pid +PidFile /app/clamav/freshclam.pid Checks 8 -DatabaseDirectory /srv/vets-api/clamav/database +DatabaseDirectory /app/clamav/database PrivateMirror dsva-vetsgov-utility-clamav.s3-us-gov-west-1.amazonaws.com -NotifyClamd /srv/vets-api/src/config/clamd.conf +NotifyClamd /app/config/clamd.conf ReceiveTimeout 600 diff --git a/config/initializers/clamav.rb b/config/initializers/clamav.rb new file mode 100644 index 00000000000..289587cb204 --- /dev/null +++ b/config/initializers/clamav.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +if Rails.env.development? + # If running ClamAV through container + # Update host and port on settings.local.yml to override the socket + ENV['CLAMD_TCP_HOST'] = Settings.clamav.host + ENV['CLAMD_TCP_PORT'] = Settings.clamav.port + + # If running ClamAV natively (via daemon) + # Update host and port on settings.local.yml to override the tcp connection + ENV['CLAMD_UNIX_SOCKET'] = '/usr/local/etc/clamav/clamd.sock' +end diff --git a/config/initializers/clamscan.rb b/config/initializers/clamscan.rb deleted file mode 100644 index 2f864ddfb9a..00000000000 --- a/config/initializers/clamscan.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -ClamScan.configure do |clam_config| - clam_config.client_location = Settings.binaries.clamdscan -end diff --git a/config/settings.local.yml.example b/config/settings.local.yml.example index ad55d272e4a..c9db66102e6 100644 --- a/config/settings.local.yml.example +++ b/config/settings.local.yml.example @@ -6,11 +6,11 @@ # The relative path to department-of-veterans-affairs/vets-api-mockdata # cache_dir: ../vets-api-mockdata -# binaries: - # For NATIVE and DOCKER installation +# clamav: # A "virus scanner" that always returns success for development purposes - # NOTE: You may need to specify a full path instead of a relative path - # clamdscan: ./bin/fake_clamdscan + # mock: true + # host: '0.0.0.0' + # port: '33100' # NOTE: This file is excluded by railsconfig in the test env. # Use config/settings/test.local.yml instead. diff --git a/config/settings.yml b/config/settings.yml index c1cd8bdb74f..e27296534ed 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -97,6 +97,13 @@ binaries: pdftk: pdftk clamdscan: /usr/bin/clamdscan +clamav: + mock: false + # host & port here are only used in development here: + # config/initializers/clamav.rb + host: 'clamav' + port: '3310' + db_encryption_key: f01ff8ebd1a2b053ad697ae1f0d86adb database_url: postgis:///vets-api @@ -914,7 +921,7 @@ vetext: hqva_mobile: url: "https://veteran.apps.va.gov" mock: false - key_path: /srv/vets-api/secret/health-quest.key + key_path: /app/secret/health-quest.key development_key_path: modules/health_quest/config/rsa/sandbox_rsa timeout: 15 facilities: @@ -937,7 +944,7 @@ hqva_mobile: health_api: "health_api" pgd_api: "pgd_api" mock: false - key_path: /srv/vets-api/secret/health-quest.lighthouse.key + key_path: /app/secret/health-quest.lighthouse.key pgd_api_scopes: - "launch launch/patient" - "patient/Observation.read" @@ -1034,7 +1041,7 @@ lighthouse: aud_claim_url: "https://deptva-eval.okta.com/oauth2/aus8nm1q0f7VQ0a482p7/v1/token" client_id: "0oaaxkp0aeXEJkMFw2p7" grant_type: "client_credentials" - api_key: /srv/vets-api/secret/lighthouse_fast_track_api.key + api_key: /app/secret/lighthouse_fast_track_api.key facilities: url: https://sandbox-api.va.gov api_key: fake_key @@ -1071,7 +1078,7 @@ vbms: cert: vetsapi.client.vbms.aide.oit.va.gov.crt client_keyfile: vetsapi.client.vbms.aide.oit.va.gov.p12 server_cert: vbms.aide.oit.va.gov.crt - environment_directory: /srv/vets-api/secret + environment_directory: /app/secret env: test vet_verification: diff --git a/docker-compose-clamav.yml b/docker-compose-clamav.yml new file mode 100644 index 00000000000..239aa2936a3 --- /dev/null +++ b/docker-compose-clamav.yml @@ -0,0 +1,10 @@ +version: '3.4' +services: + clamav: + volumes: + - shared-vol:/vets-api + image: clamav/clamav + ports: + - 33100:3310 +volumes: + shared-vol: diff --git a/docker-compose-deps.yml b/docker-compose-deps.yml index b1ab99a4d31..489576324b8 100644 --- a/docker-compose-deps.yml +++ b/docker-compose-deps.yml @@ -13,4 +13,11 @@ services: - ./data:/var/lib/postgresql/data:cached ports: - "54320:5432" - + clamav: + volumes: + - shared-vol:/vets-api + image: clamav/clamav + ports: + - 33100:3310 +volumes: + shared-vol: diff --git a/docker-compose.review.yml b/docker-compose.review.yml index 43769083ab7..e831bb480b1 100644 --- a/docker-compose.review.yml +++ b/docker-compose.review.yml @@ -1,55 +1,70 @@ version: '3.4' + +x-app: &common + build: + args: + BUNDLE_ENTERPRISE__CONTRIBSYS__COM: "${BUNDLE_ENTERPRISE__CONTRIBSYS__COM}" + USER_ID: ${VETS_API_USER_ID} + context: . + environment: + RAILS_ENV: development + BUNDLE_ENTERPRISE__CONTRIBSYS__COM: "${BUNDLE_ENTERPRISE__CONTRIBSYS__COM}" + "Settings.database_url": "postgis://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-password}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DATABASE:-vets_api_development}?pool=4" + "Settings.test_database_url": "postgis://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-password}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DATABASE:-vets_api_test}" + "Settings.redis.app_data.url": "redis://redis:6379" + "Settings.redis.sidekiq.url": "redis://redis:6379" + "Settings.redis.rails_cache.url": "redis://redis:6379" + "Settings.saml_ssoe.idp_metadata_file": "config/ssoe_idp_sqa_metadata_isam.xml" + "Settings.betamocks.cache_dir": "config/vets-api-mockdata" + image: vets-api:${DOCKER_IMAGE:-latest} + restart: unless-stopped + volumes: + - "../vets-api-mockdata:/cache" + - ../.secret:/app/secret:cached + - ../.pki:/app/pki:cached + - shared-vol:/tmp + working_dir: /app + depends_on: + - clamav + - postgres + - redis + links: + - clamav + - postgres + - redis + services: + clamav: + image: clamav/clamav + restart: unless-stopped + ports: + - 3310:3310 + volumes: + - shared-vol:/vets-api redis: image: redis:6.2-alpine restart: unless-stopped + ports: + - 6379:6379 postgres: - image: mdillon/postgis:11-alpine + command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c max_connections=200 environment: POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-password}" POSTGRES_USER: "${POSTGRES_USER:-postgres}" - volumes: - - db-data:/var/lib/postgresql/data:cached + PGDATA: /tmp + image: postgis/postgis:14-3.3-alpine ports: - - "54320:5432" - restart: unless-stopped - vets-api: - build: - context: . - target: development - args: - sidekiq_license: "${BUNDLE_ENTERPRISE__CONTRIBSYS__COM}" - userid: "${VETS_API_USER_ID}" - command: > - bash -c "bundle exec rake db:migrate || bundle exec rake db:setup db:migrate - && touch tmp/caching-dev.txt && foreman start -m all=1,clamd=0,freshclam=0" - image: "vets-api:${DOCKER_IMAGE:-latest}" + - 5432:5432 volumes: - - .:/srv/vets-api/src:cached - - dev_bundle:/usr/local/bundle - - ../.secret:/srv/vets-api/secret:cached - - ../.pki:/srv/vets-api/pki:cached + - ./data:/var/lib/postgresql/data:cached + web: + <<: *common + command: bash -c "bundle exec rake db:migrate || bundle exec rake db:reset db:migrate && bundle exec rails s -b 0.0.0.0" ports: - - "3000:3000" - environment: - "Settings.database_url": "postgis://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-password}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DATABASE:-vets_api_development}?pool=4" - "Settings.test_database_url": "postgis://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-password}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DATABASE:-vets_api_test}?pool=4" - "Settings.redis.app_data.url": "redis://redis:6379" - "Settings.redis.sidekiq.url": "redis://redis:6379" - "Settings.redis.rails_cache.url": "redis://redis:6379" - "Settings.binaries.clamdscan": "clamscan" # Not running a separate process within the container for clamdscan, so we use clamscan which requires no daemon - POSTGRES_HOST: "${POSTGRES_HOST:-postgres}" - POSTGRES_PORT: "${POSTGRES_PORT:-5432}" - POSTGRES_USER: "${POSTGRES_USER:-postgres}" - POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-password}" - PUMA_THREADS: "${PUMA_THREADS:-4}" - depends_on: - - postgres - - redis - links: - - postgres - - redis - restart: unless-stopped + - 3000:3000 + worker: + <<: *common + command: bundle exec sidekiq -q critical,4 -q tasker,3 -q default,2 -q low,1 + volumes: - db-data: - dev_bundle: + shared-vol: diff --git a/docker-compose.test.yml b/docker-compose.test.yml index a69d19449dc..a4eb3e15436 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -8,7 +8,7 @@ services: environment: POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-password}" POSTGRES_USER: "${POSTGRES_USER:-postgres}" - vets-api: + web: build: context: . target: development @@ -17,12 +17,11 @@ services: userid: "${VETS_API_USER_ID}" image: "vets-api:${DOCKER_IMAGE:-latest}" volumes: - - ".:/srv/vets-api/src" + - ".:/app" - test_bundle:/usr/local/bundle environment: "Settings.database_url": "postgis://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-password}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DATABASE:-vets_api_development}?pool=4" "Settings.test_database_url": "postgis://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-password}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DATABASE:-vets_api_test}" - "Settings.binaries.clamdscan": "clamscan" # Not running a separate process within the container for clamdscan, so we use clamscan which requires no daemon "Settings.redis.app_data.url": "redis://redis:6379" "Settings.redis.sidekiq.url": "redis://redis:6379" POSTGRES_HOST: "${POSTGRES_HOST:-postgres}" @@ -43,4 +42,4 @@ services: - postgres - redis volumes: - test_bundle: \ No newline at end of file + test_bundle: diff --git a/docker-compose.yml b/docker-compose.yml index a19def5b318..dcac6012677 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,4 +58,4 @@ services: command: bundle exec sidekiq -q critical,4 -q tasker,3 -q default,2 -q low,1 volumes: - shared-vol: \ No newline at end of file + shared-vol: diff --git a/lib/clamav/commands/patch_scan_command.rb b/lib/clamav/commands/patch_scan_command.rb new file mode 100644 index 00000000000..0a3586b9ff0 --- /dev/null +++ b/lib/clamav/commands/patch_scan_command.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +## Monkey Patch for the clamav SCAN COMMAND +## file path needs to be vets-api/ because of shared volumes + +# clamav-client - ClamAV client +# Copyright (C) 2014 Franck Verrot + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +require 'clamav/commands/command' + +module ClamAV + module Commands + class PatchScanCommand < Command + def initialize(path, path_finder = Util) + @path = path + @path_finder = path_finder + super() + end + + def call(conn) + @path_finder.path_to_files(@path).map { |file| scan_file(conn, file) } + end + + def scan_file(conn, file) + stripped_filename = file.gsub(%r{^clamav_tmp/}, '') # need to send the file + get_status_from_response(conn.send_request("SCAN /vets-api/#{stripped_filename}")) + end + end + end +end diff --git a/lib/clamav/patch_client.rb b/lib/clamav/patch_client.rb new file mode 100644 index 00000000000..ac36546e91c --- /dev/null +++ b/lib/clamav/patch_client.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# clamav-client - ClamAV client +# Copyright (C) 2014 Franck Verrot + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +require 'clamav/connection' +require 'clamav/commands/ping_command' +require 'clamav/commands/quit_command' +require 'clamav/commands/scan_command' +require 'clamav/commands/instream_command' +require 'clamav/util' +require 'clamav/wrappers/new_line_wrapper' +require 'clamav/wrappers/null_termination_wrapper' +require_relative 'commands/patch_scan_command' + +module ClamAV + class PatchClient + def initialize(connection = default_connection) + @connection = connection + connection.establish_connection + end + + def execute(command) + command.call(@connection) + end + + def default_connection + ClamAV::Connection.new( + socket: resolve_default_socket, + wrapper: ::ClamAV::Wrappers::NewLineWrapper.new + ) + end + + def resolve_default_socket + unix_socket, tcp_host, tcp_port = ENV.values_at('CLAMD_UNIX_SOCKET', 'CLAMD_TCP_HOST', 'CLAMD_TCP_PORT') + if tcp_host && tcp_port + ::TCPSocket.new(tcp_host, tcp_port) + else + ::UNIXSocket.new(unix_socket || '/var/run/clamav/clamd.ctl') + end + end + + def ping + execute Commands::PingCommand.new + end + + def safe?(target) + return instream(target).virus_name.nil? if target.is_a?(StringIO) + + scan(target).all? { |file| file.virus_name.nil? } + end + + private + + def instream(io) + execute Commands::InstreamCommand.new(io) + end + + def scan(file_path) + execute Commands::PatchScanCommand.new(file_path) + end + end +end diff --git a/lib/common/file_helpers.rb b/lib/common/file_helpers.rb index fb49eda26cc..3fb1ebe5e75 100644 --- a/lib/common/file_helpers.rb +++ b/lib/common/file_helpers.rb @@ -22,5 +22,22 @@ def generate_temp_file(file_body, file_name = nil) file_path end + + def generate_clamav_temp_file(file_body, file_name = nil) + file_name = SecureRandom.hex if file_name.nil? + clamav_directory = Rails.root.join('clamav_tmp') + unless Dir.exist?(clamav_directory) + # Create the directory if it doesn't exist + Dir.mkdir(clamav_directory) + end + + file_path = "clamav_tmp/#{file_name}" + + File.open(file_path, 'wb') do |file| + file.write(file_body) + end + + file_path + end end end diff --git a/lib/common/virus_scan.rb b/lib/common/virus_scan.rb index e221b911faa..aefde7e31b5 100644 --- a/lib/common/virus_scan.rb +++ b/lib/common/virus_scan.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'clamav/commands/patch_scan_command' +require 'clamav/patch_client' + module Common module VirusScan module_function @@ -8,10 +11,15 @@ def scan(file_path) # `clamd` runs within service group, needs group read File.chmod(0o640, file_path) - # NOTE: If using custom_args, no other arguments can be passed to - # ClamScan::Client.scan. All other arguments will be ignored - args = ['-c', Rails.root.join('config', 'clamd.conf').to_s, file_path] - ClamScan::Client.scan(custom_args: args) + if mock_enabled? + true + else + ClamAV::PatchClient.new.safe?(file_path) # patch to call our class + end + end + + def mock_enabled? + Settings.clamav.mock end end end diff --git a/lib/shrine/plugins/validate_virus_free.rb b/lib/shrine/plugins/validate_virus_free.rb index 260ed57d1c0..9a4cf6aca19 100644 --- a/lib/shrine/plugins/validate_virus_free.rb +++ b/lib/shrine/plugins/validate_virus_free.rb @@ -9,9 +9,11 @@ module ValidateVirusFree module AttacherMethods def validate_virus_free(message: nil) Datadog::Tracing.trace('Scan Upload for Viruses') do - cached_path = get.download.path - result = Common::VirusScan.scan(cached_path) - result.safe? || add_error_msg(message || result.body) + file_to_scan = get.download + temp_file_path = Common::FileHelpers.generate_clamav_temp_file(file_to_scan) + result = Common::VirusScan.scan(temp_file_path) + File.delete(temp_file_path) + result || add_error_msg(message || "Virus Found + #{temp_file_path}") end end diff --git a/modules/vaos/reporting/logs_processor.rb b/modules/vaos/reporting/logs_processor.rb index 22d32b45fec..d3a79e46452 100644 --- a/modules/vaos/reporting/logs_processor.rb +++ b/modules/vaos/reporting/logs_processor.rb @@ -10,7 +10,7 @@ require 'uri' require 'csv' -AWS_LOG_PATH = 'dsva-vagov-prod/srv/vets-api/src/log/vets-api-server.log' +AWS_LOG_PATH = 'dsva-vagov-prod/app/log/vets-api-server.log' class LogsProcessor def request_by_id(id) @@ -28,7 +28,7 @@ def self.fetch_data(options) stdout_str = stdout.read stderr_str = stderr.read - stdout_arr = stdout_str.split(%r{dsva-vagov-prod/srv/vets-api/src/log/vets-api-server.log[^|]+\| }) + stdout_arr = stdout_str.split(%r{dsva-vagov-prod/app/log/vets-api-server.log[^|]+\| }) stdout_arr = stdout_arr[1..] puts stderr_str if stderr_str diff --git a/spec/lib/shrine/plugins/validate_virus_free_spec.rb b/spec/lib/shrine/plugins/validate_virus_free_spec.rb index b98f9d56503..2bb879feec6 100644 --- a/spec/lib/shrine/plugins/validate_virus_free_spec.rb +++ b/spec/lib/shrine/plugins/validate_virus_free_spec.rb @@ -29,31 +29,23 @@ def errors context 'with errors' do before do - allow(ClamScan.configuration).to receive(:client_location).and_return('found') + allow(Common::VirusScan).to receive(:scan).and_return(false) end context 'while in development' do it 'logs an error message if clamd is not running' do expect(Rails.env).to receive(:development?).and_return(true) expect(Rails.logger).to receive(:error).with(/PLEASE START CLAMD/) - allow(ClamScan::Client).to receive(:scan) - .and_return(instance_double('ClamScan::Response', - safe?: false, - body: 'ERROR: Could not lookup : nodename nor servname provided, or not known')) - - result = instance.validate_virus_free + result = instance.validate_virus_free(message: 'nodename nor servname provided') expect(result).to be(true) end end context 'with the default error message' do it 'adds an error if clam scan returns not safe' do - allow(ClamScan::Client).to receive(:scan) - .and_return(instance_double('ClamScan::Response', safe?: false, body: nil)) - result = instance.validate_virus_free expect(result).to be(false) - expect(instance.errors).to eq(['virus or malware detected']) + expect(instance.errors).to include(match(/Virus Found/)) end end @@ -61,9 +53,6 @@ def errors let(:message) { 'oh noes!' } it 'adds an error with a custom error message if clam scan returns not safe' do - allow(ClamScan::Client).to receive(:scan) - .and_return(instance_double('ClamScan::Response', safe?: false)) - result = instance.validate_virus_free(message:) expect(result).to be(false) expect(instance.errors).to eq(['oh noes!']) @@ -71,21 +60,18 @@ def errors end end - it 'does not add an error if clam scan returns safe' do - allow(ClamScan::Client).to receive(:scan) - .and_return(instance_double('ClamScan::Response', safe?: true)) - - expect(instance).not_to receive(:add_error_msg) - result = instance.validate_virus_free - expect(result).to be(true) - end + context 'it returns safe' do + before do + allow(Common::VirusScan).to receive(:scan).and_return(true) + end - it 'changes group permissions of the uploaded file' do - allow(ClamScan::Client).to receive(:scan) - .and_return(instance_double('ClamScan::Response', safe?: true)) + it 'does not add an error if clam scan returns safe' do + allow_any_instance_of(ClamAV::PatchClient).to receive(:safe?).and_return(true) - expect(File).to receive(:chmod).with(0o640, 'foo/bar.jpg').and_return(1) - instance.validate_virus_free + expect(instance).not_to receive(:add_error_msg) + result = instance.validate_virus_free + expect(result).to be(true) + end end end end diff --git a/spec/models/persistent_attachments/dependency_claim_spec.rb b/spec/models/persistent_attachments/dependency_claim_spec.rb index a4339736e80..7e9ac88134a 100644 --- a/spec/models/persistent_attachments/dependency_claim_spec.rb +++ b/spec/models/persistent_attachments/dependency_claim_spec.rb @@ -6,13 +6,16 @@ let(:file) { Rails.root.join('spec', 'fixtures', 'files', 'marriage-certificate.pdf') } let(:instance) { described_class.new(form_id: '686C-674') } + before do + allow(Common::VirusScan).to receive(:scan).and_return(true) + end + it 'sets a guid on initialize' do expect(instance.guid).to be_a(String) end it 'allows adding a file' do - allow(ClamScan::Client).to receive(:scan) - .and_return(instance_double('ClamScan::Response', safe?: true)) + allow_any_instance_of(ClamAV::PatchClient).to receive(:safe?).and_return(true) instance.file = file.open expect(instance.valid?).to be(true) expect(instance.file.shrine_class).to be(ClaimDocumentation::Uploader) diff --git a/spec/models/persistent_attachments/lgy_claim_spec.rb b/spec/models/persistent_attachments/lgy_claim_spec.rb index b0c06a6d131..230b9db1467 100644 --- a/spec/models/persistent_attachments/lgy_claim_spec.rb +++ b/spec/models/persistent_attachments/lgy_claim_spec.rb @@ -6,13 +6,16 @@ let(:file) { Rails.root.join('spec', 'fixtures', 'files', 'marriage-certificate.pdf') } let(:instance) { described_class.new(form_id: '28-1880') } + before do + allow(Common::VirusScan).to receive(:scan).and_return(true) + end + it 'sets a guid on initialize' do expect(instance.guid).to be_a(String) end it 'allows adding a file' do - allow(ClamScan::Client).to receive(:scan) - .and_return(instance_double('ClamScan::Response', safe?: true)) + allow_any_instance_of(ClamAV::PatchClient).to receive(:safe?).and_return(true) instance.file = file.open expect(instance.valid?).to be(true) expect(instance.file.shrine_class).to be(ClaimDocumentation::Uploader) diff --git a/spec/models/persistent_attachments/pension_burial_spec.rb b/spec/models/persistent_attachments/pension_burial_spec.rb index 86485ad393d..2dd2bdb0ec4 100644 --- a/spec/models/persistent_attachments/pension_burial_spec.rb +++ b/spec/models/persistent_attachments/pension_burial_spec.rb @@ -6,13 +6,16 @@ let(:file) { Rails.root.join('spec', 'fixtures', 'files', 'doctors-note.pdf') } let(:instance) { described_class.new(form_id: 'T-123') } + before do + allow(Common::VirusScan).to receive(:scan).and_return(true) + end + it 'sets a guid on initialize' do expect(instance.guid).to be_a(String) end it 'allows adding a file' do - allow(ClamScan::Client).to receive(:scan) - .and_return(instance_double('ClamScan::Response', safe?: true)) + allow_any_instance_of(ClamAV::PatchClient).to receive(:safe?).and_return(true) instance.file = file.open expect(instance.valid?).to be(true) expect(instance.file.shrine_class).to be(ClaimDocumentation::Uploader) diff --git a/spec/requests/claim_documents_spec.rb b/spec/requests/claim_documents_spec.rb index f927db74dfa..a02810c323b 100644 --- a/spec/requests/claim_documents_spec.rb +++ b/spec/requests/claim_documents_spec.rb @@ -6,8 +6,8 @@ before do allow(Rails.logger).to receive(:info) allow(Rails.logger).to receive(:error) - allow(ClamScan::Client).to receive(:scan) - .and_return(instance_double('ClamScan::Response', safe?: true)) + allow(Common::VirusScan).to receive(:scan).and_return(true) + allow_any_instance_of(Common::VirusScan).to receive(:scan).and_return(true) end context 'with a valid file' do diff --git a/spec/simplecov_helper.rb b/spec/simplecov_helper.rb index cb82fd49798..78b15d8fbb7 100644 --- a/spec/simplecov_helper.rb +++ b/spec/simplecov_helper.rb @@ -39,7 +39,6 @@ def merge_results def self.add_filters add_filter 'app/controllers/concerns/accountable.rb' - add_filter 'config/initializers/clamscan.rb' add_filter 'lib/apps/configuration.rb' add_filter 'lib/apps/responses/response.rb' add_filter 'lib/config_helper.rb' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 593140521e4..ac0ff59beac 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,7 +26,6 @@ add_filter 'app/controllers/concerns/accountable.rb' add_filter 'app/models/in_progress_disability_compensation_form.rb' add_filter 'app/serializers/appeal_serializer.rb' - add_filter 'config/initializers/clamscan.rb' add_filter 'lib/apps/configuration.rb' add_filter 'lib/apps/responses/response.rb' add_filter 'lib/config_helper.rb' diff --git a/spec/support/uploader_helpers.rb b/spec/support/uploader_helpers.rb index bb2cf7abd36..f7559ce71fb 100644 --- a/spec/support/uploader_helpers.rb +++ b/spec/support/uploader_helpers.rb @@ -7,14 +7,8 @@ module UploaderHelpers module ClassMethods def stub_virus_scan - let(:result) do - { - safe?: true - } - end - before do - allow(Common::VirusScan).to receive(:scan).and_return(OpenStruct.new(result)) + allow(Common::VirusScan).to receive(:scan).and_return(true) end end end diff --git a/spec/uploaders/uploader_virus_scan_spec.rb b/spec/uploaders/uploader_virus_scan_spec.rb index 6f8d0a330f8..0726bde9961 100644 --- a/spec/uploaders/uploader_virus_scan_spec.rb +++ b/spec/uploaders/uploader_virus_scan_spec.rb @@ -24,19 +24,15 @@ def store_image end context 'with a virus' do - let(:result) do - { - safe?: false, - body: 'virus found' - } - end + let(:result) { false } it 'raises an error' do + allow(Common::VirusScan).to receive(:scan).and_return(false) expect(Rails.env).to receive(:production?).and_return(true) expect(file).to receive(:delete) expect { store_image }.to raise_error( - UploaderVirusScan::VirusFoundError, 'virus found' + UploaderVirusScan::VirusFoundError ) end end