emasser 3.10.0 → 3.22.0

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.env-example +18 -12
  3. data/.github/workflows/anchore-syft.yml +38 -0
  4. data/.github/workflows/codeql-analysis.yml +4 -4
  5. data/.github/workflows/gh-pages.yml +1 -1
  6. data/.github/workflows/push-to-docker-mail.yml +6 -7
  7. data/.github/workflows/push-to-docker.yml +6 -6
  8. data/.github/workflows/release.yml +1 -1
  9. data/.github/workflows/rubocop.yml +2 -2
  10. data/.github/workflows/test-cli.yml +5 -5
  11. data/.mergify.yml +11 -11
  12. data/.rubocop.yml +1 -1
  13. data/CHANGELOG.md +58 -2
  14. data/Dockerfile +6 -4
  15. data/Gemfile.lock +108 -64
  16. data/README.md +23 -22
  17. data/docs/features.md +682 -539
  18. data/emasser.gemspec +19 -13
  19. data/images/emasser_architecture.png +0 -0
  20. data/lib/emasser/configuration.rb +136 -35
  21. data/lib/emasser/constants.rb +4 -4
  22. data/lib/emasser/delete.rb +145 -15
  23. data/lib/emasser/errors.rb +9 -0
  24. data/lib/emasser/get.rb +891 -251
  25. data/lib/emasser/help/approvalCac_post_mapper.md +6 -5
  26. data/lib/emasser/help/approvalPac_post_mapper.md +1 -5
  27. data/lib/emasser/help/artifacts_del_mapper.md +2 -2
  28. data/lib/emasser/help/artifacts_post_mapper.md +23 -34
  29. data/lib/emasser/help/artifacts_put_mapper.md +28 -9
  30. data/lib/emasser/help/cloudresource_post_mapper.md +4 -3
  31. data/lib/emasser/help/controls_put_mapper.md +24 -16
  32. data/lib/emasser/help/hardware_post_mapper.md +41 -0
  33. data/lib/emasser/help/hardware_put_mapper.md +42 -0
  34. data/lib/emasser/help/milestone_del_mapper.md +1 -1
  35. data/lib/emasser/help/milestone_post_mapper.md +3 -1
  36. data/lib/emasser/help/milestone_put_mapper.md +1 -8
  37. data/lib/emasser/help/poam_del_mapper.md +1 -1
  38. data/lib/emasser/help/poam_post_mapper.md +40 -14
  39. data/lib/emasser/help/poam_put_mapper.md +43 -18
  40. data/lib/emasser/help/software_post_mapper.md +59 -0
  41. data/lib/emasser/help/software_put_mapper.md +60 -0
  42. data/lib/emasser/help/staticcode_post_mapper.md +0 -4
  43. data/lib/emasser/help/testresults_post_mapper.md +8 -11
  44. data/lib/emasser/output_converters.rb +64 -46
  45. data/lib/emasser/post.rb +603 -231
  46. data/lib/emasser/put.rb +453 -193
  47. data/lib/emasser/version.rb +1 -1
  48. metadata +51 -33
  49. data/images/emasser_architecture.jpg +0 -0
  50. data/images/emasser_diagram-Page-3.jpg +0 -0
data/lib/emasser/post.rb CHANGED
@@ -24,7 +24,7 @@ class SubCommandBase < Thor
24
24
  # rubocop:enable Style/OptionalBooleanParameter
25
25
  end
26
26
 
27
- # Override thor's long_desc identation behavior
27
+ # Override thor's long_desc indentation behavior
28
28
  class Thor
29
29
  module Shell
30
30
  class Basic
@@ -44,7 +44,7 @@ module Emasser
44
44
  # The Registration endpoint provides the ability to register a certificate & obtain an API-key.
45
45
  #
46
46
  # Endpoint:
47
- # /api/api-key - Register certificate and obtain API key
47
+ # /api/api-key
48
48
  class Register < SubCommandBase
49
49
  def self.exit_on_failure?
50
50
  true
@@ -54,11 +54,11 @@ module Emasser
54
54
  # rubocop:disable Style/RedundantBegin
55
55
  def cert
56
56
  begin
57
- result = EmassClient::RegistrationApi.new.register_user({})
57
+ result = EmassClient::RegistrationApi.new.register_user(Emasser::GET_REGISTER_RETURN_TYPE)
58
58
  puts to_output_hash(result).green
59
59
  rescue EmassClient::ApiError => e
60
60
  puts 'Exception when calling RegistrationApi->register_user'.red
61
- puts to_output_hash(e)
61
+ puts to_output_hash(e).split('\n').join('. ')
62
62
  end
63
63
  end
64
64
  # rubocop:enable Style/RedundantBegin
@@ -68,7 +68,7 @@ module Emasser
68
68
  # system's Assessment Procedures (CCIs) which determine Security Control compliance.
69
69
  #
70
70
  # Endpoint:
71
- # /api/systems/{systemId}/test-results - Add one or many test results for a system
71
+ # /api/systems/{systemId}/test-results
72
72
  class TestResults < SubCommandBase
73
73
  def self.exit_on_failure?
74
74
  true
@@ -78,17 +78,17 @@ module Emasser
78
78
  long_desc Help.text(:testresults_post_mapper)
79
79
 
80
80
  # Required fields
81
- option :systemId, type: :numeric, required: true,
81
+ option :systemId, aliases: '-s', type: :numeric, required: true,
82
82
  desc: 'A numeric value representing the system identification'
83
- option :cci, type: :string, required: true, desc: 'The system CCI string numerical value'
84
- option :testedBy, type: :string, required: true, desc: 'The person that conducted the test (Last Name, First)'
85
- option :testDate, type: :numeric, required: true, desc: 'The date test was conducted, Unix time format.'
86
- option :description, type: :string, required: true, desc: 'The description of test result. 4000 Characters.'
87
- option :complianceStatus, type: :string, required: true, enum: ['Compliant', 'Non-Compliant', 'Not Applicable']
83
+ option :assessmentProcedure, type: :string, required: true, desc: 'The Security Control Assessment Procedure being assessed'
84
+ option :testedBy, type: :string, required: true, desc: 'The person that conducted the test (Last Name, First)'
85
+ option :testDate, type: :numeric, required: true, desc: 'The date test was conducted, Unix time format.'
86
+ option :description, type: :string, required: true, desc: 'The description of test result. 4000 Characters.'
87
+ option :complianceStatus, type: :string, required: true, enum: ['Compliant', 'Non-Compliant', 'Not Applicable']
88
88
 
89
89
  def add
90
90
  body = EmassClient::TestResultsGet.new
91
- body.cci = options[:cci]
91
+ body.assessment_procedure = options[:assessmentProcedure]
92
92
  body.tested_by = options[:testedBy]
93
93
  body.test_date = options[:testDate]
94
94
  body.description = options[:description]
@@ -111,7 +111,7 @@ module Emasser
111
111
  # items to a system.
112
112
  #
113
113
  # Endpoint:
114
- # /api/systems/{systemId}/poams - Add one or many poa&m items in a system
114
+ # /api/systems/{systemId}/poams
115
115
  class Poams < SubCommandBase
116
116
  def self.exit_on_failure?
117
117
  true
@@ -128,7 +128,6 @@ module Emasser
128
128
  # completionDate, milestones (at least 1)
129
129
  # Not Applicable POAM can not be created
130
130
  #--------------------------------------------------------------------------
131
- #
132
131
  # If a POC email is supplied, the application will attempt to locate a user
133
132
  # already registered within the application and pre-populate any information
134
133
  # not explicitly supplied in the request. If no such user is found, these
@@ -139,76 +138,135 @@ module Emasser
139
138
  long_desc Help.text(:poam_post_mapper)
140
139
 
141
140
  # Required parameters/fields (the poamId and displayPoamId are generated by the PUT call)
142
- option :systemId, type: :numeric, required: true, desc: 'A numeric value representing the system identification'
143
- option :status, type: :string, required: true, enum: ['Ongoing', 'Risk Accepted', 'Completed', 'Not Applicable']
141
+ option :systemId, aliases: '-s', type: :numeric, required: true, desc: 'A numeric value representing the system identification'
142
+ option :status, type: :string, required: true, enum: ['Ongoing', 'Risk Accepted', 'Completed', 'Not Applicable', 'Archived']
144
143
  option :vulnerabilityDescription, type: :string, required: true, desc: 'POA&M vulnerability description'
145
- option :sourceIdentVuln,
144
+ option :sourceIdentifyingVulnerability,
146
145
  type: :string, required: true, desc: 'Source that identifies the vulnerability'
147
146
  option :pocOrganization, type: :string, required: true, desc: 'Organization/Office represented'
148
147
  option :resources, type: :string, required: true, desc: 'List of resources used'
149
148
 
149
+ # Some eMASS instances also require the Risk Analysis fields
150
+ # Note: These are grouped here for identification only, they are not marked as required.
151
+ option :severity, type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
152
+ option :relevanceOfThreat, type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
153
+ option :likelihood, type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
154
+ option :impact, type: :string, required: false, desc: 'Description of Security Control’s impact'
155
+ option :residualRiskLevel, type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
156
+ option :mitigations, type: :string, required: false, desc: 'Mitigations explanation'
157
+
150
158
  # Conditional parameters/fields
