Top Level Namespace

Defined Under Namespace

Classes: Hiera

Instance Method Summary collapse

Instance Method Details

#add_file_to_client(config, compliance_map) ⇒ Object



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
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# File 'lib/puppetx/simp/compliance_map.rb', line 535

def add_file_to_client(config, compliance_map)
  if config[:client_report]
    client_vardir = @context.lookupvar('puppet_vardir')

    unless client_vardir
      raise(Puppet::ParseError, "compliance_map(): Cannot find fact `puppet_vardir`. Ensure `puppetlabs/stdlib` is installed")
    else
      compliance_report_target = %(#{client_vardir}/compliance_report.#{config[:format]})
    end

    # Retrieve the catalog resource if it already exists, create one if it
    # does not
    compliance_resource = @context.catalog.resources.find{ |res|
      res.type == 'File' && res.name == compliance_report_target
    }

    if compliance_resource
      # This is a massive hack that should be removed in the future.  Some
      # versions of Puppet, including the latest 3.X, do not check to see if
      # a resource has the 'remove' capability defined before calling it.  We
      # patch in the method here to work around this issue.
      unless compliance_resource.respond_to?(:remove)
        # Using this instead of define_singleton_method for Ruby 1.8 compatibility.
        class << compliance_resource
          self
        end.send(:define_method, :remove) do nil end
      end

      @context.catalog.remove_resource(compliance_resource)
    else
      compliance_resource = Puppet::Parser::Resource.new(
        'file',
        compliance_report_target,
        :scope => @context,
        :source => @context.source
      )
      compliance_resource.set_parameter('owner',Process.uid)
      compliance_resource.set_parameter('group',Process.gid)
      compliance_resource.set_parameter('mode','0600')
    end

    if config[:format] == 'json'
      compliance_resource.set_parameter('content',%(#{(compliance_map.to_json)}\n))
    elsif config[:format] == 'yaml'
      compliance_resource.set_parameter('content',%(#{compliance_map.to_yaml}\n))
    end

    # Inject new information into the catalog
    @context.catalog.add_resource(compliance_resource)
  end
end

#cached_lookup(key, default, &block) ⇒ Object

These cache functions are assumed to be created by the wrapper object, either the v3 backend or v5 backend.



143
144
145
146
147
148
149
150
151
# File 'lib/puppetx/simp/compliance_mapper.rb', line 143

def cached_lookup(key, default, &block)
  if (cache_has_key(key))
    retval = cached_value(key)
  else
    retval = yield key, default
    cache(key, retval)
  end
  retval
end

#compliance_map(args, context) ⇒ Object



608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
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
675
# File 'lib/puppetx/simp/compliance_map.rb', line 608

def compliance_map(args, context)
  ### BEGIN MAIN PROCESSING ###
  @context = context
  main_config = process_options(args)

  # Pick up our compiler hitchhiker
  # This is only needed when passing arguments. Users should no longer call
  # compliance_map() without arguments directly inside their classes or
  # definitions.
  hitchhiker = @context.compiler.instance_variable_get(:@compliance_map_function_data)

  if hitchhiker
    compliance_map = hitchhiker

    # Need to update the config for further processing options
    compliance_map.config = main_config
  else
    compliance_profiles = get_compliance_profiles

    # If we didn't find any profiles to map, bail
    return unless compliance_profiles

    # Create the validation report object
    # Have to break things out because jruby can't handle '::' in const_get
    compliance_map = profile_info.new(compliance_profiles, main_config)
  end

  # If we've gotten this far, we're ready to process *everything* and update
  # the file object.
  if main_config[:custom_call]
    # Here, we will only be adding custom items inside of classes or defined
    # types.

    resource_name = %(#{@context.resource.type}::#{@context.resource.title})

    # Add in custom materials if they exist

    _entry_opts = {}
    if main_config[:custom][:notes]
      _entry_opts['notes'] = main_config[:custom][:notes]
    end

    file_info = custom_call_file_info

    compliance_map.add(
      resource_name,
      main_config[:custom][:profile],
      main_config[:custom][:identifier],
      %(#{file_info[:file]}:#{file_info[:line]}),
      _entry_opts
    )
  else
    reference_map = get_reference_map
    validate_reference_map(reference_map)

    compliance_map.process_catalog(@context.resource.scope.catalog, reference_map)

    # Drop an entry on the server so that it can be processed when applicable.
    write_server_report(main_config, compliance_map)

    # Embed a File resource that will place the report on the client.
    add_file_to_client(main_config, compliance_map)
  end

  # This gets a little hairy, we need to persist the compliance map across
  # the entire compilation so we hitch a ride on the compiler.
  @context.compiler.instance_variable_set(:@compliance_map_function_data, compliance_map)
end

#custom_call_file_infoObject



501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
# File 'lib/puppetx/simp/compliance_map.rb', line 501

def custom_call_file_info
  file_info = {
    :file => @context.source.file,
    # We may not know the line number if this is at Top Scope
    :line => @context.source.line || '<unknown>',
  }

  # If we don't know the filename, guess....
  # This is probably because we're running in Puppet 4
  if @context.is_topscope?
    # Cast this to a string because it could potentially be a symbol from
    # the bowels of Puppet, or 'nil', or whatever and is purely
    # informative.
    env_manifest = "#{@context.environment.manifest}"

    if env_manifest =~ /\.pp$/
      file = env_manifest
    else
      file = File.join(env_manifest,'site.pp')
    end
  else
    filename = @context.source.name.split('::')
    filename[-1] = filename[-1] + '.pp'

    file = File.join(
      '<estimate>',
      "#{@context.environment.modulepath.first}",
      filename
    )
  end

  return file_info
end

#enforcement(key, &block) ⇒ Object

This is the shared codebase for the compliance_markup hiera backend. Each calling object (either the hiera backend class or the puppet lookup function) uses instance_eval to add these functions to the object.

Then the object can call enforcement like so: enforcement('key::name') do |key, default| lookup(key, { “default_value” => default}) end

The block is used to abstract lookup() since Hiera v5 and Hiera v3 have different calling conventions

This block will also return a KeyError if there is no key found, which must be trapped and converted into the correct response for the api. either throw :no_such_key or context.not_found()

We also expect a small api in the object that includes these functions:

debug(message) cached(key) cache(key, value) cache_has_key(key)

which allow for debug logging, and caching, respectively. Hiera v5 provides this function natively, while Hiera v3 has to create it itself



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/puppetx/simp/compliance_mapper.rb', line 28

def enforcement(key, &block)

  # Throw away keys we know we can't handle.
  # This also prevents recursion since these are the only keys internally we call.
  case key
  when "lookup_options"
    # XXX ToDo See note about compiling a lookup_options hash in the compiler
    throw :no_such_key
  when "compliance_map"
    throw :no_such_key
  when "compliance_markup::compliance_map"
    throw :no_such_key
  when "compliance_markup::compliance_map::percent_sign"
    throw :no_such_key
  when "compliance_markup::enforcement"
    throw :no_such_key
  when "compliance_markup::version"
    throw :no_such_key
  when "compliance_markup::percent_sign"
    throw :no_such_key
  else
    retval = :notfound
    if cache_has_key(:lock)
      lock = cached_value(:lock)
    else
      lock = false
    end
    if (lock == false)
      cache(:lock, true)
      begin
        profile_list = cached_lookup "compliance_markup::enforcement", [], &block
        unless (profile_list == [])
          debug("compliance_markup::enforcement set to #{profile_list}, attempting to enforce")
          version = cached_lookup "compliance_markup::version", "1.0.0", &block
          case version
          when /1.*/
            v1_compliance_map = {}

            if (cache_has_key(:v1_compliance_map))
              v1_compliance_map = cached_value(:v1_compliance_map)
            else
              debug("loading compliance_map data from compliance_markup::compliance_map")
              module_scope_compliance_map = cached_lookup "compliance_markup::compliance_map", {}, &block
              top_scope_compliance_map = cached_lookup "compliance_map", {}, &block
              v1_compliance_map.merge!(module_scope_compliance_map)
              v1_compliance_map.merge!(top_scope_compliance_map)
              cache(:v1_compliance_map, v1_compliance_map)
              # XXX ToDo: Add a dynamic loader for compliance data, so that modules can embed
              # their own compliance map information. Dylan has a way to do this in testing
              # in Abacus
            end


            profile = profile_list.hash.to_s
            v1_compile(profile, profile_list, v1_compliance_map)
            if (v1_compliance_map.key?(profile))
              # Handle a knockout prefix
              unless (v1_compliance_map[profile].key?("--" + key))
                if (v1_compliance_map[profile].key?(key))
                  retval = v1_compliance_map[profile][key]
                end
              end
            end
          end
        end
      ensure
        cache(:lock, false)
      end
    end
    if (retval == :notfound)
      throw :no_such_key
    end
  end
  return retval
end

#get_compliance_profilesObject



446
447
448
449
450
451
452
453
454
455
# File 'lib/puppetx/simp/compliance_map.rb', line 446

def get_compliance_profiles
  # Global lookup for the legacy stack
  compliance_profiles = lookup_global_silent('compliance_profile')
  # ENC compatible lookup
  compliance_profiles ||= lookup_global_silent('compliance_markup::validate_profiles')
  # Module-level lookup
  compliance_profiles ||= @context.catalog.resource('Class[compliance_markup]')[:validate_profiles]

  return compliance_profiles
end

#get_reference_mapObject



457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/puppetx/simp/compliance_map.rb', line 457

def get_reference_map
  reference_map = lookup_global_silent('compliance_map')
  reference_map ||= Hash.new

  if ( !reference_map || reference_map.empty? )
    # If not using an ENC, need to dig deeper

    # First, check the backwards-compatible lookup entry
    if @context.respond_to?(:call_function)
      reference_map = @context.call_function('lookup',['compliance_map', {'merge' => 'deep', 'default_value' => nil}])
    end

    # If lookup didn't find it, fish it out of the resource directly
    if ( !reference_map || reference_map.empty? )
      compliance_resource = @context.catalog.resource('Class[compliance_markup]')

      unless compliance_resource
        compliance_resource = @context.catalog.resource('Class[compliance_markup]')
      end

      if compliance_resource
        catalog_resource_map = compliance_resource['compliance_map']

        if catalog_resource_map && !catalog_resource_map.empty?
          reference_map = catalog_resource_map
        end
      end
    end
  end

  return reference_map
end

#lookup_global_silent(param) ⇒ Object

There is no way to silence the global warnings on looking up a qualified variable, so we're going to hack around it here.



336
337
338
# File 'lib/puppetx/simp/compliance_map.rb', line 336

def lookup_global_silent(param)
  @context.find_global_scope.to_hash[param]
end

#process_options(args) ⇒ Object



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
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
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
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
# File 'lib/puppetx/simp/compliance_map.rb', line 340

def process_options(args)
  config = {
    :custom_call              => false,
    :report_types             => [
      'non_compliant',
      'unknown_parameters',
      'custom_entries'
    ],
    :format                   => 'json',
    :client_report            => false,
    :server_report            => true,
    :server_report_dir        => File.join(Puppet[:vardir], 'simp', 'compliance_reports'),
    :default_map              => {},
    :catalog_to_compliance_map => false
  }

  # What profile are we using?
  if args && !args.empty?
    unless (args.first.is_a?(String) || args.first.is_a?(Hash))
      raise Puppet::ParseError, "compliance_map(): First parameter must be a String or Hash"
    end

    # This is used during the main call
    if args.first.is_a?(Hash)
      # Convert whatever was passed in to a symbol so that the Hash merge
      # works properly.
      user_config = Hash[args.first.map{|k,v| [k.to_sym, v] }]
      if user_config[:report_types]
        user_config[:report_types] = Array(user_config[:report_types])
      end

      # Takes care of things that have been set to 'undef' in Puppet
      user_config.delete_if{|k,v|
        v.nil? || v.is_a?(Symbol)
      }

      config.merge!(user_config)

      # This is used for custom content
    else
      config[:custom_call] = true
      config[:custom] = {
        :profile    => args.shift,
        :identifier => args.shift,
        :notes      => args.shift
      }

      if config[:custom][:profile] && !config[:custom][:identifier]
        raise Puppet::ParseError, "compliance_map(): You must pass at least two parameters"
      end

      unless config[:custom][:identifier].is_a?(String)
        raise Puppet::ParseError, "compliance_map(): Second parameter must be a compliance identifier String"
      end

      unless config[:custom][:notes].is_a?(String)
        raise Puppet::ParseError, "compliance_map(): Third parameter must be a compliance notes String"
      end
    end
  end

  valid_formats = [
    'json',
    'yaml'
  ]

  unless valid_formats.include?(config[:format])
    raise Puppet::ParseError, "compliance_map(): 'valid_formats' must be one of: '#{valid_formats.join(', ')}'"
  end

  valid_report_types = [
    'full',
    'non_compliant',
    'compliant',
    'unknown_resources',
    'unknown_parameters',
    'custom_entries'
  ]

  unless (config[:report_types] - valid_report_types).empty?
    raise Puppet::ParseError, "compliance_map(): 'report_type' must include '#{valid_report_types.join(', ')}'"
  end

  config[:extra_data] = {
    # Add the rest of the useful information to the map
    'fqdn'              => @context.lookupvar('fqdn'),
    'hostname'          => @context.lookupvar('hostname'),
    'ipaddress'         => @context.lookupvar('ipaddress'),
    'puppetserver_info' => 'local_compile'
  }

  puppetserver_facts = lookup_global_silent('server_facts')

  if puppetserver_facts && !puppetserver_facts.empty?
    config[:extra_data]['puppetserver_info'] = puppetserver_facts
  end

  if config[:site_data]
    unless config[:site_data].is_a?(Hash)
      raise Puppet::ParseError, %(compliance_map(): 'site_data' must be a Hash)
    end
  end

  return config
end

#profile_infoObject

END COMPLIANCE PROFILE



330
331
332
# File 'lib/puppetx/simp/compliance_map.rb', line 330

def profile_info
  @profile_info
end

#v1_compile(profile, profile_list, v1_compliance_map) ⇒ Object

Pre-compile the values for each profile list array. We use hash.to_s, then create a hash named that in v1_compliance_map, that is a raw key => value mapping. This simplifies our code as we can assume that if the key exists, then the value is what we use. We also don't have to worry about exponential time issues since this is linearlly done once, not every time for every key.



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/puppetx/simp/compliance_mapper.rb', line 111

def v1_compile(profile, profile_list, v1_compliance_map)
  unless (v1_compliance_map.key?(profile))
    compile_start_time = Time.now
    debug("compliance map for #{profile_list} not found, starting compiler")
    table = {}
    # Set the keys in reverse order. This means that [ 'disa', 'nist'] would prioritize
    # disa values over nist. Only bother to store the highest priority value
    profile_list.reverse.each do |profile_map|
      if (profile_map != /^v[0-9]+/)
        if (v1_compliance_map.key?(profile_map))
          v1_compliance_map[profile_map].each do |key, entry|
            if (entry.key?("value"))
              # XXX ToDo: Generate a lookup_options hash, set to 'first', if the user specifies some
              # option that toggles it on. This would allow un-overridable enforcement at the hiera
              # layer (though it can still be overridden by resource-style class definitions
              table[key] = entry["value"]
            end
          end
        end
      end
    end
    v1_compliance_map[profile] = table
    compile_end_time = Time.now
    debug("compiled compliance_map containing #{table.size} keys in #{compile_end_time - compile_start_time} seconds")
    # This is necessary for hiera v5 since the cache
    # is immutable.
    cache(:v1_compliance_map, v1_compliance_map)
  end
end

#validate_reference_map(reference_map) ⇒ Object



490
491
492
493
494
495
496
497
498
499
# File 'lib/puppetx/simp/compliance_map.rb', line 490

def validate_reference_map(reference_map)
  # If we still don't have a reference map, we need to let the user know!
  if !reference_map || (reference_map.respond_to?(:empty) && reference_map.empty?)
    if main_config[:default_map] && !main_config[:default_map].empty?
      reference_map = main_config[:default_map]
    else
      raise(Puppet::ParseError, %(compliance_map(): Could not find the 'compliance_map' Hash at the global level or via Lookup))
    end
  end
end

#write_server_report(config, compliance_map) ⇒ Object



587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
# File 'lib/puppetx/simp/compliance_map.rb', line 587

def write_server_report(config, compliance_map)
  report_dir = File.join(config[:server_report_dir], @context.lookupvar('fqdn'))
  FileUtils.mkdir_p(report_dir)

  if config[:server_report]
    File.open(File.join(report_dir,"compliance_report.#{config[:format]}"),'w') do |fh|
      if config[:format] == 'json'
        fh.puts(compliance_map.to_json)
      elsif config[:format] == 'yaml'
        fh.puts(compliance_map.to_yaml)
      end
    end
  end

  if config[:catalog_to_compliance_map]
    File.open(File.join(report_dir,'catalog_compliance_map.yaml'),'w') do |fh|
      fh.puts(compliance_map.catalog_to_map(@context.resource.scope.catalog))
    end
  end
end