rails_template_18f 0.1.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +4 -1
  5. data/README.md +16 -19
  6. data/exe/rails_template_18f +60 -0
  7. data/lib/generators/rails_template18f/active_storage/active_storage_generator.rb +135 -0
  8. data/lib/generators/rails_template18f/active_storage/templates/app/jobs/file_scan_job.rb +33 -0
  9. data/lib/generators/rails_template18f/active_storage/templates/app/models/file_upload.rb +25 -0
  10. data/lib/generators/rails_template18f/active_storage/templates/doc/adr/clamav.md.tt +30 -0
  11. data/lib/generators/rails_template18f/active_storage/templates/spec/jobs/file_scan_job_spec.rb +35 -0
  12. data/lib/generators/rails_template18f/active_storage/templates/spec/models/file_upload_spec.rb +38 -0
  13. data/lib/generators/rails_template18f/circleci/circleci_generator.rb +26 -10
  14. data/lib/generators/rails_template18f/cloud_gov_config/cloud_gov_config_generator.rb +28 -0
  15. data/lib/generators/rails_template18f/cloud_gov_config/templates/app/models/cloud_gov_config.rb +15 -0
  16. data/lib/generators/rails_template18f/cloud_gov_config/templates/spec/models/cloud_gov_config_spec.rb +44 -0
  17. data/lib/generators/rails_template18f/dap/dap_generator.rb +72 -0
  18. data/lib/generators/rails_template18f/github_actions/github_actions_generator.rb +27 -11
  19. data/lib/generators/rails_template18f/i18n/i18n_generator.rb +107 -0
  20. data/{templates → lib/generators/rails_template18f/i18n/templates}/config/locales/en.yml.tt +3 -3
  21. data/{templates → lib/generators/rails_template18f/i18n/templates}/config/locales/es.yml +3 -3
  22. data/{templates → lib/generators/rails_template18f/i18n/templates}/config/locales/fr.yml +3 -6
  23. data/{templates → lib/generators/rails_template18f/i18n/templates}/config/locales/zh.yml +0 -0
  24. data/lib/generators/rails_template18f/i18n_js/i18n_js_generator.rb +60 -0
  25. data/lib/generators/rails_template18f/i18n_js/templates/lib/tasks/i18n.rake +9 -0
  26. data/lib/generators/rails_template18f/newrelic/newrelic_generator.rb +79 -0
  27. data/{templates/config/newrelic.yml → lib/generators/rails_template18f/newrelic/templates/config/newrelic.yml.tt} +7 -7
  28. data/lib/generators/rails_template18f/sidekiq/sidekiq_generator.rb +70 -0
  29. data/lib/generators/rails_template18f/sidekiq/templates/config/initializers/redis.rb +14 -0
  30. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/README.md.tt +0 -0
  31. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/bootstrap/import.sh +0 -0
  32. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/bootstrap/main.tf.tt +3 -3
  33. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/bootstrap/providers.tf +0 -0
  34. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/bootstrap/run.sh.tt +1 -1
  35. data/lib/generators/rails_template18f/terraform/templates/terraform/bootstrap/teardown_creds.sh.tt +5 -0
  36. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/bootstrap/variables.tf +0 -0
  37. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/create_space_deployer.sh +0 -0
  38. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/destroy_space_deployer.sh +0 -0
  39. data/lib/generators/rails_template18f/terraform/templates/terraform/production/main.tf.tt +82 -0
  40. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/production/providers.tf.tt +0 -0
  41. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/production/variables.tf +0 -0
  42. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/main.tf.tt +50 -0
  43. data/{templates/terraform/shared/database → lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav}/providers.tf +0 -0
  44. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/variables.tf +47 -0
  45. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/shared/database/main.tf.tt +0 -0
  46. data/{templates/terraform/shared/domain → lib/generators/rails_template18f/terraform/templates/terraform/shared/database}/providers.tf +0 -0
  47. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/shared/database/variables.tf +0 -0
  48. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/shared/domain/main.tf.tt +1 -1
  49. data/{templates/terraform/shared/s3 → lib/generators/rails_template18f/terraform/templates/terraform/shared/domain}/providers.tf +0 -0
  50. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/shared/domain/variables.tf +0 -0
  51. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/main.tf.tt +23 -0
  52. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/providers.tf +16 -0
  53. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/variables.tf +42 -0
  54. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/shared/s3/main.tf +0 -0
  55. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/s3/providers.tf +16 -0
  56. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/shared/s3/variables.tf +0 -0
  57. data/lib/generators/rails_template18f/terraform/templates/terraform/staging/main.tf.tt +62 -0
  58. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/staging/providers.tf.tt +0 -0
  59. data/{templates → lib/generators/rails_template18f/terraform/templates}/terraform/staging/variables.tf +0 -0
  60. data/lib/generators/rails_template18f/terraform/terraform_generator.rb +84 -0
  61. data/lib/rails_template18f/app_updater.rb +19 -0
  62. data/lib/rails_template18f/generators/base.rb +53 -0
  63. data/lib/rails_template18f/generators/cloud_gov_options.rb +53 -0
  64. data/lib/rails_template18f/generators/pipeline_options.rb +18 -0
  65. data/lib/rails_template18f/generators.rb +11 -0
  66. data/lib/rails_template18f/version.rb +1 -1
  67. data/lib/rails_template_18f.rb +1 -4
  68. data/rails-template-18f.gemspec +1 -0
  69. data/template.rb +84 -148
  70. data/templates/README.md.tt +7 -44
  71. data/templates/config/deployment/staging.yml +1 -1
  72. data/templates/config/environments/ci.rb +0 -1
  73. data/templates/doc/compliance/apps/application.boundary.md.tt +4 -31
  74. data/templates/githooks/{pre-commit.tt → pre-commit} +0 -15
  75. data/templates/manifest.yml.tt +1 -2
  76. metadata +77 -34
  77. data/lib/rails_template18f/terraform_options.rb +0 -68
  78. data/templates/terraform/bootstrap/teardown_creds.sh.tt +0 -5
  79. data/templates/terraform/production/main.tf.tt +0 -50
  80. data/templates/terraform/staging/main.tf.tt +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: daa50caac6d934e7a1d6611ac427068601ad836f81fab7b088aed2b47de3724b