151
159
  option :milestone, type: :hash, required: false, desc: 'key:values are: description and scheduledCompletionDate'
152
160
  option :pocFirstName, type: :string, required: false, desc: 'First name of POC'
153
161
  option :pocLastName, type: :string, required: false, desc: 'Last name of POC.'
154
162
  option :pocEmail, type: :string, required: false, desc: 'Email address of POC'
155
163
  option :pocPhoneNumber, type: :string, required: false, desc: 'Phone number of POC (area code) ***-**** format'
156
- option :severity, type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
164
+ # option :severity, type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
157
165
  option :scheduledCompletionDate,
158
166
  type: :numeric, required: false, desc: 'The scheduled completion date - Unix time format'
159
167
  option :completionDate,
160
168
  type: :numeric, required: false, desc: 'The schedule completion date - Unix time format'
161
169
  option :comments, type: :string, required: false, desc: 'Comments for completed and risk accepted POA&M items'
170
+ # The next fields are Required for VA. Optional for Army and USCG.
171
+ option :personnelResourcesFundedBaseHours, type: :numeric, required: false, desc: 'Funded based hours (125.34)'
172
+ option :personnelResourcesCostCode, type: :string, required: false, desc: 'Values are specific per eMASS instance'
173
+ option :personnelResourcesUnfundedBaseHours, type: :numeric, required: false, desc: 'Funded based hours (100.00)'
174
+ option :personnelResourcesNonfundingObstacle, type: :string, required: false, desc: 'Values are specific per eMASS instance'
175
+ option :personnelResourcesNonfundingObstacleOtherReason, type: :string, required: false, desc: 'Reason (text 2,000 char)'
176
+ option :nonPersonnelResourcesFundedAmount, type: :numeric, required: false, desc: 'Funded based hours (100.00)'
177
+ option :nonPersonnelResourcesCostCode, type: :string, required: false, desc: 'Values are specific per eMASS instance'
178
+ option :nonPersonnelResourcesUnfundedAmount, type: :numeric, required: false, desc: 'Funded based hours (100.00)'
179
+ option :nonPersonnelResourcesNonfundingObstacle, type: :string, required: false, desc: 'Values are specific per eMASS instance'
180
+ option :nonPersonnelResourcesNonfundingObstacleOtherReason, type: :string, required: false, desc: 'Reason (text 2,000 char)'
162
181
 
163
182
  # Optional parameters/fields
164
183
  option :externalUid, type: :string, required: false, desc: 'External ID associated with the POA&M'
165
184
  option :controlAcronym, type: :string, required: false, desc: 'The system acronym(s) e.g "AC-1, AC-2"'
166
- option :cci, type: :string, required: false, desc: 'The system CCIS string numerical value'
185
+ option :assessmentProcedure, type: :string, required: false, desc: 'The system CCIS string numerical value'
167
186
  option :securityChecks, type: :string, required: false, desc: 'Security Checks that are associated with the POA&M'
168
187
  option :rawSeverity, type: :string, required: false, enum: %w[I II III]
169
- option :relevanceOfThreat,
170
- type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
171
- option :likelihood, type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
172
- option :impact, type: :string, required: false, desc: 'Description of Security Control’s impact'
188
+ # option :relevanceOfThreat, type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
189
+ # option :likelihood, type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
190
+ # option :impact, type: :string, required: false, desc: 'Description of Security Control’s impact'
173
191
  option :impactDescription, type: :string, required: false, desc: 'Description of the security control impact'
174
- option :residualRiskLevel,
175
- type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
176
- option :recommendations, type: :string, required: false, desc: 'Recomendations'
177
- option :mitigation, type: :string, required: false, desc: 'Mitigation explanation'
178
-
179
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
192
+ # option :residualRiskLevel, type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
193
+ option :recommendations, type: :string, required: false, desc: 'Recommendations'
194
+ # option :mitigations, type: :string, required: false, desc: 'Mitigation explanation'
195
+ # The next field is Required for VA. Optional for Army and USCG.
196
+ option :identifiedInCFOAuditOrOtherReview, type: :boolean, required: false, default: false, desc: 'BOOLEAN - true or false.'
197
+ # The next fields are for Navy Only
198
+ option :resultingResidualRiskLevelAfterProposedMitigations, type: :string, required: false, enum: ['Very Low', 'Low', 'Moderate', 'High', 'Very High']
199
+ option :predisposingConditions, type: :string, required: false, desc: 'Conditions (text 2,000 char)'
200
+ option :threatDescription, type: :string, required: false, desc: 'Threat description (text 2,000 char)'
201
+ option :devicesAffected, type: :string, required: false, desc: 'devicesAffected'
202
+
203
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
180
204
  def add
181
- # Required fields
182
- body = EmassClient::PoamGet.new
183
- body.status = options[:status]
184
- body.vulnerability_description = options[:vulnerabilityDescription]
185
- body.source_ident_vuln = options[:sourceIdentVuln]
186
- body.poc_organization = options[:pocOrganization]
187
- body.resources = options[:resources]
205
+ # Check if business logic is satisfied
206
+ process_business_logic
188
207
 
189
- process_business_logic(body)
208
+ # Required fields
209
+ require_fields = EmassClient::PoamRequiredFields.new
210
+ require_fields.status = options[:status]
211
+ require_fields.vulnerability_description = options[:vulnerabilityDescription]
212
+ require_fields.source_identifying_vulnerability = options[:sourceIdentifyingVulnerability]
213
+ require_fields.poc_organization = options[:pocOrganization]
214
+ require_fields.resources = options[:resources]
215
+ # Required for VA, optional for Army and USCG. - defaults to false
216
+ require_fields.identified_in_cfo_audit_or_other_review = options[:identifiedInCFOAuditOrOtherReview] if options[:identifiedInCFOAuditOrOtherReview]
190
217
 
191
218
  # Add conditional fields
192
- body.poc_first_name = options[:pocFirstName] if options[:pocFirstName]
193
- body.poc_last_name = options[:pocLastName] if options[:pocLastName]
194
- body.poc_email = options[:pocEmail] if options[:pocEmail]
195
- body.poc_phone_number = options[:pocPhoneNumber] if options[:pocPhoneNumber]
196
- body.severity = options[:severity] if options[:severity]
219
+ conditional_fields = EmassClient::PoamConditionalFields.new
220
+ conditional_fields.poc_first_name = options[:pocFirstName] if options[:pocFirstName]
221
+ conditional_fields.poc_last_name = options[:pocLastName] if options[:pocLastName]
222
+ conditional_fields.poc_email = options[:pocEmail] if options[:pocEmail]
223
+ conditional_fields.poc_phone_number = options[:pocPhoneNumber] if options[:pocPhoneNumber]
224
+ conditional_fields.severity = options[:severity] if options[:severity]
225
+ conditional_fields.scheduled_completion_date = options[:scheduledCompletionDate] if options[:scheduledCompletionDate]
226
+ conditional_fields.completion_date = options[:completionDate] if options[:completionDate]
227
+ conditional_fields.comments = options[:comments] if options[:comments]
228
+ conditional_fields.personnel_resources_funded_base_hours = options[:personnelResourcesFundedBaseHours] if options[:personnelResourcesFundedBaseHours]
229
+ conditional_fields.personnel_resources_cost_code = options[:personnelResourcesCostCode] if options[:personnelResourcesCostCode]
230
+ conditional_fields.personnel_resources_unfunded_base_hours = options[:personnelResourcesUnfundedBaseHours] if options[:personnelResourcesUnfundedBaseHours]
231
+ conditional_fields.personnel_resources_nonfunding_obstacle = options[:personnelResourcesNonfundingObstacle] if options[:personnelResourcesNonfundingObstacle]
232
+ conditional_fields.personnel_resources_nonfunding_obstacle_other_reason = options[:personnelResourcesNonfundingObstacleOtherReason] if options[:personnelResourcesNonfundingObstacleOtherReason]
233
+ conditional_fields.non_personnel_resources_funded_amount = options[:nonPersonnelResourcesFundedAmount] if options[:nonPersonnelResourcesFundedAmount]
234
+ conditional_fields.non_personnel_resources_cost_code = options[:nonPersonnelResourcesCostCode] if options[:nonPersonnelResourcesCostCode]
235
+ conditional_fields.non_personnel_resources_unfunded_amount = options[:nonPersonnelResourcesUnfundedAmount] if options[:nonPersonnelResourcesUnfundedAmount]
236
+ conditional_fields.non_personnel_resources_nonfunding_obstacle = options[:nonPersonnelResourcesNonfundingObstacle] if options[:nonPersonnelResourcesNonfundingObstacle]
237
+ conditional_fields.non_personnel_resources_nonfunding_obstacle_other_reason = options[:nonPersonnelResourcesNonfundingObstacleOtherReason] if options[:nonPersonnelResourcesNonfundingObstacleOtherReason]
197
238
 
198
239
  # Add optional fields
