Class: Scelint::Lint
- Inherits:
-
Object
- Object
- Scelint::Lint
- Defined in:
- lib/scelint.rb
Overview
Check SCE data in the specified directories
Instance Attribute Summary collapse
-
#data ⇒ Object
Returns the value of attribute data.
-
#errors ⇒ Object
Returns the value of attribute errors.
-
#log ⇒ Object
Returns the value of attribute log.
-
#notes ⇒ Object
Returns the value of attribute notes.
-
#warnings ⇒ Object
Returns the value of attribute warnings.
Instance Method Summary collapse
-
#check_ce(file, file_data) ⇒ Object
Check a CE.
-
#check_check_ces(file, file_data) ⇒ Object
Check CEs in a check.
-
#check_checks(file, file_data) ⇒ Object
Check checks.
-
#check_confine(file, file_data) ⇒ Object
Check the confine data structure for any unexpected keys and legacy facts.
-
#check_controls(file, file_data) ⇒ Object
Check the controls in the given data.
-
#check_description(file, file_data) ⇒ Object
Check the description of the given data.
-
#check_identifiers(file, file_data) ⇒ Object
Check identifiers.
-
#check_imported_data(file, file_data) ⇒ Object
Check imported_data.
-
#check_keys(file, file_data) ⇒ Object
Check that all the top-level keys in the data are recognized.
-
#check_oval_ids(file, file_data) ⇒ Object
Check oval-ids.
-
#check_parameter(file, check, parameter) ⇒ Object
Check parameter.
-
#check_profile_ces(file, file_data) ⇒ Object
Check the CEs in a profile.
-
#check_profile_checks(file, file_data) ⇒ Object
Check the checks in a profile.
-
#check_profiles(file, file_data) ⇒ Object
Check profiles.
-
#check_remediation(file, check, remediation_section) ⇒ Object
Check remediation.
-
#check_settings(file, check, file_data) ⇒ Object
Check settings.
-
#check_title(file, file_data) ⇒ Object
Check the title of the given data.
-
#check_type(file, check, file_data) ⇒ Object
Check type.
-
#check_value(_file, _check, _value) ⇒ Boolean
Check a value.
-
#check_version(file, file_data) ⇒ Object
Check that the value of the version key in the data is correct.
-
#confines ⇒ Array
Retrieve confines from the loaded data.
-
#files ⇒ Object
Return an array of all the files found in the loaded data.
-
#initialize(paths = ['.'], logger: Logger.new(STDOUT, level: Logger::INFO)) ⇒ Lint
constructor
Create a new Lint object.
-
#lint(file, file_data) ⇒ Object
Lint the given file.
-
#normalize_confinement(confine) ⇒ Array<Hash>
Normalize the given confinement hash by: * Expanding all possible combinations of Array values * Converting dotted fact names into a nested facts hash.
-
#validate ⇒ Object
Validate the Hiera data for each available profiles.
Constructor Details
#initialize(paths = ['.'], logger: Logger.new(STDOUT, level: Logger::INFO)) ⇒ Lint
Create a new Lint object
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/scelint.rb', line 137 def initialize(paths = ['.'], logger: Logger.new(STDOUT, level: Logger::INFO)) @log = logger @errors = [] @warnings = [] @notes = [] @data = ComplianceEngine::Data.new(*Array(paths)) @data.files.each do |file| lint(file, @data.get(file)) end merged_data_lint validate end |
Instance Attribute Details
#data ⇒ Object
Returns the value of attribute data.
131 132 133 |
# File 'lib/scelint.rb', line 131 def data @data end |
#errors ⇒ Object
Returns the value of attribute errors.
131 132 133 |
# File 'lib/scelint.rb', line 131 def errors @errors end |
#log ⇒ Object
Returns the value of attribute log.
131 132 133 |
# File 'lib/scelint.rb', line 131 def log @log end |
#notes ⇒ Object
Returns the value of attribute notes.
131 132 133 |
# File 'lib/scelint.rb', line 131 def notes @notes end |
#warnings ⇒ Object
Returns the value of attribute warnings.
131 132 133 |
# File 'lib/scelint.rb', line 131 def warnings @warnings end |
Instance Method Details
#check_ce(file, file_data) ⇒ Object
Check a CE
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 |
# File 'lib/scelint.rb', line 357 def check_ce(file, file_data) ok = [ 'title', 'description', 'controls', 'identifiers', 'oval-ids', 'confine', 'imported_data', 'notes', ] file_data.each do |ce, value| value.each_key do |key| warnings << "#{file} (CE '#{ce}'): unexpected key '#{key}'" unless ok.include?(key) end check_title(file, value['title']) unless value['title'].nil? check_description(file, value['description']) unless value['description'].nil? check_controls(file, value['controls']) unless value['controls'].nil? check_identifiers(file, value['identifiers']) unless value['identifiers'].nil? check_oval_ids(file, value['oval-ids']) unless value['oval-ids'].nil? check_confine(file, value['confine']) unless value['confine'].nil? check_imported_data(file, value['imported_data']) unless value['imported_data'].nil? end end |
#check_check_ces(file, file_data) ⇒ Object
Check CEs in a check
517 518 519 520 521 522 523 |
# File 'lib/scelint.rb', line 517 def check_check_ces(file, file_data) warnings << "#{file}: bad ces '#{file_data}'" unless file_data.is_a?(Array) file_data.each do |key| warnings << "#{file}: bad ce '#{key}'" unless key.is_a?(String) end end |
#check_checks(file, file_data) ⇒ Object
Check checks
529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 |
# File 'lib/scelint.rb', line 529 def check_checks(file, file_data) ok = [ 'type', 'settings', 'controls', 'identifiers', 'oval-ids', 'ces', 'confine', 'remediation', ] file_data.each do |check, value| if value.nil? warnings << "#{file} (check '#{check}'): empty value" next end if value.is_a?(Hash) value.each_key do |key| warnings << "#{file} (check '#{check}'): unexpected key '#{key}'" unless ok.include?(key) end else errors << "#{file} (check '#{check}'): contains something other than a hash, this is most likely caused by a missing note or ce element under the check" end check_type(file, check, value['type']) if value['type'] || file == 'merged data' check_settings(file, check, value['settings']) if value['settings'] || file == 'merged data' unless value['remediation'].nil? check_remediation(file, check, value['remediation']) if value['remediation'] end check_controls(file, value['controls']) unless value['controls'].nil? check_identifiers(file, value['identifiers']) unless value['identifiers'].nil? check_oval_ids(file, value['oval-ids']) unless value['oval-ids'].nil? check_check_ces(file, value['ces']) unless value['ces'].nil? check_confine(file, value['confine']) unless value['confine'].nil? end end |
#check_confine(file, file_data) ⇒ Object
Check the confine data structure for any unexpected keys and legacy facts
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/scelint.rb', line 249 def check_confine(file, file_data) not_ok = [ 'type', 'settings', 'parameter', 'value', 'remediation', 'risk', 'level', 'reason', ] unless file_data.is_a?(Hash) warnings << "#{file}: bad confine '#{file_data}'" return end file_data.each_key do |key| warnings << "#{file}: unexpected key '#{key}' in confine '#{file_data}'" if not_ok.include?(key) if Scelint::LEGACY_FACTS.any? { |legacy_fact| legacy_fact.is_a?(Regexp) ? legacy_fact.match?(key) : (legacy_fact == key) } warning = "#{file}: legacy fact '#{key}' in confine '#{file_data}'" warnings << warning unless warnings.include?(warning) end end end |
#check_controls(file, file_data) ⇒ Object
Check the controls in the given data
207 208 209 210 211 212 213 214 215 |
# File 'lib/scelint.rb', line 207 def check_controls(file, file_data) if file_data.is_a?(Hash) file_data.each do |key, value| warnings << "#{file}: bad control '#{key}'" unless key.is_a?(String) && value # Should be truthy end else warnings << "#{file}: bad controls '#{file_data}'" end end |
#check_description(file, file_data) ⇒ Object
Check the description of the given data
199 200 201 |
# File 'lib/scelint.rb', line 199 def check_description(file, file_data) warnings << "#{file}: bad description '#{file_data}'" unless file_data.is_a?(String) end |
#check_identifiers(file, file_data) ⇒ Object
Check identifiers
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
# File 'lib/scelint.rb', line 279 def check_identifiers(file, file_data) if file_data.is_a?(Hash) file_data.each do |key, value| if key.is_a?(String) && value.is_a?(Array) value.each do |identifier| warnings << "#{file}: bad identifier '#{identifier}'" unless identifier.is_a?(String) end else warnings << "#{file}: bad identifier '#{key}'" end end else warnings << "#{file}: bad identifiers '#{file_data}'" end end |
#check_imported_data(file, file_data) ⇒ Object
Check imported_data
313 314 315 316 317 318 319 320 321 |
# File 'lib/scelint.rb', line 313 def check_imported_data(file, file_data) ok = ['checktext', 'fixtext'] file_data.each do |key, value| warnings << "#{file}: unexpected key '#{key}'" unless ok.include?(key) warnings << "#{file} (key '#{key}'): bad data '#{value}'" unless value.is_a?(String) end end |
#check_keys(file, file_data) ⇒ Object
Check that all the top-level keys in the data are recognized
173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/scelint.rb', line 173 def check_keys(file, file_data) ok = [ 'version', 'profiles', 'ce', 'checks', 'controls', ] file_data.each_key do |key| warnings << "#{file}: unexpected key '#{key}'" unless ok.include?(key) end end |
#check_oval_ids(file, file_data) ⇒ Object
Check oval-ids
299 300 301 302 303 304 305 306 307 |
# File 'lib/scelint.rb', line 299 def check_oval_ids(file, file_data) if file_data.is_a?(Array) file_data.each do |key| warnings << "#{file}: bad oval-id '#{key}'" unless key.is_a?(String) end else warnings << "#{file}: bad oval-ids '#{file_data}'" end end |
#check_parameter(file, check, parameter) ⇒ Object
Check parameter
398 399 400 |
# File 'lib/scelint.rb', line 398 def check_parameter(file, check, parameter) errors << "#{file} (check '#{check}'): invalid parameter '#{parameter}'" unless parameter.is_a?(String) && !parameter.empty? end |
#check_profile_ces(file, file_data) ⇒ Object
Check the CEs in a profile
221 222 223 224 225 226 227 228 229 |
# File 'lib/scelint.rb', line 221 def check_profile_ces(file, file_data) if file_data.is_a?(Hash) file_data.each do |key, value| warnings << "#{file}: bad ce '#{key}'" unless key.is_a?(String) && value.is_a?(TrueClass) end else warnings << "#{file}: bad ces '#{file_data}'" end end |
#check_profile_checks(file, file_data) ⇒ Object
Check the checks in a profile
235 236 237 238 239 240 241 242 243 |
# File 'lib/scelint.rb', line 235 def check_profile_checks(file, file_data) if file_data.is_a?(Hash) file_data.each do |key, value| warnings << "#{file}: bad check '#{key}'" unless key.is_a?(String) && value.is_a?(TrueClass) end else warnings << "#{file}: bad checks '#{file_data}'" end end |
#check_profiles(file, file_data) ⇒ Object
Check profiles
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 |
# File 'lib/scelint.rb', line 327 def check_profiles(file, file_data) ok = [ 'title', 'description', 'controls', 'ces', 'checks', 'confine', 'id', 'benchmark_version', ] file_data.each do |profile, value| value.each_key do |key| warnings << "#{file} (profile '#{profile}'): unexpected key '#{key}'" unless ok.include?(key) end check_title(file, value['title']) unless value['title'].nil? check_description(file, value['description']) unless value['description'].nil? check_controls(file, value['controls']) unless value['controls'].nil? check_profile_ces(file, value['ces']) unless value['ces'].nil? check_profile_checks(file, value['checks']) unless value['checks'].nil? check_confine(file, value['confine']) unless value['confine'].nil? end end |
#check_remediation(file, check, remediation_section) ⇒ Object
Check remediation
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 |
# File 'lib/scelint.rb', line 407 def check_remediation(file, check, remediation_section) reason_ok = [ 'reason', ] risk_ok = [ 'level', 'reason', ] if remediation_section.is_a?(Hash) remediation_section.each do |section, value| case section when 'scan-false-positive', 'disabled' value.each do |reason| # If the element in the remediation section isn't a hash, it is incorrect. if reason.is_a?(Hash) # Check for unknown elements and warn the user rather than failing (reason.keys - reason_ok).each do |unknown_element| warnings << "#{file} (check '#{check}'): Unknown element #{unknown_element} in remediation section #{section}" end errors << "#{file} (check '#{check}'): malformed remediation section #{section}, must be an array of reason hashes." unless reason['reason'].is_a?(String) else errors << "#{file} (check '#{check}'): malformed remediation section #{section}, must be an array of reason hashes." end end when 'risk' value.each do |risk| # If the element in the remediation section isn't a hash, it is incorrect. if risk.is_a?(Hash) # Check for unknown elements and warn the user rather than failing (risk.keys - risk_ok).each do |unknown_element| warnings << "#{file} (check '#{check}'): Unknown element #{unknown_element} in remediation section #{section}" end # Since reasons are optional here, we won't be checking for those errors << "#{file} (check '#{check}'): malformed remediation section #{section}, must be an array of hashes containing levels and reasons." unless risk['level'].is_a?(Integer) else errors << "#{file} (check '#{check}'): malformed remediation section #{section}, must be an array of hashes containing levels and reasons." end end else warnings << "#{file} (check '#{check}'): #{section} is not a recognized section within the remediation section" end end else errors << "#{file} (check '#{check}'): malformed remediation section, expecting a hash." end end |
#check_settings(file, check, file_data) ⇒ Object
Check settings
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 |
# File 'lib/scelint.rb', line 473 def check_settings(file, check, file_data) ok = ['parameter', 'value'] if file_data.nil? msg = "#{file} (check '#{check}'): missing settings" if file == 'merged data' errors << msg else warnings << msg end return false end if file_data.key?('parameter') check_parameter(file, check, file_data['parameter']) else msg = "#{file} (check '#{check}'): missing key 'parameter'" if file == 'merged data' errors << msg else warnings << msg end end if file_data.key?('value') check_value(file, check, file_data['value']) else msg = "#{file} (check '#{check}'): missing key 'value'" if file == 'merged data' errors << msg else warnings << msg end end file_data.each_key do |key| warnings << "#{file} (check '#{check}'): unexpected key '#{key}'" unless ok.include?(key) end end |
#check_title(file, file_data) ⇒ Object
Check the title of the given data
191 192 193 |
# File 'lib/scelint.rb', line 191 def check_title(file, file_data) warnings << "#{file}: bad title '#{file_data}'" unless file_data.is_a?(String) end |
#check_type(file, check, file_data) ⇒ Object
Check type
389 390 391 |
# File 'lib/scelint.rb', line 389 def check_type(file, check, file_data) errors << "#{file} (check '#{check}'): unknown type '#{file_data}'" unless file_data == 'puppet-class-parameter' end |
#check_value(_file, _check, _value) ⇒ Boolean
Check a value
463 464 465 466 |
# File 'lib/scelint.rb', line 463 def check_value(_file, _check, _value) # rubocop:disable Naming/PredicateMethod # value could be anything true end |
#check_version(file, file_data) ⇒ Object
The version is currently hardcoded to ‘2.0.0’
Check that the value of the version key in the data is correct
165 166 167 |
# File 'lib/scelint.rb', line 165 def check_version(file, file_data) errors << "#{file}: version check failed" unless file_data == '2.0.0' end |
#confines ⇒ Array
Retrieve confines from the loaded data
612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 |
# File 'lib/scelint.rb', line 612 def confines return @confines unless @confines.nil? @confines = [] [:profiles, :ces, :checks, :controls].each do |type| data.public_send(type).each_value do |value| # FIXME: This is calling a private method value.send(:fragments).each_value do |v| next unless v.is_a?(Hash) next unless v.key?('confine') normalize_confinement(v['confine']).each do |confine| @confines << confine unless @confines.include?(confine) end end end end @confines end |
#files ⇒ Object
Return an array of all the files found in the loaded data
155 156 157 |
# File 'lib/scelint.rb', line 155 def files data.files end |
#lint(file, file_data) ⇒ Object
Lint the given file
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 |
# File 'lib/scelint.rb', line 680 def lint(file, file_data) unless file_data.is_a?(Hash) errors << "#{file}: Expected a Hash, got a #{file_data.class}" return end check_version(file, file_data['version']) check_keys(file, file_data) check_profiles(file, file_data['profiles']) if file_data['profiles'] check_ce(file, file_data['ce']) if file_data['ce'] check_checks(file, file_data['checks']) if file_data['checks'] check_controls(file, file_data['controls']) if file_data['controls'] rescue => e errors << "#{file}: #{e.} (not a hash?)" end |
#normalize_confinement(confine) ⇒ Array<Hash>
Normalize the given confinement hash by:
-
Expanding all possible combinations of Array values
-
Converting dotted fact names into a nested facts hash
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 |
# File 'lib/scelint.rb', line 574 def normalize_confinement(confine) normalized = [] # Step 1, sort the hash keys sorted = confine.sort.to_h # Step 2, expand all possible combinations of Array values index = 0 max_count = 1 sorted.each_value { |value| max_count *= Array(value).size } sorted.each do |key, value| (index..(max_count - 1)).each do |i| normalized[i] ||= {} normalized[i][key] = Array(value)[i % Array(value).size] end end # Step 3, convert dotted fact names into a facts hash normalized.map do |c| c.each_with_object({}) do |(key, value), result| current = result parts = key.split('.') parts.each_with_index do |part, i| if i == parts.length - 1 current[part] = value else current[part] ||= {} current = current[part] end end end end end |
#validate ⇒ Object
Validate the Hiera data for each available profiles
This method performs validation in two stages:
-
Unconfined: Checks if Hiera data exists for each profile.
-
Confined: Checks if Hiera data exists for each profile with specific facts.
638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 |
# File 'lib/scelint.rb', line 638 def validate if data.profiles.keys.empty? notes << 'No profiles found, unable to validate Hiera data' return nil end # Unconfined, verify that hiera data exists data.profiles.each_key do |profile| hiera = data.hiera([profile]) if hiera.nil? errors << "Profile '#{profile}': Invalid Hiera data (returned nil)" next end if hiera.empty? warnings << "Profile '#{profile}': No Hiera data found" next end log.debug "Profile '#{profile}': Hiera data found (#{hiera.keys.count} keys)" end # Again, this time confined confines.each do |confine| data.facts = confine data.profiles.select { |_, value| value.ces&.count&.positive? || value.controls&.count&.positive? }.each_key do |profile| hiera = data.hiera([profile]) if hiera.nil? errors << "Profile '#{profile}': Invalid Hiera data (returned nil) with facts #{confine}" next end if hiera.empty? warnings << "Profile '#{profile}': No Hiera data found with facts #{confine}" next end log.debug "Profile '#{profile}': Hiera data found (#{hiera.keys.count} keys) with facts #{confine}" end end end |