4
- data.tar.gz: 7dd5b479db57b51ce6eadbe53134605a346db5de2e4c490510a59c3ec56f644b
3
+ metadata.gz: c87267d368d3e90a500c0fc1bf23312a3f6edf437ba991a9a4c7b994fff4282d
4
+ data.tar.gz: ec24cf97ec587cf730bb2f63ff4cf0fd5b13ae64364b175041b0b0afd42a4321
5
5
  SHA512:
6
- metadata.gz: '08bba6304087afcfda8ac54101628312329963338a36ed488768bdfdc2a296a9b54140df951108eaa8cbc563d29cd35a461379e6039d17a4f910a50b60313890'
7
- data.tar.gz: ad54e4be93ee88883503ed96f6a7ace02f00509d584b02e122b7fb66117a2baf6cd19f444f3f74f26c3209280be0204926e3ab2e8c217a1a59d600572b7f9abe
6
+ metadata.gz: c2c05519936ff836d7c99dd16a5363755a6e86731c5d41454adbc0f6a7ada083fd1b636d0f5da3db446225866f095b42c2278100c85d3c10a3a2182100fcf550
7
+ data.tar.gz: b2e6da4f298a5c0054997c2c44244e4956044af83af81792937adf7d52716a68b3f8ebf8332f598943b9bad9a37bebf3c6dd4da0eb196ac119cc32455db00e57
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2022-02-24
4
+
5
+ - helper script to run rails app:update
6
+ - cloud.gov configuration helper generator
7
+ - activestorage/clamav generator
8
+ - activejob/sidekiq generator
9
+ - i18n-js generator
10
+
11
+ ## [0.3.0] - 2022-02-17
12
+
13
+ - i18n generator
14
+ - helper script to run rails new without cloning repo
15
+
16
+ ## [0.2.0] - 2022-02-16
17
+
18
+ - terraform generator
19
+ - DAP generator
20
+ - Newrelic generator
21
+
3
22
  ## [0.1.0] - 2022-02-14