199
- body.external_uid = options[:externalUid] if options[:externalUid]
200
- body.control_acronyms = options[:controlAcronym] if options[:controlAcronym]
201
- body.cci = options[:cci] if options[:cci]
202
- body.security_checks = options[:securityChecks] if options[:securityChecks]
203
- body.raw_severity = options[:rawSeverity] if options[:rawSeverity]
204
- body.relevance_of_threat = options[:relevanceOfThreat] if options[:relevanceOfThreat]
205
- body.likelihood = options[:likelihood] if options[:likelihood]
206
- body.impact = options[:impact] if options[:impact]
207
- body.impact_description = options[:impactDescription] if options[:impactDescription]
208
- body.residual_risk_level = options[:residualRiskLevel] if options[:residualRiskLevel]
209
- body.recommendations = options[:recommendations] if options[:recommendations]
210
- body.mitigation = options[:mitigation] if options[:mitigation]
211
-
240
+ optional_fields = EmassClient::PoamOptionalFields.new
241
+ optional_fields.external_uid = options[:externalUid] if options[:externalUid]
242
+ optional_fields.control_acronym = options[:controlAcronym] if options[:controlAcronym]
243
+ optional_fields.assessment_procedure = options[:assessmentProcedure] if options[:assessmentProcedure]
244
+ optional_fields.security_checks = options[:securityChecks] if options[:securityChecks]
245
+ optional_fields.raw_severity = options[:rawSeverity] if options[:rawSeverity]
246
+ optional_fields.relevance_of_threat = options[:relevanceOfThreat] if options[:relevanceOfThreat]
247
+ optional_fields.likelihood = options[:likelihood] if options[:likelihood]
248
+ optional_fields.impact = options[:impact] if options[:impact]
249
+ optional_fields.impact_description = options[:impactDescription] if options[:impactDescription]
250
+ optional_fields.residual_risk_level = options[:residualRiskLevel] if options[:residualRiskLevel]
251
+ optional_fields.recommendations = options[:recommendations] if options[:recommendations]
252
+ optional_fields.mitigations = options[:mitigations] if options[:mitigations]
253
+ optional_fields.resulting_residual_risk_level_after_proposed_mitigations = options[:resultingResidualRiskLevelAfterProposedMitigations] if options[:resultingResidualRiskLevelAfterProposedMitigations]
254
+ optional_fields.predisposing_conditions = options[:predisposingConditions] if options[:predisposingConditions]
255
+ optional_fields.threat_description = options[:threatDescription] if options[:threatDescription]
256
+ optional_fields.devices_affected = options[:devicesAffected] if options[:devicesAffected]
257
+
258
+ # Build the milestones object array
259
+ milestone = {}
260
+ milestone['description'] = options[:milestone]['description'] if options[:milestone]['description']
261
+ milestone['scheduledCompletionDate'] = options[:milestone]['scheduledCompletionDate'].to_f if options[:milestone]['scheduledCompletionDate']
262
+ milestone_array = Array.new(1, milestone)
263
+
264
+ # Build the request body
265
+ body = {}
266
+ body = body.merge(require_fields)
267
+ body = body.merge(optional_fields)
268
+ body = body.merge(conditional_fields)
269
+ body = body.merge({ milestones: milestone_array })
212
270
  body_array = Array.new(1, body)
213
271
 
214
272
  begin
@@ -219,11 +277,11 @@ module Emasser
219
277
  puts to_output_hash(e)
220
278
  end
221
279
  end
222
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
280
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
223
281
 
224
- # rubocop:disable Metrics/BlockLength, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
282
+ # rubocop:disable Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
225
283
  no_commands do
226
- def process_business_logic(body)
284
+ def process_business_logic
227
285
  #-----------------------------------------------------------------------------
228
286
  # Conditional fields based on the status field values
229
287
  # "Risk Accepted" comments, resources
@@ -244,8 +302,6 @@ module Emasser
244
302
  puts ' scheduledCompletionDate, or milestone'.red
245
303
  puts POAMS_PUT_HELP_MESSAGE.yellow
246
304
  exit
247
- else
248
- body.comments = options[:comments]
249
305
  end
250
306
  elsif options[:status] == "Ongoing"
251
307
  if options[:scheduledCompletionDate].nil? || options[:milestone].nil?
@@ -255,17 +311,14 @@ module Emasser
255
311
  puts POAMS_POST_HELP_MESSAGE.yellow
256
312
  exit
257
313
  elsif options[:milestone]["description"].nil? || options[:milestone]["scheduledCompletionDate"].nil?
258
- puts 'Missing milstone parameters/fields'.red
314
+ puts 'Missing milestone parameters/fields'.red
259
315
  print_milestone_help
260
316
  exit
261
- else
262
- body.scheduled_completion_date = options[:scheduledCompletionDate]
263
-
264
- milestone = EmassClient::MilestonesRequiredPost.new
265
- milestone.description = options[:milestone]["description"]
266
- milestone.scheduled_completion_date = options[:milestone]["scheduledCompletionDate"]
267
- milestone_array = Array.new(1, milestone)
268
- body.milestones = milestone_array
317
+ elsif options[:severity].nil? || options[:relevanceOfThreat].nil? ||
318
+ options[:likelihood].nil? || options[:impact].nil? ||
319
+ options[:residualRiskLevel].nil? || options[:mitigation].nil?
320
+ puts 'Certain eMASS instances also require the Risk Analysis fields to be populated:'.yellow
321
+ puts ' Severity, Relevance of Threat, Likelihood, Impact, Residual Risk Level, and Mitigations'.yellow
269
322
  end
270
323
  elsif options[:status] == "Completed"
271
324
  if options[:scheduledCompletionDate].nil? || options[:comments].nil? ||
@@ -275,16 +328,6 @@ module Emasser
275
328
  print_milestone_help
276
329
  puts POAMS_POST_HELP_MESSAGE.yellow
277
330
  exit
278
- else
279
- body.scheduled_completion_date = options[:scheduledCompletionDate]
280
- body.comments = options[:comments]
281
- body.completion_date = options[:completionDate]
282
-
283
- milestone = EmassClient::MilestonesRequiredPost.new
284
- milestone.description = options[:milestone]["description"]
285
- milestone.scheduled_completion_date = options[:milestone]["scheduledCompletionDate"]
286
- milestone_array = Array.new(1, milestone)
287
- body.milestones = milestone_array
288
331
  end
289
332
  end
290
333
 
@@ -326,14 +369,14 @@ module Emasser
326
369
  puts ' --milestone description:"[value]" scheduledCompletionDate:"[value]"'.yellow
327
370
  end
328
371
  end
329
- # rubocop:enable Metrics/BlockLength, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
372
+ # rubocop:enable Metrics/BlockLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
330
373
  end
331
374
 
332
375
  # The Milestones endpoints provide the ability add milestones that are associated with
333
376
  # Plan of Action and Milestones (POA&M) items for a system.
334
377
  #
335
378
  # Endpoint:
336
- # /api/systems/{systemId}/poams/{poamId}/milestones - Add milestones in one or many poa&m items in a system
379
+ # /api/systems/{systemId}/poams/{poamId}/milestones
337
380
  class Milestones < SubCommandBase
338
381
  def self.exit_on_failure?
339
382
  true
@@ -343,11 +386,10 @@ module Emasser
343
386
  long_desc Help.text(:milestone_post_mapper)
344
387
 
345
388
  # Required parameters/fields
346
- option :systemId, type: :numeric, required: true, desc: 'A numeric value representing the system identification'
347
- option :poamId, type: :numeric, required: true, desc: 'A numeric value representing the poam identification'
348
- option :description, type: :string, required: true, desc: 'The milestone description'
349
- option :scheduledCompletionDate,
350
- type: :numeric, required: true, desc: 'The scheduled completion date - Unix time format'
389
+ option :systemId, aliases: '-s', type: :numeric, required: true, desc: 'A numeric value representing the system identification'
390
+ option :poamId, aliases: '-p', type: :numeric, required: true, desc: 'A numeric value representing the poam identification'
391
+ option :description, aliases: '-d', type: :string, required: true, desc: 'The milestone description'
392
+ option :scheduledCompletionDate, aliases: '-c', type: :numeric, required: true, desc: 'The scheduled completion date - Unix time format'
351
393
 
352
394
  def add
353
395
  body = EmassClient::MilestonesGet.new
@@ -366,10 +408,17 @@ module Emasser
366
408
  end
367
409
  end
368
410
 
369
- # Add one or many artifacts for a system (delivery method must be a zip file)
411
+ # Add one or many artifacts for a system (delivery method can be a file or a zip file)
412
+ # Two Artifact POST methods are currently accepted: individual and bulk.
413
+ # Filename uniqueness within an eMASS system will be enforced by the API for both methods.
370
414
  #
371
- # Endpoints:
372
- # /api/systems/{systemId}/artifacts - Post one or many artifacts to a system
415
+ # This method handles the upload of one or more files to the eMASS system.
416
+ # If a single file is provided (could be a file or zip file) the file is open (File.open)
417
+ # and passed to the API.
418
+ # It multiple files are provided, they are zipped into a single archive and sent to the API
419
+ #
420
+ # Endpoint:
421
+ # /api/systems/{systemId}/artifacts
373
422
  class Artifacts < SubCommandBase
374
423
  def self.exit_on_failure?
375
424
  true
@@ -379,50 +428,58 @@ module Emasser
379
428
  long_desc Help.text(:artifacts_post_mapper)
380
429
 
381
430
  # Required parameters/fields
