rails_template_18f 0.2.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/Gemfile +0 -2
- data/Gemfile.lock +3 -2
- data/README.md +12 -15
- data/exe/rails_template_18f +60 -0
- data/lib/generators/rails_template18f/active_storage/active_storage_generator.rb +142 -0
- data/lib/generators/rails_template18f/active_storage/templates/app/jobs/file_scan_job.rb +33 -0
- data/lib/generators/rails_template18f/active_storage/templates/app/models/file_upload.rb +25 -0
- data/lib/generators/rails_template18f/active_storage/templates/doc/adr/clamav.md.tt +30 -0
- data/lib/generators/rails_template18f/active_storage/templates/spec/jobs/file_scan_job_spec.rb +35 -0
- data/lib/generators/rails_template18f/active_storage/templates/spec/models/file_upload_spec.rb +38 -0
- data/lib/generators/rails_template18f/circleci/circleci_generator.rb +4 -1
- data/lib/generators/rails_template18f/cloud_gov_config/cloud_gov_config_generator.rb +29 -0
- data/lib/generators/rails_template18f/cloud_gov_config/templates/app/models/cloud_gov_config.rb +15 -0
- data/lib/generators/rails_template18f/cloud_gov_config/templates/spec/models/cloud_gov_config_spec.rb +44 -0
- data/lib/generators/rails_template18f/i18n/i18n_generator.rb +106 -0
- data/{templates → lib/generators/rails_template18f/i18n/templates}/config/locales/en.yml.tt +3 -3
- data/{templates → lib/generators/rails_template18f/i18n/templates}/config/locales/es.yml +3 -3
- data/{templates → lib/generators/rails_template18f/i18n/templates}/config/locales/fr.yml +3 -6
- data/{templates → lib/generators/rails_template18f/i18n/templates}/config/locales/zh.yml +0 -0
- data/lib/generators/rails_template18f/i18n_js/i18n_js_generator.rb +59 -0
- data/lib/generators/rails_template18f/i18n_js/templates/lib/tasks/i18n.rake +9 -0
- data/lib/generators/rails_template18f/newrelic/newrelic_generator.rb +2 -0
- data/lib/generators/rails_template18f/sidekiq/sidekiq_generator.rb +72 -0
- data/lib/generators/rails_template18f/sidekiq/templates/config/initializers/redis.rb +14 -0
- data/lib/generators/rails_template18f/terraform/templates/terraform/production/main.tf.tt +37 -5
- data/lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/main.tf.tt +50 -0
- data/lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/providers.tf +16 -0
- data/lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/variables.tf +47 -0
- data/lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/main.tf.tt +23 -0
- data/lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/providers.tf +16 -0
- data/lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/variables.tf +42 -0
- data/lib/generators/rails_template18f/terraform/templates/terraform/staging/main.tf.tt +37 -5
- data/lib/generators/rails_template18f/terraform/terraform_generator.rb +0 -11
- data/lib/rails_template18f/app_updater.rb +19 -0
- data/lib/rails_template18f/generators/base.rb +37 -5
- data/lib/rails_template18f/generators/cloud_gov_options.rb +0 -4
- data/lib/rails_template18f/version.rb +1 -1
- data/rails-template-18f.gemspec +2 -0
- data/template.rb +78 -96
- data/templates/config/deployment/staging.yml +1 -1
- data/templates/config/environments/ci.rb +0 -1
- data/templates/doc/compliance/apps/application.boundary.md.tt +0 -7
- metadata +59 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ccf6b140f15f85229d716db5c0b51db5b88b41172000f89a613bdc09975c61f7
|
4
|
+
data.tar.gz: 6a3a14efb4cb1373236a53260cf06b161375346d37097563ef65197862e538a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8fb2a0c4a4adf9fa72ea8078a6b1001671f2e562ece7b3d74b64ffa39c113b96c280f64a3eacc0233b67c8bc2b6ec160b2867c78e49363100d1cdc7055352516
|
7
|
+
data.tar.gz: 48e96e771a2fb4dd8ca03719f923ae2749b6b212577c88c8e72a3e2788d71b1a6a6bda768611509aa2001e129a7a29c833ad99a93433bf954c46552671d40c2b
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,23 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.4.1] - 2022-02-25
|
4
|
+
|
5
|
+
- update gem dependencies
|
6
|
+
- fix issues when included gem hadn't been previously installed
|
7
|
+
|
8
|
+
## [0.4.0] - 2022-02-24
|
9
|
+
|
10
|
+
- helper script to run rails app:update
|
11
|
+
- cloud.gov configuration helper generator
|
12
|
+
- activestorage/clamav generator
|
13
|
+
- activejob/sidekiq generator
|
14
|
+
- i18n-js generator
|
15
|
+
|
16
|
+
## [0.3.0] - 2022-02-17
|
17
|
+
|
18
|
+
- i18n generator
|
19
|
+
- helper script to run rails new without cloning repo
|
20
|
+
|
3
21
|
## [0.2.0] - 2022-02-16
|
4
22
|
|
5
23
|
- terraform generator
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rails_template_18f (0.
|
4
|
+
rails_template_18f (0.4.1)
|
5
5
|
activesupport (~> 7.0.0)
|
6
|
+
colorize (~> 0.8)
|
6
7
|
railties (~> 7.0.0)
|
8
|
+
thor (~> 1.0)
|
7
9
|
|
8
10
|
GEM
|
9
11
|
remote: https://rubygems.org/
|
@@ -124,7 +126,6 @@ PLATFORMS
|
|
124
126
|
DEPENDENCIES
|
125
127
|
ammeter (~> 1.1)
|
126
128
|
byebug
|
127
|
-
colorize (~> 0.8)
|
128
129
|
rails_template_18f!
|
129
130
|
rake (~> 13.0)
|
130
131
|
rspec (~> 3.11)
|
data/README.md
CHANGED
@@ -7,35 +7,32 @@ See the `rails-6` branch for Rails 6.1.x
|
|
7
7
|
|
8
8
|
## Use for new Rails Project
|
9
9
|
|
10
|
-
1.
|
11
|
-
1.
|
12
|
-
1. Run `rails new <<PATH_TO_PROJECT>> --rc=<<RC_FILE>>` with the appropriate rc file for your needs. The path should not be a subdirectory of this repository.
|
10
|
+
1. `gem install rails_template_18f`
|
11
|
+
1. `rails_template_18f help new` for usage instructions
|
13
12
|
|
14
|
-
### Choosing
|
15
|
-
|
16
|
-
You should run this template with either `railsrc` or `railsrc-hotwire` depending on your development needs.
|
13
|
+
### Choosing whether to use `--hotwire`
|
17
14
|
|
18
15
|
#### Server Rendered _or_ Single Page Applications
|
19
16
|
|
20
|
-
`
|
17
|
+
`rails_template_18f new <<PATH_TO_PROJECT>>` _or_ `rails_template_18f new <<PATH_TO_PROJECT>> --no-hotwire`
|
21
18
|
|
22
|
-
|
19
|
+
This creates a Rails application that is appropriate for both server-rendered applications,
|
23
20
|
as well as a basis for installing a separate Single Page Application (SPA) library such as React.
|
24
21
|
|
25
22
|
#### A bit more JavaScript needed
|
26
23
|
|
27
|
-
`
|
24
|
+
`rails_template_18f new <<PATH_TO_PROJECT>> --hotwire`
|
28
25
|
|
29
|
-
|
26
|
+
This creates a Rails application that includes the [Hotwire](https://hotwired.dev/) JavaScript framework.
|
30
27
|
|
31
28
|
Hotwire can be used to add [a bit of JavaScript](https://engineering.18f.gov/web-architecture/#:~:text=are%20more%20complex-,If%20your%20use%20case%20requires%20a%20bit%20of%20client%2Dside%20interactivity%2C%20use%20the%20above%20options%20with%20a%20bit%20of%20JavaScript.,-You%20might%20use)
|
32
29
|
for more interactivity than server-rendered apps, but less than a full SPA.
|
33
30
|
|
34
31
|
### Available Options
|
35
32
|
|
36
|
-
The following options can be added
|
33
|
+
The following options can be added to change how the template behaves.
|
37
34
|
|
38
|
-
**Important:** You must not pass `--skip-bundle` or `--skip-javascript` to `
|
35
|
+
**Important:** You must not pass `--skip-bundle` or `--skip-javascript` to `rails_template_18f` or various aspects of the template will be broken
|
39
36
|
|
40
37
|
#### `--javascript=esbuild`
|
41
38
|
|
@@ -46,7 +43,7 @@ maintaining IE11 support with esbuild may be tricky.
|
|
46
43
|
|
47
44
|
Each of the skipped frameworks in `railsrc` can be overridden on the command line. For example: `--no-skip-active-storage` will include support for `ActiveStorage` document uploads
|
48
45
|
|
49
|
-
### What `
|
46
|
+
### What default use or `--no-hotwire` does
|
50
47
|
|
51
48
|
```
|
52
49
|
--skip-active-storage # don't include ActiveStorage for document upload
|
@@ -61,9 +58,9 @@ Each of the skipped frameworks in `railsrc` can be overridden on the command lin
|
|
61
58
|
--database=postgresql # default to PostgreSQL
|
62
59
|
```
|
63
60
|
|
64
|
-
### What
|
61
|
+
### What `--hotwire` does
|
65
62
|
|
66
|
-
|
63
|
+
Identical to `--no-hotwire` except that [Hotwire](https://hotwired.dev/) and [ActionCable](https://guides.rubyonrails.org/action_cable_overview.html) are not skipped.
|
67
64
|
|
68
65
|
ActionCable is included to enable the [Turbo Streams](https://turbo.hotwired.dev/handbook/streams) functionality of Hotwire.
|
69
66
|
|
@@ -0,0 +1,60 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "thor"
|
5
|
+
require_relative "../lib/rails_template18f/version"
|
6
|
+
|
7
|
+
class CLI < Thor
|
8
|
+
include Thor::Actions
|
9
|
+
|
10
|
+
desc "new APP_DIRECTORY [options] [rails new arguments]", "Run rails new with 18F flavor"
|
11
|
+
option :hotwire, type: :boolean, default: false, desc: "Enable hotwire JS framework"
|
12
|
+
long_desc <<-LONGDESC
|
13
|
+
Create a new rails application in <APP_DIRECTORY> as customized by
|
14
|
+
|
15
|
+
* railsrc: https://github.com/18F/rails-template/blob/main/railsrc
|
16
|
+
|
17
|
+
* template.rb: https://github.com/18F/rails-template/blob/main/template.rb
|
18
|
+
|
19
|
+
with --hotwire option, includes the Hotwire JS framework
|
20
|
+
|
21
|
+
all other arguments will be passed as-is to `rails new`
|
22
|
+
LONGDESC
|
23
|
+
def new(app_directory, *rails_arguments)
|
24
|
+
gem_path = File.expand_path("..", __dir__)
|
25
|
+
railsrc = options[:hotwire] ? "railsrc-hotwire" : "railsrc"
|
26
|
+
run "rails new #{app_directory} --rc=#{File.join(gem_path, railsrc)} --template=#{File.join(gem_path, "template.rb")} #{rails_arguments.join(" ")}"
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "update", "Run rails app:update with some enhancements"
|
30
|
+
long_desc <<-LONGDESC
|
31
|
+
Run `rails app:update` with frameworks fully defined by what is commented out at the top
|
32
|
+
of config/application.rb
|
33
|
+
|
34
|
+
Example: to enable ActiveStorage
|
35
|
+
|
36
|
+
1) Uncomment `require "active_storage/engine"` in `config/application.rb`
|
37
|
+
|
38
|
+
2) Run `bin/rails active_storage:install`
|
39
|
+
|
40
|
+
3) Run bundle exec rails_template_18f update
|
41
|
+
|
42
|
+
4) Optional: run other rails_template18f generators that may be applicable
|
43
|
+
LONGDESC
|
44
|
+
def update
|
45
|
+
require_relative "../lib/rails_template18f/app_updater"
|
46
|
+
require "rails/command"
|
47
|
+
Rails::Command.invoke "app:update"
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "version", "Output gem version"
|
51
|
+
def version
|
52
|
+
puts RailsTemplate18f::VERSION
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.exit_on_failure?
|
56
|
+
true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
CLI.start(ARGV)
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
|
5
|
+
module RailsTemplate18f
|
6
|
+
module Generators
|
7
|
+
class ActiveStorageGenerator < ::Rails::Generators::Base
|
8
|
+
include Base
|
9
|
+
|
10
|
+
desc <<~DESC
|
11
|
+
Description:
|
12
|
+
Document use of Clamav as ActiveStorage scanner
|
13
|
+
DESC
|
14
|
+
|
15
|
+
def configure_active_storage
|
16
|
+
generate "rails_template18f:cloud_gov_config", inline: true
|
17
|
+
rails_command "active_storage:install", inline: true
|
18
|
+
comment_lines "config/environments/production.rb", /active_storage\.service/
|
19
|
+
insert_into_file "config/environments/production.rb", "\n config.active_storage.service = :amazon", after: /active_storage\.service.*$/
|
20
|
+
environment "config.active_storage.service = :local", env: "ci"
|
21
|
+
append_to_file "config/storage.yml", <<~EOYAML
|
22
|
+
|
23
|
+
amazon:
|
24
|
+
service: S3
|
25
|
+
access_key_id: <%= CloudGovConfig.dig(:s3, :credentials, :access_key_id) %>
|
26
|
+
secret_access_key: <%= CloudGovConfig.dig(:s3, :credentials, :secret_access_key) %>
|
27
|
+
region: us-gov-west-1
|
28
|
+
bucket: <%= CloudGovConfig.dig(:s3, :credentials, :bucket) %>
|
29
|
+
EOYAML
|
30
|
+
end
|
31
|
+
|
32
|
+
def install_gems
|
33
|
+
faraday_installed = gem_installed?("faraday")
|
34
|
+
middleware_installed = gem_installed?("faraday-multipart")
|
35
|
+
sdk_installed = gem_installed?("aws-sdk-s3")
|
36
|
+
return if faraday_installed && middleware_installed && sdk_installed
|
37
|
+
gem "faraday", "~> 2.2" unless faraday_installed
|
38
|
+
gem "faraday-multipart", "~> 1.0" unless middleware_installed
|
39
|
+
unless sdk_installed
|
40
|
+
gem_group :production do
|
41
|
+
gem "aws-sdk-s3", "~> 1.112"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
bundle_install
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_scanned_upload_model_and_job
|
48
|
+
generate :migration, "CreateFileUploads", "file:attachment", "record:references{polymorphic}", "scan_status:string", inline: true
|
49
|
+
migration_file = Dir.glob(File.expand_path(File.join("db", "migrate", "[0-9]*_*.rb"), destination_root)).grep(/\d+_create_file_uploads.rb$/).first
|
50
|
+
unless migration_file.nil?
|
51
|
+
gsub_file migration_file, ":scan_status", ":scan_status, null: false, default: \"uploaded\""
|
52
|
+
end
|
53
|
+
directory "app"
|
54
|
+
directory "spec"
|
55
|
+
end
|
56
|
+
|
57
|
+
def configure_local_clamav_runner
|
58
|
+
append_to_file "Procfile.dev", "clamav: docker run --rm -p 9443:9443 ajilaag/clamav-rest:20211229\n"
|
59
|
+
end
|
60
|
+
|
61
|
+
def configure_clamav_env_var
|
62
|
+
append_to_file ".env", <<~EOM
|
63
|
+
|
64
|
+
|
65
|
+
# CLAMAV_API_URL tells FileScanJob where to send files for virus scans
|
66
|
+
CLAMAV_API_URL=https://localhost:9443
|
67
|
+
EOM
|
68
|
+
insert_into_file "manifest.yml", " CLAMAV_API_URL: \"https://#{app_name}-clamapi-((env)).apps.internal:9443\"\n", before: /^\s+processes:/
|
69
|
+
insert_into_file "manifest.yml", "\n - #{app_name}-s3-((env))", after: "services:"
|
70
|
+
end
|
71
|
+
|
72
|
+
def update_boundary_diagram
|
73
|
+
boundary_filename = "doc/compliance/apps/application.boundary.md"
|
74
|
+
|
75
|
+
insert_into_file boundary_filename, indent(<<~EOB, 16), after: /ContainerDb\(app_db.*$\n/
|
76
|
+
Container(clamav, "File Scanning API", "ClamAV", "Internal application for scanning user uploads")
|
77
|
+
ContainerDb(app_s3, "File Storage", "AWS S3", "User-uploaded file storage")
|
78
|
+
EOB
|
79
|
+
insert_into_file boundary_filename, <<~EOB, before: "@enduml"
|
80
|
+
Rel(app, app_s3, "reads/writes file data", "https (443)")
|
81
|
+
EOB
|
82
|
+
if has_active_job?
|
83
|
+
insert_into_file boundary_filename, <<~EOB, before: "@enduml"
|
84
|
+
Rel(worker, app_s3, "reads/writes file data", "https (443)")
|
85
|
+
Rel(worker, clamav, "scans files", "https (9443)")
|
86
|
+
EOB
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def update_data_model_uml
|
91
|
+
insert_into_file "doc/compliance/apps/data.logical.md", data_model_uml, before: "@enduml"
|
92
|
+
end
|
93
|
+
|
94
|
+
def generate_adr
|
95
|
+
adr_dir = File.expand_path(File.join("doc", "adr"), destination_root)
|
96
|
+
if Dir.exist? adr_dir
|
97
|
+
@next_adr_id = `ls #{adr_dir} | tail -n 1 | awk -F '-' '{print $1}'`.strip.to_i + 1
|
98
|
+
template "doc/adr/clamav.md", "doc/adr/#{"%04d" % @next_adr_id}-clamav-file-scanning.md"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
no_tasks do
|
103
|
+
def data_model_uml
|
104
|
+
<<~UML
|
105
|
+
class file_uploads {
|
106
|
+
* id : bigint <<generated>>
|
107
|
+
* scan_status : string
|
108
|
+
* record_id : bigint
|
109
|
+
* record_type : string
|
110
|
+
}
|
111
|
+
class active_storage_attachments {
|
112
|
+
* id : bigint <<generated>>
|
113
|
+
* name : string
|
114
|
+
* record_type : string
|
115
|
+
* record_id : bigint
|
116
|
+
* blob_id : bigint
|
117
|
+
* created_at : timestamp without time zone
|
118
|
+
}
|
119
|
+
class active_storage_blobs {
|
120
|
+
* id : bigint <<generated>>
|
121
|
+
* key : string
|
122
|
+
* filename : string
|
123
|
+
content_type : string
|
124
|
+
metadata : text
|
125
|
+
* service_name : string
|
126
|
+
* byte_size : bigint
|
127
|
+
checksum : string
|
128
|
+
* created_at : timestamp without time zone
|
129
|
+
}
|
130
|
+
class active_storage_variant_records {
|
131
|
+
* id : bigint <<generated>>
|
132
|
+
* variation_digest : string
|
133
|
+
}
|
134
|
+
file_uploads ||--|| active_storage_attachments
|
135
|
+
active_storage_attachments ||--|{ active_storage_blobs
|
136
|
+
active_storage_variant_records ||--|{ active_storage_blobs
|
137
|
+
UML
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class FileScanJob < ApplicationJob
|
2
|
+
queue_as :default
|
3
|
+
|
4
|
+
def perform(file_upload)
|
5
|
+
return if file_upload.nil? || file_upload.clean?
|
6
|
+
file_upload.open do |file|
|
7
|
+
payload = {file: Faraday::Multipart::FilePart.new(
|
8
|
+
file,
|
9
|
+
file_upload.content_type,
|
10
|
+
file_upload.filename
|
11
|
+
)}
|
12
|
+
response = connection.post("/scan", payload)
|
13
|
+
if response.success?
|
14
|
+
file_upload.update_columns scan_status: "scanned", updated_at: Time.now
|
15
|
+
else
|
16
|
+
logger.error "File Scan for #{file_upload.id} failed: #{response.body}"
|
17
|
+
file_upload.update_columns scan_status: "quarantined", updated_at: Time.now
|
18
|
+
end
|
19
|
+
end
|
20
|
+
rescue => ex
|
21
|
+
file_upload&.update_columns scan_status: "scan_failed", updated_at: Time.now
|
22
|
+
raise ex
|
23
|
+
end
|
24
|
+
|
25
|
+
def connection
|
26
|
+
@connection ||= Faraday.new(
|
27
|
+
url: ENV["CLAMAV_API_URL"],
|
28
|
+
ssl: {verify: false}
|
29
|
+
) do |f|
|
30
|
+
f.request :multipart
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class FileUpload < ApplicationRecord
|
2
|
+
belongs_to :record, polymorphic: true
|
3
|
+
has_one_attached :file
|
4
|
+
|
5
|
+
delegate :open, :content_type, to: :file
|
6
|
+
|
7
|
+
validates_presence_of :file
|
8
|
+
validates_inclusion_of :scan_status, in: %w[uploaded scan_failed scanned quarantined]
|
9
|
+
|
10
|
+
after_commit :scan
|
11
|
+
|
12
|
+
def clean?
|
13
|
+
scan_status == "scanned"
|
14
|
+
end
|
15
|
+
|
16
|
+
def filename
|
17
|
+
file.filename.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def scan
|
23
|
+
FileScanJob.perform_later self
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# <%= @next_adr_id %>. ClamAV File Scanning
|
2
|
+
|
3
|
+
Date: <%= Date.today.iso8601 %>
|
4
|
+
|
5
|
+
## Status
|
6
|
+
|
7
|
+
Accepted
|
8
|
+
|
9
|
+
## Context
|
10
|
+
|
11
|
+
In order to satisfy the [RA-5](https://nvd.nist.gov/800-53/Rev4/control/RA-5)
|
12
|
+
control around vulnerability scanning, we wish to scan all user-uploaded files
|
13
|
+
with a malware detection service. These files are user-supplied, and thus
|
14
|
+
cannot fit into our other security controls built into the CI/CD pipeline.
|
15
|
+
|
16
|
+
## Decision
|
17
|
+
|
18
|
+
We will run a ClamAV daemon, fronted by a REST API that we will pass all user-uploaded
|
19
|
+
files through as part of the upload process.
|
20
|
+
<% if terraform_dir_exists? %>
|
21
|
+
The ClamAV app is deployed along with other infrastructure by terraform.
|
22
|
+
<% else %>
|
23
|
+
The ClamAV app is based on a [separate government-controlled repository](https://github.com/18f/clamav-api-cg-app)
|
24
|
+
<% end %>
|
25
|
+
|
26
|
+
## Consequences
|
27
|
+
|
28
|
+
While our user-supplied files will now have vulnerability protection, this architecture
|
29
|
+
does not provide scanning of the application itself. Therefore we must find other solutions
|
30
|
+
to addressing SI-3 or RA-5 in the context of the application.
|
data/lib/generators/rails_template18f/active_storage/templates/spec/jobs/file_scan_job_spec.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require "rails_helper"
|
2
|
+
|
3
|
+
RSpec.describe FileScanJob, type: :job do
|
4
|
+
subject { described_class.new }
|
5
|
+
let(:scanned_file) { double(clean?: true) }
|
6
|
+
let(:unscanned_file) { double(id: 1, clean?: false, content_type: "text/plain", filename: "test.txt") }
|
7
|
+
let(:success_response) { double(success?: true) }
|
8
|
+
let(:error_response) { double(success?: false, body: "Error response body") }
|
9
|
+
|
10
|
+
it "deals with a nil argument" do
|
11
|
+
expect { subject.perform nil }.to_not raise_error
|
12
|
+
end
|
13
|
+
|
14
|
+
it "returns quickly if the file is already scanned" do
|
15
|
+
expect { subject.perform scanned_file }.to_not raise_error
|
16
|
+
end
|
17
|
+
|
18
|
+
it "updates the scan_status after scanning the file" do
|
19
|
+
now = Time.now
|
20
|
+
allow(Time).to receive(:now).and_return now
|
21
|
+
allow(unscanned_file).to receive(:open).and_yield __FILE__
|
22
|
+
expect(unscanned_file).to receive(:update_columns).with scan_status: "scanned", updated_at: Time.now
|
23
|
+
allow(subject).to receive(:connection).and_return double(post: success_response)
|
24
|
+
subject.perform unscanned_file
|
25
|
+
end
|
26
|
+
|
27
|
+
it "marks the file as quarantined when dirty" do
|
28
|
+
now = Time.now
|
29
|
+
allow(Time).to receive(:now).and_return now
|
30
|
+
allow(unscanned_file).to receive(:open).and_yield __FILE__
|
31
|
+
expect(unscanned_file).to receive(:update_columns).with scan_status: "quarantined", updated_at: Time.now
|
32
|
+
allow(subject).to receive(:connection).and_return double(post: error_response)
|
33
|
+
subject.perform unscanned_file
|
34
|
+
end
|
35
|
+
end
|
data/lib/generators/rails_template18f/active_storage/templates/spec/models/file_upload_spec.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require "rails_helper"
|
2
|
+
|
3
|
+
RSpec.describe FileUpload, type: :model do
|
4
|
+
subject { described_class.new }
|
5
|
+
|
6
|
+
describe "validations" do
|
7
|
+
before do
|
8
|
+
subject.file.attach(io: File.open(__FILE__), filename: "file_upload_spec.rb")
|
9
|
+
end
|
10
|
+
|
11
|
+
%w[uploaded scan_failed scanned quarantined].each do |valid_status|
|
12
|
+
it "allows scan_status=#{valid_status}" do
|
13
|
+
pending "#{described_class.name} cannot be valid without a record to belong_to"
|
14
|
+
subject.scan_status = valid_status
|
15
|
+
expect(subject).to be_valid
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it "is invalid with a bad scan_status" do
|
20
|
+
subject.scan_status = "invalid"
|
21
|
+
expect(subject).to_not be_valid
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#clean?" do
|
26
|
+
it "returns true when scan_status is scanned" do
|
27
|
+
subject.scan_status = "scanned"
|
28
|
+
expect(subject).to be_clean
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns false when scan_status is not scanned" do
|
32
|
+
subject.scan_status = "uploaded"
|
33
|
+
expect(subject).to_not be_clean
|
34
|
+
subject.scan_status = "quarantined"
|
35
|
+
expect(subject).to_not be_clean
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -14,7 +14,10 @@ module RailsTemplate18f
|
|
14
14
|
DESC
|
15
15
|
|
16
16
|
def install_needed_gems
|
17
|
-
|
17
|
+
gem_name = "rspec_junit_formatter"
|
18
|
+
return if gem_installed? gem_name
|
19
|
+
gem gem_name, "~> 0.5", group: :test
|
20
|
+
bundle_install
|
18
21
|
end
|
19
22
|
|
20
23
|
def install_pipeline
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
|
5
|
+
module RailsTemplate18f
|
6
|
+
module Generators
|
7
|
+
class CloudGovConfigGenerator < ::Rails::Generators::Base
|
8
|
+
include Base
|
9
|
+
|
10
|
+
desc <<~DESC
|
11
|
+
Description:
|
12
|
+
Install a helper class to retrieve configuration from ENV["VCAP_SERVICES"]
|
13
|
+
DESC
|
14
|
+
|
15
|
+
def install_climate_control
|
16
|
+
return if gem_installed?("climate_control")
|
17
|
+
gem_group :test do
|
18
|
+
gem "climate_control", "~> 1.0"
|
19
|
+
end
|
20
|
+
bundle_install
|
21
|
+
end
|
22
|
+
|
23
|
+
def install_model_and_test
|
24
|
+
copy_file "app/models/cloud_gov_config.rb"
|
25
|
+
copy_file "spec/models/cloud_gov_config_spec.rb"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/generators/rails_template18f/cloud_gov_config/templates/app/models/cloud_gov_config.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CloudGovConfig
|
4
|
+
ENV_VARIABLE = "VCAP_SERVICES"
|
5
|
+
|
6
|
+
def self.dig(*path)
|
7
|
+
return nil if ENV[ENV_VARIABLE].blank?
|
8
|
+
first, *rest = path
|
9
|
+
vcap_services[first]&.first&.dig(*rest)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.vcap_services
|
13
|
+
@vcap_services ||= JSON.parse(ENV[ENV_VARIABLE]).with_indifferent_access
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails_helper"
|
4
|
+
|
5
|
+
RSpec.describe CloudGovConfig, type: :model do
|
6
|
+
subject { described_class }
|
7
|
+
|
8
|
+
describe ".dig" do
|
9
|
+
context "VCAP_SERVICES is blank" do
|
10
|
+
it "returns nil" do
|
11
|
+
expect(subject.dig(:s3, :credentials, :bucket)).to be_nil
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "VCAP_SERVICES is set" do
|
16
|
+
let(:bucket_name) { "bucket-name" }
|
17
|
+
let(:vcap) {
|
18
|
+
{
|
19
|
+
s3: [
|
20
|
+
{
|
21
|
+
credentials: {
|
22
|
+
bucket: bucket_name
|
23
|
+
}
|
24
|
+
}
|
25
|
+
]
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
around do |example|
|
30
|
+
ClimateControl.modify VCAP_SERVICES: vcap.to_json do
|
31
|
+
example.run
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it "can find a path" do
|
36
|
+
expect(subject.dig(:s3, :credentials, :bucket)).to eq bucket_name
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns nil for a missing path" do
|
40
|
+
expect(subject.dig(:s3, :credentials, :other)).to be_nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|