4
23
 
5
24
  - Initial release
data/Gemfile CHANGED
@@ -8,3 +8,5 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
 
10
10
  gem "colorize", "~> 0.8"
11
+
12
+ gem "byebug"
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rails_template_18f (0.1.0)
4
+ rails_template_18f (0.4.0)
5
5
  activesupport (~> 7.0.0)
6
6
  railties (~> 7.0.0)
7
+ thor (~> 1.0)
7
8
 
8
9
  GEM
9
10
  remote: https://rubygems.org/
@@ -32,6 +33,7 @@ GEM
32
33
  rspec-rails (>= 2.2)
33
34
  ast (2.4.2)
34
35
  builder (3.2.4)
36
+ byebug (11.1.3)
35
37
  colorize (0.8.1)
36
38
  concurrent-ruby (1.1.9)
37
39
  crass (1.0.6)
@@ -122,6 +124,7 @@ PLATFORMS
122
124
 
123
125
  DEPENDENCIES
124
126
  ammeter (~> 1.1)
127
+ byebug
125
128
  colorize (~> 0.8)
126
129
  rails_template_18f!
127
130
  rake (~> 13.0)
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. Clone this repository to your computer
11
- 1. Change directory into the clone
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 which RC file to use
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
- `rails new <<PATH_TO_PROJECT>> --rc=railsrc`
17
+ `rails_template_18f new <<PATH_TO_PROJECT>>` _or_ `rails_template_18f new <<PATH_TO_PROJECT>> --no-hotwire`
21
18
 
22
- The base `railsrc` file creates a Rails application that is appropriate for both server-rendered applications,
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
- `rails new <<PATH_TO_PROJECT>> --rc=railsrc-hotwire`
24
+ `rails_template_18f new <<PATH_TO_PROJECT>> --hotwire`
28
25
 