382
- option :systemId, type: :numeric, required: true, desc: 'A numeric value representing the system identification'
383
- option :files, type: :array, required: true, desc: 'Artifact file(s) to post to the given system'
384
- option :type,
385
- type: :string, required: true,
386
- enum: ['Procedure', 'Diagram', 'Policy', 'Labor', 'Document',
387
- 'Image', 'Other', 'Scan Result', 'Auditor Report']
388
- option :category, type: :string, required: true, enum: ['Implementation Guidance', 'Evidence']
389
- option :isTemplate, type: :boolean, required: false, default: false, desc: 'BOOLEAN - true or false.'
390
-
391
- # Optional parameters/fields
392
- option :description, type: :string, required: false, desc: 'Artifact description'
393
- option :refPageNumber, type: :string, required: false, desc: 'Artifact reference page number'
394
- option :ccis, type: :string, required: false, desc: 'The system CCIs string numerical value'
395
- option :controls,
396
- type: :string, required: false,
397
- desc: 'Control acronym associated with the artifact. NIST SP 800-53 Revision 4 defined'
398
- option :artifactExpirationDate,
399
- type: :numeric, required: false, desc: 'Date Artifact expires and requires review - Unix time format'
400
- option :lastReviewedDate,
401
- type: :numeric, required: false, desc: 'Date Artifact was last reviewed - Unix time format'
431
+ option :systemId, aliases: '-s', type: :numeric, required: true, desc: 'A numeric value representing the system identification'
432
+ option :files, aliases: '-f', type: :array, required: true, desc: 'Artifact file(s) to post to the given system'
433
+ option :isBulk, aliases: '-B', type: :boolean, require: true, default: false, desc: 'Set to false for single file upload, true for multiple file upload (expects a .zip file)'
434
+
435
+ # Optional parameters/fields - if not provided, default values are used
436
+ # These are the only options the backend will accept, all others are ignored
437
+ option :type, aliases: '-t', type: :string, required: false, default: 'Other',
438
+ desc: 'The type of artifact. Possible values are: Procedure, Diagram, Policy, Labor, Document, Image, Other, Scan Result, Auditor Report. May also accept other values set by system administrators.'
439
+ option :category, aliases: '-c', type: :string, required: false, default: 'Evidence',
440
+ desc: 'The category of artifact. Possible values are: Implementation Guidance, Evidence. May also accept other values set by system administrators.'
441
+ option :isTemplate, aliases: '-T', type: :boolean, required: false, default: false, desc: 'BOOLEAN - true or false.'
402
442
 
403
443
  def upload
404
444
  optional_options_keys = optional_options(@_initializer).keys
405
445
  optional_options = to_input_hash(optional_options_keys, options)
406
- # Remove the isTemplate as we can't use the required = true.
407
- optional_options.delete(:is_template)
446
+ # Remove isBulk as it is an options parameter sent to the API.
447
+ optional_options.delete(:is_bulk)
408
448
 
449
+ # Options contain the default values (type, category, and isTemplate)
450
+ # They are sent to the API in the form_params option
409
451
  opts = {}
410
- opts[:type] = options[:type]
411
- opts[:category] = options[:category]
412
- opts[:is_template] = options[:is_template]
452
+ opts[:is_bulk] = options[:isBulk]
413
453
  opts[:form_params] = optional_options
414
454
 
415
- tempfile = Tempfile.create(['artifacts', '.zip'])
416
-
417
- Zip::OutputStream.open(tempfile.path) do |z|
418
- options[:files].each do |file|
419
- # Add file name to the archive: Don't use the full path
420
- z.put_next_entry(File.basename(file))
421
- # Add the file to the archive
422
- z.print File.read(file)
455
+ # Configure the upload file(s)
456
+ remove_temp_file = false
457
+ begin
458
+ # If we have a single file, could be a zip file
459
+ if options[:files].length == 1
460
+ tempfile = File.open(options[:files][0], 'r')
461
+ # if we have multiple files zip them into a zip file
462
+ elsif options[:files].length > 1
463
+ remove_temp_file = true
464
+ tempfile = Tempfile.create(['artifacts', '.zip'])
465
+
466
+ Zip::OutputStream.open(tempfile.path) do |z|
467
+ options[:files].each do |file|
468
+ # Add file name to the archive: Don't use the full path
469
+ z.put_next_entry(File.basename(file))
470
+ # Add the file to the archive
471
+ z.print File.read(file)
472
+ end
473
+ end
474
+ else
475
+ puts 'No file(s) provided!'.yellow
423
476
  end
477
+ rescue Errno::ENOENT => e
478
+ warn "File open exception: #{e}".red
479
+ exit 1
424
480
  end
425
481
 
482
+ # Call the API
426
483
  begin
427
484
  result = EmassClient::ArtifactsApi
428
485
  .new
@@ -432,9 +489,10 @@ module Emasser
432
489
  puts 'Exception when calling ArtifactsApi->add_artifacts_by_system_id'.red
433
490
  puts to_output_hash(e)
434
491
  ensure
492
+ # Close the file
493
+ tempfile.close
435
494
  # Delete the temp file
436
- unless File.exist? tempfile
437
- tempfile.close
495
+ if remove_temp_file
438
496
  FileUtils.remove_file(tempfile, true)
439
497
  end
440
498
  end
@@ -444,7 +502,7 @@ module Emasser
444
502
  # Add a Control Approval Chain (CAC)
445
503
  #
446
504
  # Endpoints:
447
- # /api/systems/{systemId}/approval/cac - Submit control to second stage of CAC
505
+ # /api/systems/{systemId}/approval/cac
448
506
  class CAC < SubCommandBase
449
507
  def self.exit_on_failure?
450
508
  true
@@ -454,11 +512,11 @@ module Emasser
454
512
  long_desc Help.text(:approvalCac_post_mapper)
455
513
 
456
514
  # Required parameters/fields
457
- option :systemId, type: :numeric, required: true, desc: 'A numeric value representing the system identification'
458
- option :controlAcronym, type: :string, required: true, desc: 'The system acronym "AC-1, AC-2"'
515
+ option :systemId, aliases: '-s', type: :numeric, required: true, desc: 'A numeric value representing the system identification'
516
+ option :controlAcronym, aliases: '-a', type: :string, required: true, desc: 'The system acronym "AC-1, AC-2"'
459
517
 
460
518
  # Conditional parameters/fields
461
- option :comments, type: :string, required: false, desc: 'The control approval chain comments'
519
+ option :comments, aliases: '-c', type: :string, required: false, desc: 'The control approval chain comments'
462
520
 
463
521
  def add
464
522
  body = EmassClient::CacGet.new
@@ -481,7 +539,7 @@ module Emasser
481
539
  # Add a Package Approval Chain (PAC)
482
540
  #
483
541
  # Endpoints:
484
- # /api/systems/{systemId}/approval/pac - Initiate system workflow for review
542
+ # /api/systems/{systemId}/approval/pac
485
543
  class PAC < SubCommandBase
486
544
  def self.exit_on_failure?
487
545
  true
@@ -491,13 +549,10 @@ module Emasser
491
549
  long_desc Help.text(:approvalPac_post_mapper)
492
550
 
493
551
  # Required parameters/fields
494
- option :systemId, type: :numeric, required: true,
495
- desc: 'A numeric value representing the system identification'
496
- option :workflow, type: :string, required: true,
497
- enum: ['Assess and Authorize', 'Assess Only', 'Security Plan Approval']
498
- option :name, type: :string, required: true, desc: 'The control package name'
499
- option :comments, type: :string, required: true,
500
- desc: 'Comments submitted upon initiation of the indicated workflow'
552
+ option :systemId, aliases: '-s', type: :numeric, required: true, desc: 'A numeric value representing the system identification'
553
+ option :workflow, aliases: '-f', type: :string, required: true, enum: ['Assess and Authorize', 'Assess Only', 'Security Plan Approval']
554
+ option :name, aliases: '-n', type: :string, required: true, desc: 'The control package name'
555
+ option :comments, aliases: '-c', type: :string, required: true, desc: 'Comments submitted upon initiation of the indicated workflow'
501
556
 
502
557
  def add
503
558
  body = EmassClient::PacGet.new
@@ -515,105 +570,300 @@ module Emasser
515
570
  end
516
571
  end
517
572
 
518
- # The Static Code Scans endpoint provides the ability to upload application
519
- # scan findings into a system's assets module.
520
- #
521
- # Application findings can also be cleared from the system.
573
+ # Add Hardware Baseline assets for a system
522
574
  #
523
- # Endpoint:
524
- # /api/systems/{systemId}/static-code-scans - Upload static code scans
525
- class ScanFindings < SubCommandBase
575
+ # Endpoints:
576
+ # /api/systems/{systemId}/hw-baseline - Add one or many hardware assets in a system
577
+ class Hardware < SubCommandBase
526
578
  def self.exit_on_failure?
527
579
  true
528
580
  end
529
581
 
530
- desc 'add', 'Upload static code scans'
531
- long_desc Help.text(:staticcode_post_mapper)
582
+ desc 'add', 'Add one or many hardware assets in a system'
583
+ long_desc Help.text(:hardware_post_mapper)
532
584
 
533
585
  # Required parameters/fields
534
- option :systemId, type: :numeric, required: true, desc: 'A numeric value representing the system identification'
535
- option :applicationName, type: :string, required: true, desc: 'Name of the software application that was assessed'
536
- option :version, type: :string, required: true, desc: 'The version of the application'
537
- option :codeCheckName, type: :string, required: true, desc: 'Name of the software vulnerability or weakness'
538
- option :scanDate, type: :numeric, required: true, desc: 'The findings scan date - Unix time format'
539
- option :cweId, type: :string, required: true, desc: 'The Common Weakness Enumerator (CWE) identifier'
540
- option :count, type: :numeric, required: true, desc: 'Number of instances observed for a specified finding'
541
- # Optional parameter/fields
542
- option :rawSeverity, type: :string, required: false, enum: %w[Low Medium Moderate High Critical]
586
+ option :systemId, aliases: '-s', type: :numeric, required: true, desc: 'A numeric value representing the system identification'
587
+ option :assetName, aliases: '-a', type: :string, required: true, desc: 'Name of the hardware asset'
588
+
589
+ # Conditional fields
590
+ option :publicFacingFqdn, type: :string, required: false, desc: 'Public facing FQDN. Only applicable if Public Facing is set to true'
591
+ option :publicFacingIpAddress, type: :string, required: false, desc: 'Public facing IP address. Only applicable if Public Facing is set to true'
592
+ option :publicFacingUrls, type: :string, required: false, desc: 'Public facing URL(s). Only applicable if Public Facing is set to true'
593
+
594
+ # Optional fields
595
+ option :componentType, type: :string, required: false, desc: 'Component type of the hardware asset'
596
+ option :nickname, type: :string, required: false, desc: 'Nickname of the hardware asset'
597
+ option :assetIpAddress, type: :string, required: false, desc: 'IP address of the hardware asset'
598
+ option :publicFacing, type: :boolean, required: false, desc: 'Public facing is defined as any asset that is accessible from a commercial connection'
599
+ option :virtualAsset, type: :boolean, required: false, default: false, desc: 'Determine if this is a virtual hardware asset'
600
+ option :manufacturer, type: :string, required: false, desc: 'Manufacturer of the hardware asset. Populated with “Virtual” by default if Virtual Asset is true'
601
+ option :modelNumber, type: :string, required: false, desc: 'Model number of the hardware asset. Populated with “Virtual” by default if Virtual Asset is true'
602
+ option :serialNumber, type: :string, required: false, desc: 'Serial number of the hardware asset. Populated with “Virtual” by default if Virtual Asset is true'
603
+ option :OsIosFwVersion, type: :string, required: false, desc: 'OS/iOS/FW version of the hardware asset'
604
+ option :memorySizeType, type: :string, required: false, desc: 'Memory size / type of the hardware asset'
605
+ option :location, type: :string, required: false, desc: 'Location of the hardware asset'
606
+ option :approvalStatus, type: :string, required: false, desc: 'Approval status of the hardware asset'
607
+ option :criticalAsset, type: :boolean, required: false, default: false, desc: 'Indicates whether the asset is a critical information system asset'
543
608
 
609
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
544
610
  def add
545
- application = EmassClient::StaticCodeRequestPostBodyApplication.new
546
- application.application_name = options[:applicationName]
547
- application.version = options[:version]
548
-
549
- application_findings = EmassClient::StaticCodeApplication.new
550
- application_findings.code_check_name = options[:codeCheckName]
551
- application_findings.scan_date = options[:scanDate]
552
- application_findings.cwe_id = options[:cweId]
553
- application_findings.count = options[:count]
554
- application_findings.raw_severity = options[:rawSeverity] if options[:rawSeverity]
555
-
556
- app_findings_array = Array.new(1, application_findings)
611
+ # Required fields
612
+ require_field = EmassClient::HwBaselineRequiredFields.new
613
+ require_field.asset_name = options[:assetName]
557
614
 
558
- body = EmassClient::StaticCodeRequestPostBody.new
559
- body.application = application
560
- body.application_findings = app_findings_array
615
+ # Conditional fields
616
+ conditional_fields = EmassClient::HwBaselineConditionalFields.new
617
+ conditional_fields.public_facing_fqdn = options[:publicFacingFqdn] if options[:publicFacingFqdn]
618
+ conditional_fields.public_facing_ip_address = options[:publicFacingIpAddress] if options[:publicFacingIpAddress]
619
+ conditional_fields.public_facing_urls = options[:publicFacingUrls] if options[:publicFacingUrls]
561
620
 
621
+ # Optional fields
622
+ optional_fields = EmassClient::HwBaselineOptionalFields.new
623
+ optional_fields.component_type = options[:componentType] if options[:componentType]
624
+ optional_fields.nickname = options[:nickname] if options[:nickname]
625
+ optional_fields.asset_ip_address = options[:assetIpAddress] if options[:assetIpAddress]
626
+ optional_fields.public_facing = options[:publicFacing] if options[:publicFacing]
627
+ optional_fields.virtual_asset = options[:virtualAsset] if options[:virtualAsset]
628
+ optional_fields.manufacturer = options[:manufacturer] if options[:manufacturer]
629
+ optional_fields.model_number = options[:modelNumber] if options[:modelNumber]
630
+ optional_fields.serial_number = options[:serialNumber] if options[:serialNumber]
631
+ optional_fields.os_ios_fw_version = options[:OsIosFwVersion] if options[:OsIosFwVersion]
632
+ optional_fields.memory_size_type = options[:memorySizeType] if options[:memorySizeType]
633
+ optional_fields.location = options[:location] if options[:location]
634
+ optional_fields.approval_status = options[:approvalStatus] if options[:approvalStatus]
635
+ optional_fields.critical_asset = options[:criticalAsset] if options[:criticalAsset]
636
+
637
+ # Build the body array
638
+ body = {}
639
+ body = body.merge(require_field)
640
+ body = body.merge(conditional_fields)
641
+ body = body.merge(optional_fields)
562
642
  body_array = Array.new(1, body)
563
643
 
564
- begin
565
- result = EmassClient::StaticCodeScansApi
566
- .new.add_static_code_scans_by_system_id(options[:systemId], body_array)
567
- puts to_output_hash(result).green
568
- rescue EmassClient::ApiError => e
569
- puts 'Exception when calling StaticCodeScansApi->add_static_code_scans_by_system_id'.red
570
- puts to_output_hash(e)
571
- end
644
+ # Call the API
645
+ result = EmassClient::HardwareBaselineApi.new.add_hw_baseline_assets(options[:systemId], body_array)
646
+ puts to_output_hash(result).green
647
+ rescue EmassClient::ApiError => e
648
+ puts 'Exception when calling HardwareBaselineApi->add_hw_baseline_assets'.red
649
+ puts to_output_hash(e)
572
650
  end
651
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
652
+ end
573
653
 
574
- # CLEAR ------------------------------------------------------------------------------------
575
- desc 'clear', 'Clear an application findings'
576
- long_desc Help.text(:staticcode_clear_mapper)
654
+ # Add Software Baseline assets for a system
655
+ #
656
+ # Endpoints:
657
+ # /api/systems/{systemId}/sw-baseline
658
+ class Software < SubCommandBase
659
+ def self.exit_on_failure?
660
+ true
661
+ end
662
+
663
+ desc 'add', 'Add one or many software assets into a system'
664
+ long_desc Help.text(:software_post_mapper)
577
665
 
578
666
  # Required parameters/fields