29
- The `railsrc-hotwire` file creates a Rails application that includes the [Hotwire](https://hotwired.dev/) JavaScript framework.
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 after `--rc=<<RC_FILE>>` to change how the template behaves.
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 `rails new` or various aspects of the template will be broken
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 `railsrc` does
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 `railsrc-hotwire` does
61
+ ### What `--hotwire` does
65
62
 
66
- `railsrc-hotwire` is identical to `railsrc` except that [Hotwire](https://hotwired.dev/) and [ActionCable](https://guides.rubyonrails.org/action_cable_overview.html) are not skipped.
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
 
@@ -97,10 +94,10 @@ ActionCable is included to enable the [Turbo Streams](https://turbo.hotwired.dev
97
94
  1. Optionally create Github Actions workflows for testing and cloud.gov deploy
98
95
  1. Optionally create terraform modules supporting staging & production cloud.gov spaces
99
96
  1. Optionally create CircleCI workflows for testing and cloud.gov deploy
100
- 1. Optionally create [Architecture Decision Records](https://adr.github.io/) for above setup
101
97
  1. Optionally create a New Relic config with FEDRAMP-specific host
102
98
  1. Optionally configure DAP (Digital Analytics Program)
103
99
  1. Optionally add base translation files and routes for Spanish, French, and Simplified Chinese (es.yml, fr.yml, and zh.yml)
100
+ 1. Create [Architecture Decision Records](https://adr.github.io/) for above setup
104
101
  1. Commit the resulting project with git (unless `--skip-git` is passed)
105
102
 
106
103
  ## Use for an existing Rails project
@@ -110,7 +107,7 @@ ActionCable is included to enable the [Turbo Streams](https://turbo.hotwired.dev
110
107
  Add this line to your application's Gemfile:
111
108
 
112
109
  ```ruby
113
- gem "rails-template-18f"
110
+ gem "rails_template_18f", group: :development
114
111
  ```
115
112
 
116
113
  And then run:
@@ -119,7 +116,7 @@ And then run:
119
116
 
120
117
  Or install it yourself as:
121
118
 
122
- $ gem install rails-template-18f
119
+ $ gem install rails_template_18f
123
120
 
124
121
  ### Usage
125
122
 
@@ -133,7 +130,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
133
130
 
134
131
  ## Contributing
135
132
 
136
- Bug reports and pull requests are welcome on GitHub at https://github.com/rahearn/rails-template-18f. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/rahearn/rails-template-18f/blob/main/CODE_OF_CONDUCT.md).
133
+ Bug reports and pull requests are welcome on GitHub at https://github.com/18f/rails-template. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/18f/rails-template/blob/main/CODE_OF_CONDUCT.md).
137
134
 
138
135
  ## Code of Conduct
139
136
 
@@ -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,135 @@
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"
17
+ rails_command "active_storage:install"
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
+ gem "faraday", "~> 2.2"
34
+ gem "faraday-multipart", "~> 1.0"
35
+ gem_group :production do
36
+ gem "aws-sdk-s3", "~> 1.112"
37
+ end
38
+ end
39
+
40
+ def create_scanned_upload_model_and_job
41
+ generate :migration, "CreateFileUploads", "file:attachment", "record:references{polymorphic}", "scan_status:string"
42
+ migration_file = Dir.glob(File.expand_path(File.join("db", "migrate", "[0-9]*_*.rb"), destination_root)).grep(/\d+_create_file_uploads.rb$/).first
43
+ unless migration_file.nil?
44
+ gsub_file migration_file, ":scan_status", ":scan_status, null: false, default: \"uploaded\""
45
+ end
46
+ directory "app"
47
+ directory "spec"
48
+ end
49
+
50
+ def configure_local_clamav_runner
51
+ append_to_file "Procfile.dev", "clamav: docker run --rm -p 9443:9443 ajilaag/clamav-rest:20211229\n"
52
+ end
53
+
54
+ def configure_clamav_env_var
55
+ append_to_file ".env", <<~EOM
56
+
57
+
58
+ # CLAMAV_API_URL tells FileScanJob where to send files for virus scans
59
+ CLAMAV_API_URL=https://localhost:9443
60
+ EOM
61
+ insert_into_file "manifest.yml", " CLAMAV_API_URL: \"https://#{app_name}-clamapi-((env)).apps.internal:9443\"\n", before: /^\s+processes:/
62
+ insert_into_file "manifest.yml", "\n - #{app_name}-s3-((env))", after: "services:"
63
+ end
64
+
65
+ def update_boundary_diagram
66
+ boundary_filename = "doc/compliance/apps/application.boundary.md"
67
+
68
+ insert_into_file boundary_filename, indent(<<~EOB, 16), after: /ContainerDb\(app_db.*$\n/
69
+ Container(clamav, "File Scanning API", "ClamAV", "Internal application for scanning user uploads")
70
+ ContainerDb(app_s3, "File Storage", "AWS S3", "User-uploaded file storage")
71
+ EOB
72
+ insert_into_file boundary_filename, <<~EOB, before: "@enduml"
73
+ Rel(app, app_s3, "reads/writes file data", "https (443)")
74
+ EOB
75
+ if has_active_job?
76
+ insert_into_file boundary_filename, <<~EOB, before: "@enduml"
77
+ Rel(worker, app_s3, "reads/writes file data", "https (443)")
78
+ Rel(worker, clamav, "scans files", "https (9443)")
79
+ EOB
80
+ end
81
+ end
82
+
83
+ def update_data_model_uml
84
+ insert_into_file "doc/compliance/apps/data.logical.md", data_model_uml, before: "@enduml"
85
+ end
86
+
87
+ def generate_adr
88
+ adr_dir = File.expand_path(File.join("doc", "adr"), destination_root)
89
+ if Dir.exist? adr_dir
90
+ @next_adr_id = `ls #{adr_dir} | tail -n 1 | awk -F '-' '{print $1}'`.strip.to_i + 1
91
+ template "doc/adr/clamav.md", "doc/adr/#{"%04d" % @next_adr_id}-clamav-file-scanning.md"
92
+ end
93
+ end
94
+
95
+ no_tasks do
96
+ def data_model_uml
97
+ <<~UML
98
+ class file_uploads {
99
+ * id : bigint <<generated>>
100
+ * scan_status : string
101
+ * record_id : bigint
102
+ * record_type : string
103
+ }
104
+ class active_storage_attachments {
105
+ * id : bigint <<generated>>
106
+ * name : string
107
+ * record_type : string
108
+ * record_id : bigint
109
+ * blob_id : bigint
110
+ * created_at : timestamp without time zone
111
+ }
112
+ class active_storage_blobs {
113
+ * id : bigint <<generated>>
114
+ * key : string
115
+ * filename : string
116
+ content_type : string
117
+ metadata : text
118
+ * service_name : string
119
+ * byte_size : bigint
120
+ checksum : string
121
+ * created_at : timestamp without time zone
122
+ }
123
+ class active_storage_variant_records {
124
+ * id : bigint <<generated>>
125
+ * variation_digest : string
126
+ }
127
+ file_uploads ||--|| active_storage_attachments
128
+ active_storage_attachments ||--|{ active_storage_blobs
129
+ active_storage_variant_records ||--|{ active_storage_blobs
130
+ UML
131
+ end
132
+ end
133
+ end
134
+ end
135
+ 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.
@@ -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
@@ -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
@@ -1,20 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rails/generators"
4
+
3
5
  module RailsTemplate18f
4
6
  module Generators
5
7
  class CircleciGenerator < ::Rails::Generators::Base
6
- include ::Rails::Generators::AppName
7
- include RailsTemplate18f::TerraformOptions
8
+ include Base
9
+ include PipelineOptions
8
10
 
9
11
  desc <<~DESC
10
12
  Description:
11
13
  Install CircleCI pipeline files
12
14
  DESC
13
15
 
14
- def self.source_root
15
- @source_root ||= File.expand_path(File.join(File.dirname(__FILE__), "templates"))
16
- end
17
-
18
16
  def install_needed_gems
19
17
  gem "rspec_junit_formatter", "~> 0.5", group: :test
20
18
  end
@@ -27,10 +25,28 @@ module RailsTemplate18f
27
25
  end
28
26
 
29
27
  def update_readme
30
- insert_into_file "README.md", readme_cicd, after: "## CI/CD\n"
31
- insert_into_file "README.md", readme_staging_deploy, after: "#### Staging\n"
32
- insert_into_file "README.md", readme_prod_deploy, after: "#### Production\n"
33
- insert_into_file "README.md", readme_credentials, after: "#### Credentials and other Secrets\n"
28
+ if file_content("README.md").match?(/^## CI\/CD$/)
29
+ insert_into_file "README.md", readme_cicd, after: "## CI/CD\n"
30
+ insert_into_file "README.md", readme_staging_deploy, after: "#### Staging\n"
31
+ insert_into_file "README.md", readme_prod_deploy, after: "#### Production\n"
32
+ insert_into_file "README.md", readme_credentials, after: "#### Credentials and other Secrets\n"
33
+ else
34
+ append_to_file "README.md", <<~EOM
35
+ ## CI/CD
36
+ #{readme_cicd}
37
+
38
+ ### Deployment
39
+
40
+ #### Staging
41
+ #{readme_staging_deploy}
42
+
43
+ #### Production
44
+ #{readme_prod_deploy}
45
+
46
+ #### Credentials and other Secrets
47
+ #{readme_credentials}
48
+ EOM
49
+ end
34
50
  end
35
51
 
36
52
  def update_boundary_diagram
@@ -0,0 +1,28 @@
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 file_content("Gemfile").match?(/gem "climate_control"/)
17
+ gem_group :test do
18
+ gem "climate_control", "~> 1.0"
19
+ end
20
+ end
21
+
22
+ def install_model_and_test
23
+ copy_file "app/models/cloud_gov_config.rb"
24
+ copy_file "spec/models/cloud_gov_config_spec.rb"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -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