579
- option :systemId, type: :numeric, required: true, desc: 'A numeric value representing the system identification'
580
- option :applicationName, type: :string, required: true, desc: 'Name of the software application that was assessed'
581
- option :version, type: :string, required: true, desc: 'The version of the application'
582
- option :clearFindings, type: :boolean, required: false, default: false, desc: 'BOOLEAN - true or false'
583
- # NOTE: clearFindings is a required parameter to clear an application's findings, however Thor does not allow
584
- # a boolean type to be required because it automatically creates a --no-clearFindings option for clearFindings=false
667
+ option :systemId, aliases: '-s', type: :numeric, required: true, desc: 'A numeric value representing the system identification'
668
+ option :softwareVendor, aliases: '-V', type: :string, required: true, desc: 'Vendor of the software asset'
669
+ option :softwareName, aliases: '-N', type: :string, required: true, desc: 'Name of the software asset'
670
+ option :version, aliases: '-v', type: :string, required: true, desc: 'Version of the software asset'
671
+
672
+ # Conditional field
673
+ # If Approval Status is set to “Unapproved” or “In Progress”, Approval Date will be set to null.
674
+ option :approvalDate, type: :numeric, required: false, desc: 'Approval date of the software asset.'
675
+
676
+ # Optional fields
677
+ option :softwareType, type: :string, required: false, desc: 'Type of the software asset'
678
+ option :parentSystem, type: :string, required: false, desc: 'Parent system of the software asset'
679
+ option :subsystem, type: :string, required: false, desc: 'Subsystem of the software asset'
680
+ option :network, type: :string, required: false, desc: 'Network of the software asset'
681
+ option :hostingEnvironment, type: :string, required: false, desc: 'Hosting environment of the software asset'
682
+ option :softwareDependencies, type: :string, required: false, desc: 'Dependencies for the software asset'
683
+ option :cryptographicHash, type: :string, required: false, desc: 'Cryptographic hash for the software asset'
684
+ option :inServiceData, type: :string, required: false, desc: 'In service data for the software asset'
685
+ option :itBudgetUii, type: :string, required: false, desc: 'IT budget UII for the software asset'
686
+ option :fiscalYear, type: :string, required: false, desc: 'Fiscal year (FY) for the software asset'
687
+ option :popEndDate, type: :numeric, required: false, desc: 'Period of performance (POP) end date for the software asset'
688
+ option :licenseOrContract, type: :string, required: false, desc: 'License or contract for the software asset'
689
+ option :licenseTerm, type: :string, required: false, desc: 'License term for the software asset'
690
+ option :costPerLicense, type: :numeric, required: false, desc: 'Cost per license for the software asset'
691
+ option :totalLicenses, type: :numeric, required: false, desc: 'Number of total licenses for the software asset'
692
+ option :totalLicenseCost, type: :numeric, required: false, desc: 'Total cost of the licenses for the software asset'
693
+ option :licensesUsed, type: :numeric, required: false, desc: 'Number of licenses used for the software asset'
694
+ option :licensePoc, type: :string, required: false, desc: 'Point of contact (POC) for the software asset'
695
+ option :licenseRenewalDate, type: :numeric, required: false, desc: 'License renewal date for the software asset'
696
+ option :licenseExpirationDate, type: :numeric, required: false, desc: 'License expiration date for the software asset'
697
+ option :approvalStatus, type: :string, required: false, desc: 'Approval status of the software asset'
698
+ option :releaseDate, type: :numeric, required: false, desc: 'Release date of the software asset'
699
+ option :maintenanceDate, type: :numeric, required: false, desc: 'Maintenance date of the software asset'
700
+ option :retirementDate, type: :numeric, required: false, desc: 'Retirement date of the software asset'
701
+ option :endOfLifeSupportDate, type: :numeric, required: false, desc: 'End of life/support date of the software asset'
702
+ option :extendedEndOfLifeSupportDate, type: :numeric, required: false, desc: 'Extended End of Life/Support Date cannot occur prior to the End of Life/Support Date'
703
+ option :criticalAsset, type: :boolean, required: false, default: false, desc: 'Indicates whether the asset is a critical information system asset'
704
+ option :location, type: :string, required: false, desc: 'Location of the software asset'
705
+ option :purpose, type: :string, required: false, desc: 'Purpose of the software asset'
706
+ # VA only
707
+ option :unsupportedOperatingSystem, type: :boolean, required: false, default: false, desc: 'Unsupported operating system'
708
+ option :unapprovedSoftwareFromTrm, type: :boolean, required: false, default: false, desc: 'Unapproved software from TRM'
709
+ option :approvedWaiver, type: :boolean, required: false, default: false, desc: 'Approved waiver'
710
+
711
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
712
+ def add
713
+ # Required fields
714
+ require_field = EmassClient::SwBaselineRequiredFields.new
715
+ require_field.software_vendor = options[:softwareVendor]
716
+ require_field.software_name = options[:softwareName]
717
+ require_field.version = options[:version]
585
718
 
586
- def clear
587
- unless options[:clearFindings]
588
- puts 'To clear an application findings, the field clearFindings (--clearFindings) is required'.red
589
- puts SCAN_POST_HELP_MESSAGE.yellow
590
- exit
591
- end
719
+ # Conditional fields
720
+ conditional_fields = EmassClient::SwBaselineConditionalFields.new
721
+ conditional_fields.approval_date = options[:approvalDate] if options[:approvalDate]
592
722
 
593
- application = EmassClient::StaticCodeRequestPostBodyApplication.new
594
- application.application_name = options[:applicationName]
595
- application.version = options[:version]
723
+ # Optional fields
724
+ optional_fields = EmassClient::SwBaselineOptionalFields.new
725
+ optional_fields.software_type = options[:softwareType] if options[:softwareType]
726
+ optional_fields.parent_system = options[:parentSystem] if options[:parentSystem]
727
+ optional_fields.subsystem = options[:subsystem] if options[:subsystem]
728
+ optional_fields.network = options[:network] if options[:network]
729
+ optional_fields.hosting_environment = options[:hostingEnvironment] if options[:hostingEnvironment]
730
+ optional_fields.software_dependencies = options[:softwareDependencies] if options[:softwareDependencies]
731
+ optional_fields.cryptographic_hash = options[:cryptographicHash] if options[:cryptographicHash]
732
+ optional_fields.in_service_data = options[:inServiceData] if options[:inServiceData]
733
+ optional_fields.it_budget_uii = options[:itBudgetUii] if options[:itBudgetUii]
734
+ optional_fields.fiscal_year = options[:fiscalYear] if options[:fiscalYear]
735
+ optional_fields.pop_end_date = options[:popEndDate] if options[:popEndDate]
736
+ optional_fields.license_or_contract = options[:licenseOrContract] if options[:licenseOrContract]
737
+ optional_fields.license_term = options[:licenseTerm] if options[:licenseTerm]
738
+ optional_fields.cost_per_license = options[:costPerLicense] if options[:costPerLicense]
739
+ optional_fields.total_licenses = options[:totalLicenses] if options[:totalLicenses]
740
+ optional_fields.total_license_cost = options[:totalLicenseCost] if options[:totalLicenseCost]
741
+ optional_fields.licenses_used = options[:licensesUsed] if options[:licensesUsed]
742
+ optional_fields.license_poc = options[:licensePoc] if options[:licensePoc]
743
+ optional_fields.license_renewal_date = options[:licenseRenewalDate] if options[:licenseRenewalDate]
744
+ optional_fields.license_expiration_date = options[:licenseExpirationDate] if options[:licenseExpirationDate]
745
+ optional_fields.approval_status = options[:approvalStatus] if options[:approvalStatus]
746
+ optional_fields.release_date = options[:releaseDate] if options[:releaseDate]
747
+ optional_fields.maintenance_date = options[:maintenanceDate] if options[:maintenanceDate]
748
+ optional_fields.retirement_date = options[:retirementDate] if options[:retirementDate]
749
+ optional_fields.end_of_life_support_date = options[:endOfLifeSupportDate] if options[:endOfLifeSupportDate]
750
+ optional_fields.extended_end_of_life_support_date = options[:extendedEndOfLifeSupportDate] if options[:extendedEndOfLifeSupportDate]
751
+ optional_fields.critical_asset = options[:criticalAsset] if options[:criticalAsset]
752
+ optional_fields.location = options[:location] if options[:location]
753
+ optional_fields.purpose = options[:purpose] if options[:purpose]
754
+ # VA only.
755
+ optional_fields.unsupported_operating_system = options[:unsupportedOperatingSystem] if options[:unsupportedOperatingSystem]
756
+ optional_fields.unapproved_software_from_trm = options[:unapprovedSoftwareFromTrm] if options[:unapprovedSoftwareFromTrm]
757
+ optional_fields.approved_waiver = options[:approvedWaiver] if options[:approvedWaiver]
758
+
759
+ # Build the body array
760
+ body = {}
761
+ body = body.merge(require_field)
762
+ body = body.merge(conditional_fields)
763
+ body = body.merge(optional_fields)
764
+ body_array = Array.new(1, body)
596
765
 
597
- application_findings = EmassClient::StaticCodeApplication.new
598
- application_findings.clear_findings = options[:clearFindings]
766
+ # Call the API
767
+ result = EmassClient::SoftwareBaselineApi.new.add_sw_baseline_assets(options[:systemId], body_array)
768
+ puts to_output_hash(result).green
769
+ rescue EmassClient::ApiError => e
770
+ puts 'Exception when calling SoftwareBaselineApi->add_sw_baseline_assets'.red
771
+ puts to_output_hash(e)
772
+ end
773
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
774
+ end
599
775
 
600
- app_findings_array = Array.new(1, application_findings)
776
+ # Upload device scans (delivery method can be a file or a zip file)
777
+ #
778
+ # The body of a request for this endpoint accepts a single binary file.
779
+ # Specific file extensions are expected depending upon the scanType parameter.
780
+ # For example, .ckl or .cklb files are accepted when using scanType is set to
781
+ # disaStigViewerCklCklb.
782
+ #
783
+ # When set to acasAsrArf or policyAuditor, a .zip file is expected which
784
+ # should contain a single scan result (for example, a single pair of .asr and
785
+ # .arf files).
786
+ #
787
+ # Single files are expected for all other scan types as this endpoint requires
788
+ # files to be uploaded consecutively as opposed to in bulk.
789
+ #
790
+ # Current scan types that are supported:
791
+ # • ACAS: ASR/ARF
792
+ # • ACAS: NESSUS
793
+ # • DISA STIG Viewer: CKL/CKLB
794
+ # • DISA STIG Viewer: CMRS
795
+ # • Policy Auditor
796
+ # • SCAP Compliance Checker
797
+ #
798
+ # Endpoint:
799
+ # /api/systems/{systemId}/device-scan-results
800
+ class DeviceScans < SubCommandBase
801
+ def self.exit_on_failure?
802
+ true
803
+ end
601
804
 
602
- body = EmassClient::StaticCodeRequestPostBody.new
603
- body.application = application
604
- body.application_findings = app_findings_array
805
+ desc 'upload', 'Uploads device scans into a system'
605
806
 
606
- body_array = Array.new(1, body)
807
+ # Required parameters/fields
808
+ option :systemId, aliases: '-s', type: :numeric, required: true, desc: 'A numeric value representing the system identification'
809
+ option :filename, aliases: '-f', type: :string, required: true, desc: 'The device scan file'
810
+ option :scanType, aliases: '-t', type: :string, required: true, desc: 'The device scan type to upload',
811
+ enum: %w{acasAsrArf acasNessus disaStigViewerCklCklb disaStigViewerCmrs policyAuditor scapComplianceChecker}
812
+
813
+ # Optional parameters/fields - if not provided, default values are used
814
+ option :isBaseline, aliases: '-B', type: :boolean, required: false, default: false, desc: 'BOOLEAN - true or false.'
815
+
816
+ def upload
817
+ # Check if business logic is satisfied
818
+ process_business_logic
819
+
820
+ # Options contain the default values (type, category, and isTemplate)
821
+ # They are sent to the API in the form_params option
822
+ opts = {}
823
+ opts[:isBaseline] = options[:isBaseline] if options[:isBaseline]
607
824
 
825
+ # Configure the upload file
608
826
  begin
609
- result = EmassClient::StaticCodeScansApi
610
- .new.add_static_code_scans_by_system_id(options[:systemId], body_array)
827
+ # If we have a single file, could be a zip file
828
+ if options[:filename]
829
+ tempfile = File.open(options[:filename], 'r')
830
+ else
831
+ puts 'One (1) file is expected!'.yellow
832
+ end
833
+ rescue Errno::ENOENT => e
834
+ warn "File open exception: #{e}".red
835
+ exit 1
836
+ end
837
+
838
+ # Call the API
839
+ begin
840
+ result = EmassClient::DeviceScanResultsApi
841
+ .new
842
+ .add_scan_results_by_system_id(options[:systemId], options[:scanType], tempfile, opts)
611
843
  puts to_output_hash(result).green
612
844
  rescue EmassClient::ApiError => e
613
- puts 'Exception when calling StaticCodeScansApi->add_static_code_scans_by_system_id'.red
845
+ puts 'Exception when calling DeviceScanResultsApi->add_scan_results_by_system_id'.red
614
846
  puts to_output_hash(e)
615
847
  end
616
848
  end
849
+
850
+ # rubocop:disable Style/MultipleComparison
851
+ no_commands do
852
+ def process_business_logic
853
+ # If scanType is set to disaStigViewerCklCklb a .ckl or .cklb file is expect
854
+ if options[:scanType] == 'disaStigViewerCklCklb' && !options[:filename].index('.ckl')
855
+ puts 'If the scan type is "disaStigViewerCklCklb" a .ckl or .cklb file is expected'.red
856
+ exit
857
+ # If scanType is set to acasAsrArf or policyAuditor a .zip file is expect
858
+ elsif options[:scanType] == 'acasAsrArf' || options[:scanType] == 'policyAuditor'
859
+ if !options[:filename].index('.zip')
860
+ puts 'If the scan type is "acasAsrArf or policyAuditor" a .zip file is expected'.red
861
+ exit
862
+ end
863
+ end
864
+ end
865
+ # rubocop:enable Style/MultipleComparison
866
+ end
617
867
  end
618
868
 
619
869
  # The Cloud Resources endpoint provides the ability to upload (add)
@@ -621,7 +871,7 @@ module Emasser
621
871
  #
622
872
  #
623
873
  # Endpoint:
624
- # /api/systems/{systemId}/cloud-resources-results - Upload cloud resources and their scan results
874
+ # /api/systems/{systemId}/cloud-resources-results
625
875
  class CloudResource < SubCommandBase
626
876
  def self.exit_on_failure?
627
877
  true
@@ -636,7 +886,7 @@ module Emasser
636
886
  option :resourceId, type: :string, required: true, desc: 'Unique identifier/resource namespace for policy compliance result'
637
887
  option :resourceName, type: :string, required: true, desc: 'Friendly name of Cloud resource'
638
888
  option :resourceType, type: :string, required: true, desc: 'Type of Cloud resource'
639
- # ComplianceResults Array Objects
889
+ # compliance_results Array Objects (booleans cannot be required)
640
890
  option :cspPolicyDefinitionId, type: :string, required: true, desc: 'Unique identifier/compliance namespace for CSP/Resource\'s policy definition/compliance check'
641
891
  option :isCompliant, type: :boolean, required: false, default: false, desc: 'BOOLEAN - true or false'
642
892
  option :policyDefinitionTitle, type: :string, required: true, desc: 'Friendly policy/compliance check title. Recommend short title'
@@ -645,10 +895,10 @@ module Emasser
645
895
  option :initiatedBy, type: :string, required: false, desc: 'Email of POC'
646
896
  option :cspAccountId, type: :string, required: false, desc: 'System/owner\'s CSP account ID/number'
647
897
  option :cspRegion, type: :string, required: false, desc: 'CSP region of system'
648
- option :isBaseline, type: :boolean, required: false, default: true, desc: 'BOOLEAN - true or false'
898
+ option :isBaseline, type: :boolean, required: false, default: false, desc: 'BOOLEAN - true or false'
649
899
  # Tags Object
650
900
  option :test, type: :string, required: false, desc: 'The test tag'
651
- # ComplianceResults Array Objects
901
+ # compliance_results Array Objects
652
902
  option :assessmentProcedure, type: :string, required: false, desc: 'Comma separated correlation to Assessment Procedure (i.e. CCI number for DoD Control Set)'
653
903
  option :complianceCheckTimestamp, type: :numeric, required: false, desc: 'The compliance timestamp Unix date format.'
654
904
  option :complianceReason, type: :string, required: false, desc: 'Reason/comments for compliance result'
@@ -688,16 +938,16 @@ module Emasser
688
938
  compliance_results[:policyDeploymentName] = options[:policyDeploymentName] if options[:policyDeploymentName]
689
939
  compliance_results[:policyDeploymentVersion] = options[:policyDeploymentVersion] if options[:policyDeploymentVersion]
690
940
  compliance_results[:severity] = options[:severity] if options[:severity]
691
-
692
941
  compliance_results_array = Array.new(1, compliance_results)
693
942
 
943
+ # Build the body array
694
944
  body[:tags] = tags
695
945
  body[:complianceResults] = compliance_results_array
696
-
697
946
  body_array = Array.new(1, body)
698
947
 
948
+ # Call the API
699
949
  begin
700
- result = EmassClient::CloudResourcesApi
950
+ result = EmassClient::CloudResourceResultsApi
701
951
  .new.add_cloud_resources_by_system_id(options[:systemId], body_array)
702
952
  puts to_output_hash(result).green
703
953
  rescue EmassClient::ApiError => e
@@ -713,7 +963,7 @@ module Emasser
713
963
  #
714
964
  #
715
965
  # Endpoint:
716
- # /api/systems/{systemId}/container-scan-results - Upload containers and their scan results
966
+ # /api/systems/{systemId}/container-scan-results
717
967
  class Container < SubCommandBase
718
968
  def self.exit_on_failure?
719
969
  true
@@ -727,9 +977,9 @@ module Emasser
727
977
  option :containerId, type: :string, required: true, desc: 'Unique identifier of the container'
728
978
  option :containerName, type: :string, required: true, desc: 'Friendly name of the container'
729
979
  option :time, type: :numeric, required: true, desc: 'Datetime of scan/result. Unix date format'
730
- # Benchmarks Array Objects
980
+ # Benchmarks Array Object - Required
731
981
  option :benchmark, type: :string, required: true, desc: 'Identifier of the benchmark/grouping of compliance results'
732
- # Benchmarks.Results Array Objects
982
+ # Benchmarks.Results Array Object - Required
733
983
  option :lastSeen, type: :numeric, required: true, desc: 'Date last seen, Unix date format'
734
984
  option :ruleId, type: :string, required: true, desc: 'Identifier for the compliance result, vulnerability, etc. the result is for'
735
985
  option :status, type: :string, required: true, enum: ['Pass', 'Fail', 'Other', 'Not Reviewed', 'Not Checked', 'Not Applicable']
@@ -738,62 +988,175 @@ module Emasser
738
988
  option :namespace, type: :string, required: false, desc: 'Namespace of container in container orchestration'
739
989
  option :podIp, type: :string, required: false, desc: 'IP address of the pod'
740
990
  option :podName, type: :string, required: false, desc: 'Name of pod (e.g. Kubernetes pod)'
741
- # Tags Object
991
+
992
+ # Tags Object - Optional
742
993
  option :test, type: :string, required: false, desc: 'The test tag'
743
- # Benchmarks Array Objects
994
+
995
+ # Benchmarks Array Objects Optional
744
996
  option :isBaseline, type: :boolean, required: false, default: true, desc: 'BOOLEAN - true or false'
745
- # Benchmarks.Results Array Objects
997
+ option :version, type: :numeric, required: false, desc: 'The benchmark version'
998
+ option :release, type: :numeric, required: false, desc: 'The benchmark release'
999
+
1000
+ # Benchmarks.Results Array Object - Optional
746
1001
  option :message, type: :string, required: false, desc: 'Benchmark result comments'
747
1002
 
748
- # rubocop:disable Metrics/CyclomaticComplexity
1003
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
749
1004
  def add
750
- # Required and Optional main fields
1005
+ # Required fields
751
1006
  body = {}
752
1007
  body[:containerId] = options[:containerId]
753
1008
  body[:containerName] = options[:containerName]
754
1009
  body[:time] = options[:time]
1010
+ # Optional fields
755
1011
  body[:namespace] = options[:namespace] if options[:namespace]
756
1012
  body[:podIp] = options[:podIp] if options[:podIp]
757
1013
  body[:podName] = options[:podName] if options[:podName]
758
1014
 
759
- # Optional tags field
1015
+ # Tags - Optional field
760
1016
  tags = {}
761
1017
  tags[:test] = options[:test] if options[:test]
762
1018
 
763
- # Required and Optional Benchmarks fields
1019
+ # Benchmarks - Required field
764
1020
  benchmarks = {}
765
1021
  benchmarks[:benchmark] = options[:benchmark]
766
- # Optional fields
1022
+ # Benchmarks - Optional fields
767
1023
  benchmarks[:isBaseline] = options[:isBaseline] if options[:isBaseline]
1024
+ benchmarks[:version] = options[:version] if options[:version]
1025
+ benchmarks[:release] = options[:release] if options[:release]
768
1026
 
769
- # Required and Optional Benchmarks.Results
1027
+ # Benchmarks.Results - Required fields
770
1028
  benchmarks_results = {}
771
1029
  benchmarks_results[:lastSeen] = options[:lastSeen]
772
1030
  benchmarks_results[:ruleId] = options[:ruleId]
773
1031
  benchmarks_results[:status] = options[:status]
1032
+ # Benchmarks.Results - Optional field
774
1033
  benchmarks_results[:message] = options[:message] if options[:message]
775
1034
 
776
1035
  # Add Benchmark results to an array and add array to benchmarks object
777
1036
  benchmarks_results_array = Array.new(1, benchmarks_results)
778
- benchmarks[:results] = benchmarks_results_array # = Array.new(1, benchmarks_results)
1037
+ benchmarks[:results] = benchmarks_results_array
1038
+
779
1039
  # Add benchmarks object to an array
780
1040
  benchmarks_array = Array.new(1, benchmarks)
781
1041
  # Add tags and benchmark ojects to body object
782
- body[:tags] = tags
1042
+ body[:tags] = tags if tags.any?
783
1043
  body[:benchmarks] = benchmarks_array
784
1044
 
1045
+ # Build the body array
785
1046
  body_array = Array.new(1, body)
786
1047
 
1048
+ # Call the API
787
1049
  begin
788
- result = EmassClient::ContainersApi
1050
+ result = EmassClient::ContainerScanResultsApi
789
1051
  .new.add_container_sans_by_system_id(options[:systemId], body_array)
790
1052
  puts to_output_hash(result).green
791
1053
  rescue EmassClient::ApiError => e
792
- puts 'Exception when calling StaticCodeScansApi->add_container_sans_by_system_id'.red
1054
+ puts 'Exception when calling ContainerScanResultsApi->add_container_sans_by_system_id'.red
1055
+ puts to_output_hash(e)
1056
+ end
1057
+ end
1058
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1059
+ end
1060
+
1061
+ # The Static Code Scans endpoint provides the ability to upload application
1062
+ # scan findings into a system's assets module.
1063
+ #
1064
+ # Application findings can also be cleared from the system.
1065
+ #
1066
+ # Endpoint:
1067
+ # /api/systems/{systemId}/static-code-scans - Upload static code scans
1068
+ class ScanFindings < SubCommandBase
1069
+ def self.exit_on_failure?
1070
+ true
1071
+ end
1072
+
1073
+ desc 'add', 'Upload static code scans'
1074
+ long_desc Help.text(:staticcode_post_mapper)
1075
+
1076
+ # Required parameters/fields
1077
+ option :systemId, type: :numeric, required: true, desc: 'A numeric value representing the system identification'
1078
+ option :applicationName, type: :string, required: true, desc: 'Name of the software application that was assessed'
1079
+ option :version, type: :string, required: true, desc: 'The version of the application'
1080
+ option :codeCheckName, type: :string, required: true, desc: 'Name of the software vulnerability or weakness'
1081
+ option :scanDate, type: :numeric, required: true, desc: 'The findings scan date - Unix time format'
1082
+ option :cweId, type: :string, required: true, desc: 'The Common Weakness Enumerator (CWE) identifier'
1083
+ option :count, type: :numeric, required: true, desc: 'Number of instances observed for a specified finding'
1084
+ # Optional parameter/fields
1085
+ option :rawSeverity, type: :string, required: false, enum: %w[Low Medium Moderate High Critical]
1086
+
1087
+ def add
1088
+ application = EmassClient::StaticCodeRequestPostBodyApplication.new
1089
+ application.application_name = options[:applicationName]
1090
+ application.version = options[:version]
1091
+
1092
+ application_findings = EmassClient::StaticCodeApplicationPost.new
1093
+ application_findings.code_check_name = options[:codeCheckName]
1094
+ application_findings.scan_date = options[:scanDate]
1095
+ application_findings.cwe_id = options[:cweId]
1096
+ application_findings.count = options[:count]
1097
+ application_findings.raw_severity = options[:rawSeverity] if options[:rawSeverity]
1098
+
1099
+ app_findings_array = Array.new(1, application_findings)
1100
+
1101
+ body = EmassClient::StaticCodeRequestPostBody.new
1102
+ body.application = application
1103
+ body.application_findings = app_findings_array
1104
+
1105
+ body_array = Array.new(1, body)
1106
+
1107
+ begin
1108
+ result = EmassClient::StaticCodeScansApi
1109
+ .new.add_static_code_scans_by_system_id(options[:systemId], body_array)
1110
+ puts to_output_hash(result).green
1111
+ rescue EmassClient::ApiError => e
1112
+ puts 'Exception when calling StaticCodeScansApi->add_static_code_scans_by_system_id'.red
1113
+ puts to_output_hash(e)
1114
+ end
1115
+ end
1116
+
1117
+ # CLEAR ------------------------------------------------------------------------------------
1118
+ desc 'clear', 'Clear an application findings'
1119
+ long_desc Help.text(:staticcode_clear_mapper)
1120
+
1121
+ # Required parameters/fields
1122
+ option :systemId, type: :numeric, required: true, desc: 'A numeric value representing the system identification'
1123
+ option :applicationName, type: :string, required: true, desc: 'Name of the software application that was assessed'
1124
+ option :version, type: :string, required: true, desc: 'The version of the application'
1125
+ option :clearFindings, type: :boolean, required: false, default: false, desc: 'BOOLEAN - true or false'
1126
+ # NOTE: clearFindings is a required parameter to clear an application's findings, however Thor does not allow
1127
+ # a boolean type to be required because it automatically creates a --no-clearFindings option for clearFindings=false
1128
+
1129
+ def clear
1130
+ unless options[:clearFindings]
1131
+ puts 'To clear an application findings, the field clearFindings (--clearFindings) is required'.red
1132
+ puts SCAN_POST_HELP_MESSAGE.yellow
1133
+ exit
1134
+ end
1135
+
1136
+ application = EmassClient::StaticCodeRequestPostBodyApplication.new
1137
+ application.application_name = options[:applicationName]
1138
+ application.version = options[:version]
1139
+
1140
+ application_findings = EmassClient::StaticCodeApplicationPost.new
1141
+ application_findings.clear_findings = options[:clearFindings]
1142
+
1143
+ app_findings_array = Array.new(1, application_findings)
1144
+
1145
+ body = EmassClient::StaticCodeRequestPostBody.new
1146
+ body.application = application
1147
+ body.application_findings = app_findings_array
1148
+
1149
+ body_array = Array.new(1, body)
1150
+
1151
+ begin
1152
+ result = EmassClient::StaticCodeScansApi
1153
+ .new.add_static_code_scans_by_system_id(options[:systemId], body_array)
1154
+ puts to_output_hash(result).green
1155
+ rescue EmassClient::ApiError => e
1156
+ puts 'Exception when calling StaticCodeScansApi->add_static_code_scans_by_system_id'.red
793
1157
  puts to_output_hash(e)
794
1158
  end
795
1159
  end
796
- # rubocop:enable Metrics/CyclomaticComplexity
797
1160
  end
798
1161
 
799
1162
  class Post < SubCommandBase
@@ -818,13 +1181,22 @@ module Emasser
818
1181
  desc 'pac', 'Add Package Approval Chain (PAC) security content'
819
1182
  subcommand 'pac', PAC
820
1183
 
821
- desc 'scan_findings', 'Upload static code scans'
822
- subcommand 'scan_findings', ScanFindings
1184
+ desc 'hardware', 'Add one or many hardware assets to a system'
1185
+ subcommand 'hardware', Hardware
1186
+
1187
+ desc 'software', 'Add one or many software assets to a system'
1188
+ subcommand 'software', Software
1189
+
1190
+ desc 'device_scans', 'Upload device scan results for a system'
1191
+ subcommand 'device_scans', DeviceScans
823
1192
 
824
1193
  desc 'cloud_resource', 'Upload cloud resource and their scan results'
825
1194
  subcommand 'cloud_resource', CloudResource
826
1195
 
827
1196
  desc 'container', 'Upload container and their scan results'
828
1197
  subcommand 'container', Container
1198
+
1199
+ desc 'scan_findings', 'Upload static code scans'
1200
+ subcommand 'scan_findings', ScanFindings
829
1201
  end
830
1202
  end