├── CODEOWNERS ├── .rspec ├── lib ├── abide_dev_utils │ ├── version.rb │ ├── sce │ │ ├── hiera_data.rb │ │ ├── validate.rb │ │ ├── generate.rb │ │ ├── validate │ │ │ ├── strings │ │ │ │ ├── puppet_defined_type_validator.rb │ │ │ │ ├── validation_finding.rb │ │ │ │ ├── puppet_class_validator.rb │ │ │ │ └── base_validator.rb │ │ │ ├── resource_data.rb │ │ │ └── strings.rb │ │ ├── hiera_data │ │ │ ├── mapping_data │ │ │ │ ├── mixins.rb │ │ │ │ └── map_data.rb │ │ │ ├── resource_data │ │ │ │ ├── .parameters.rb │ │ │ │ ├── .resource.rb │ │ │ │ └── .control.rb │ │ │ └── mapping_data.rb │ │ └── benchmark_loader.rb │ ├── resources │ │ └── generic_spec.erb │ ├── ppt │ │ ├── code_gen │ │ │ ├── resource_types.rb │ │ │ ├── resource_types │ │ │ │ ├── strings.rb │ │ │ │ ├── manifest.rb │ │ │ │ ├── parameter.rb │ │ │ │ ├── class.rb │ │ │ │ └── base.rb │ │ │ ├── generate.rb │ │ │ ├── resource.rb │ │ │ └── data_types.rb │ │ ├── code_gen.rb │ │ ├── puppet_module.rb │ │ ├── code_introspection.rb │ │ ├── strings.rb │ │ ├── score_module.rb │ │ ├── new_obj.rb │ │ └── api.rb │ ├── mixins.rb │ ├── errors │ │ ├── comply.rb │ │ ├── jira.rb │ │ ├── gcloud.rb │ │ ├── xccdf.rb │ │ ├── base.rb │ │ ├── ppt.rb │ │ ├── sce.rb │ │ └── general.rb │ ├── errors.rb │ ├── constants.rb │ ├── gcloud.rb │ ├── prompt.rb │ ├── cli │ │ ├── abstract.rb │ │ ├── test.rb │ │ ├── comply.rb │ │ └── xccdf.rb │ ├── config.rb │ ├── jira │ │ ├── helper.rb │ │ ├── client.rb │ │ ├── finder.rb │ │ ├── client_builder.rb │ │ ├── issue_builder.rb │ │ └── dry_run.rb │ ├── cli.rb │ ├── xccdf │ │ ├── parser │ │ │ ├── helpers.rb │ │ │ └── objects │ │ │ │ └── numbered_object.rb │ │ ├── parser.rb │ │ ├── utils.rb │ │ └── diff.rb │ ├── validate.rb │ ├── sce.rb │ ├── output.rb │ ├── files.rb │ ├── dot_number_comparable.rb │ ├── puppet_strings.rb │ ├── markdown.rb │ └── ppt.rb └── abide_dev_utils.rb ├── bin ├── abide.rb ├── setup └── console ├── exe └── abide ├── spec ├── abide_dev_utils │ ├── ppt │ │ ├── facter_utils_spec.rb │ │ └── new_obj_spec.rb │ ├── xccdf │ │ ├── parser_spec.rb │ │ ├── diff │ │ │ └── benchmark_spec.rb │ │ └── parser │ │ │ └── objects_spec.rb │ ├── sce │ │ └── benchmark_spec.rb │ ├── xccdf_spec.rb │ └── cli_spec.rb ├── abide_dev_utils_spec.rb └── spec_helper.rb ├── .gitignore ├── Gemfile ├── CHANGELOG.md ├── LICENSE.txt ├── Rakefile ├── .github └── workflows │ ├── mend_ruby.yaml │ └── ci.yaml ├── .rubocop_todo.yml ├── new_diff.rb ├── abide_dev_utils.gemspec └── .rubocop.yml /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @puppetlabs/abide-team -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format progress 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbideDevUtils 4 | VERSION = "0.18.4" 5 | end 6 | -------------------------------------------------------------------------------- /bin/abide.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'abide_dev_utils/cli' 5 | 6 | Abide::CLI.execute 7 | -------------------------------------------------------------------------------- /exe/abide: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'abide_dev_utils/cli' 5 | 6 | Abide::CLI.execute 7 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/hiera_data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbideDevUtils 4 | module Sce 5 | module HieraData; end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /spec/abide_dev_utils/ppt/facter_utils_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # require 'spec_helper' 4 | # require_relative '../../lib/abide_dev_utils/ppt/facter_utils' 5 | -------------------------------------------------------------------------------- /spec/abide_dev_utils_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe AbideDevUtils do 4 | it "has a version number" do 5 | expect(AbideDevUtils::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/fixtures/ 8 | /spec/reports/ 9 | /tmp/ 10 | w10_20h2.xml 11 | w10_2004.xml 12 | # rspec failure tracking 13 | .rspec_status 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem 'puppet', '>= 7.0.0', source: 'https://rubygems-puppetcore.puppet.com' if ENV['PUPPET_AUTH_TOKEN'] 6 | 7 | # Specify your gem's dependencies in abide_dev_utils.gemspec 8 | gemspec 9 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/validate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'validate/resource_data' 4 | require_relative 'validate/strings' 5 | 6 | module AbideDevUtils 7 | module Sce 8 | # Namespace for SCE validation modules / classes 9 | module Validate; end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/resources/generic_spec.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe '<%= @obj_name %>' do 6 | on_supported_os.each do |os, os_facts| 7 | context "on #{os}" do 8 | let(:facts) { os_facts } 9 | 10 | it { is_expected.to compile } 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/code_gen/resource_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/ppt/code_gen/resource_types/class' 4 | require 'abide_dev_utils/ppt/code_gen/resource_types/manifest' 5 | require 'abide_dev_utils/ppt/code_gen/resource_types/parameter' 6 | require 'abide_dev_utils/ppt/code_gen/resource_types/strings' 7 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/generate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/sce/generate/reference' 4 | require 'abide_dev_utils/sce/generate/coverage_report' 5 | 6 | module AbideDevUtils 7 | module Sce 8 | # Namespace for objects and methods used in `abide sce generate` subcommands 9 | module Generate; end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/code_gen/resource_types/strings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/ppt/code_gen/resource_types/base' 4 | 5 | module AbideDevUtils 6 | module Ppt 7 | module CodeGen 8 | class Strings < Base 9 | VALID_CHILDREN = %w[See Summary Param Example].freeze 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/code_gen/generate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/ppt/code_gen/resource_types' 4 | 5 | module AbideDevUtils 6 | module Ppt 7 | module CodeGen 8 | module Generate 9 | def self.a_manifest 10 | AbideDevUtils::Ppt::CodeGen::Manifest.new 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/code_gen/resource_types/manifest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/ppt/code_gen/resource_types/base' 4 | 5 | module AbideDevUtils 6 | module Ppt 7 | module CodeGen 8 | class Manifest < Base 9 | def initialize 10 | super 11 | @supports_children = true 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/code_gen.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/ppt/code_gen/data_types' 4 | require 'abide_dev_utils/ppt/code_gen/resource' 5 | require 'abide_dev_utils/ppt/code_gen/resource_types' 6 | 7 | module AbideDevUtils 8 | module Ppt 9 | module CodeGen 10 | def self.generate_a_manifest 11 | Manifest.new 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.1.0](https://github.com/hsnodgrass/abide_dev_utils/tree/v0.1.0) (2021-01-22) 4 | 5 | [Full Changelog](https://github.com/hsnodgrass/abide_dev_utils/compare/f24cea86afb34d0b4904576400db8b8039f1eecc...v0.1.0) 6 | 7 | 8 | 9 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 10 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/code_gen/resource_types/parameter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/ppt/code_gen/resource_types/base' 4 | 5 | module AbideDevUtils 6 | module Ppt 7 | module CodeGen 8 | class Parameter < Base 9 | def initialize 10 | @supports_children = true 11 | @supports_value = true 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/mixins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbideDevUtils 4 | module Mixins 5 | # mixin methods for the Hash data type 6 | module Hash 7 | def deep_copy 8 | Marshal.load(Marshal.dump(self)) 9 | end 10 | 11 | def diff(other) 12 | dup.delete_if { |k, v| other[k] == v }.merge!(other.dup.delete_if { |k, _| key?(k) }) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/code_gen/resource_types/class.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/ppt/code_gen/resource_types/base' 4 | 5 | module AbideDevUtils 6 | module Ppt 7 | module CodeGen 8 | class Class < Base 9 | def initialize 10 | super 11 | @supports_children = true 12 | @supports_value = true 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "abide_dev_utils" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /lib/abide_dev_utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'abide_dev_utils/version' 4 | require_relative 'abide_dev_utils/xccdf' 5 | require_relative 'abide_dev_utils/ppt' 6 | require_relative 'abide_dev_utils/jira' 7 | require_relative 'abide_dev_utils/config' 8 | require_relative 'abide_dev_utils/comply' 9 | require_relative 'abide_dev_utils/sce' 10 | 11 | # Root namespace all modules / classes 12 | module AbideDevUtils; end 13 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/errors/comply.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/errors/base' 4 | 5 | module AbideDevUtils 6 | module Errors 7 | module Comply 8 | class ComplyLoginFailedError < GenericError 9 | @default = 'Failed to login to Comply:' 10 | end 11 | 12 | class WaitOnError < GenericError 13 | @default = 'wait_on failed due to error:' 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/abide_dev_utils/xccdf/parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # require 'abide_dev_utils/xccdf/parser' 4 | require 'spec_helper' 5 | 6 | RSpec.describe AbideDevUtils::XCCDF::Parser do 7 | describe '#parse' do 8 | it 'parses a valid XCCDF file' do 9 | file_path = test_xccdf_files.first 10 | benchmark = described_class.parse(file_path) 11 | benchmark.is_a?(AbideDevUtils::XCCDF::Parser::Objects::Benchmark) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/errors/base' 4 | require 'abide_dev_utils/errors/sce' 5 | require 'abide_dev_utils/errors/comply' 6 | require 'abide_dev_utils/errors/gcloud' 7 | require 'abide_dev_utils/errors/general' 8 | require 'abide_dev_utils/errors/jira' 9 | require 'abide_dev_utils/errors/xccdf' 10 | require 'abide_dev_utils/errors/ppt' 11 | 12 | module AbideDevUtils 13 | # Namespace for Error objects 14 | module Errors; end 15 | end 16 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/validate/strings/puppet_defined_type_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'puppet_class_validator' 4 | 5 | module AbideDevUtils 6 | module Sce 7 | module Validate 8 | module Strings 9 | # Validates Puppet Defined Type strings objects 10 | class PuppetDefinedTypeValidator < PuppetClassValidator 11 | def validate_puppet_defined_type 12 | validate_puppet_class 13 | end 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/abide_dev_utils/xccdf/diff/benchmark_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe AbideDevUtils::XCCDF::Diff::BenchmarkDiff do 6 | let(:file_path1) { test_xccdf_files.find { |f| f.end_with?('v1.0.0-xccdf.xml') } } 7 | let(:file_path2) { test_xccdf_files.find { |f| f.end_with?('v1.1.0-xccdf.xml') } } 8 | 9 | describe '#new' do 10 | it 'creates new BenchmarkDiff instance' do 11 | described_class.new(file_path1, file_path2).is_a?(described_class) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbideDevUtils 4 | module CliConstants 5 | require 'abide_dev_utils/config' 6 | require 'abide_dev_utils/errors' 7 | require 'abide_dev_utils/output' 8 | require 'abide_dev_utils/prompt' 9 | require 'abide_dev_utils/validate' 10 | 11 | CONFIG = AbideDevUtils::Config 12 | ERRORS = AbideDevUtils::Errors 13 | OUTPUT = AbideDevUtils::Output 14 | PROMPT = AbideDevUtils::Prompt 15 | VALIDATE = AbideDevUtils::Validate 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/errors/jira.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/errors/base' 4 | 5 | module AbideDevUtils 6 | module Errors 7 | module Jira 8 | class CreateIssueError < GenericError 9 | @default = 'Failed to create Jira issue:' 10 | end 11 | 12 | class CreateEpicError < GenericError 13 | @default = 'Failed to create Jira epic:' 14 | end 15 | 16 | class CreateSubtaskError < GenericError 17 | @default = 'Failed to create Jira subtask for issue:' 18 | end 19 | 20 | class FindIssueError < GenericError 21 | @default = 'Failed to find Jira issue:' 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/errors/gcloud.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/errors/base' 4 | 5 | module AbideDevUtils 6 | module Errors 7 | module GCloud 8 | class MissingCredentialsError < GenericError 9 | @default = <<~EOERR 10 | Storage credentials not given. Please set environment variable ABIDE_GCLOUD_CREDENTIALS. 11 | EOERR 12 | end 13 | 14 | class MissingProjectError < GenericError 15 | @default = <<~EOERR 16 | Storage project not given. Please set the environment variable ABIDE_GCLOUD_PROJECT. 17 | EOERR 18 | end 19 | 20 | class MissingBucketNameError < GenericError 21 | @default = <<~EOERR 22 | Storage bucket name not given. Please set the environment variable ABIDE_GCLOUD_BUCKET. 23 | EOERR 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/errors/xccdf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/errors/base' 4 | 5 | module AbideDevUtils 6 | module Errors 7 | # Raised when an xpath search of an xccdf file fails 8 | class XPathSearchError < GenericError 9 | @default = 'XPath seach failed to find anything at:' 10 | end 11 | 12 | class StrategyInvalidError < GenericError 13 | @default = 'Invalid strategy selected. Should be either \'name\' or \'num\'' 14 | end 15 | 16 | class ControlPartsError < GenericError 17 | @default = 'Failed to extract parts from control name:' 18 | end 19 | 20 | class ProfilePartsError < GenericError 21 | @default = 'Failed to extract parts from profile name:' 22 | end 23 | 24 | class UnsupportedXCCDFError < GenericError 25 | @default = "XCCDF type is unsupported!" 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/gcloud.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'abide_dev_utils/errors/gcloud' 5 | 6 | module AbideDevUtils 7 | module GCloud 8 | include AbideDevUtils::Errors::GCloud 9 | 10 | def self.storage_bucket(name: nil, project: nil, credentials: nil) 11 | raise MissingProjectError if project.nil? && ENV['ABIDE_GCLOUD_PROJECT'].nil? 12 | raise MissingCredentialsError if credentials.nil? && ENV['ABIDE_GCLOUD_CREDENTIALS'].nil? 13 | raise MissingBucketNameError if name.nil? && ENV['ABIDE_GCLOUD_BUCKET'].nil? 14 | 15 | require 'google/cloud/storage' 16 | @bucket = Google::Cloud::Storage.new( 17 | project_id: project || ENV['ABIDE_GCLOUD_PROJECT'], 18 | credentials: credentials || JSON.parse(ENV['ABIDE_GCLOUD_CREDENTIALS']) 19 | ).bucket(name || ENV['ABIDE_GCLOUD_BUCKET']) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/validate/strings/validation_finding.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbideDevUtils 4 | module Sce 5 | module Validate 6 | module Strings 7 | # Represents a validation finding (warning or error) 8 | class ValidationFinding 9 | attr_reader :type, :title, :data 10 | 11 | def initialize(type, title, data) 12 | raise ArgumentError, 'type must be :error or :warning' unless %i[error warning].include?(type) 13 | 14 | @type = type.to_sym 15 | @title = title.to_sym 16 | @data = data 17 | end 18 | 19 | def to_s 20 | "#{@type}: #{@title}: #{@data}" 21 | end 22 | 23 | def to_hash 24 | { type: @type, title: @title, data: @data } 25 | end 26 | alias to_h to_hash 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/errors/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbideDevUtils 4 | module Errors 5 | # Generic error class. Errors in AbideDevUtil all follow the 6 | # same format: " ". Each error has a default 7 | # error message relating to error class name. Subjects should 8 | # always be the thing that failed (file, class, data, etc.). 9 | # @param subject [String] what failed 10 | # @param msg [String] an error message to override the default 11 | class GenericError < StandardError 12 | @default = 'Generic error:' 13 | class << self 14 | attr_reader :default 15 | end 16 | 17 | attr_reader :subject 18 | 19 | def initialize(subject = nil, msg: self.class.default) 20 | @msg = msg 21 | @subject = subject 22 | message = subject.nil? ? @msg : "#{@msg} #{@subject}" 23 | super(message) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/prompt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'io/console' 4 | require_relative 'output' 5 | 6 | module AbideDevUtils 7 | module Prompt 8 | def self.yes_no(msg, auto_approve: false, stream: $stdout) 9 | prompt_msg = "#{msg} (Y/n): " 10 | if auto_approve 11 | AbideDevUtils::Output.simple("#{prompt_msg}Y", stream: stream) 12 | return true 13 | end 14 | 15 | AbideDevUtils::Output.print(prompt_msg, stream: stream) 16 | return true if $stdin.cooked(&:gets).match?(/^[Yy].*/) 17 | 18 | false 19 | end 20 | 21 | def self.single_line(msg, stream: $stdout) 22 | AbideDevUtils::Output.print("#{msg}: ", stream: stream) 23 | $stdin.cooked(&:gets).chomp 24 | end 25 | 26 | def self.username(stream: $stdout) 27 | AbideDevUtils::Output.print('Username: ', stream: stream) 28 | $stdin.cooked(&:gets).chomp 29 | end 30 | 31 | def self.password 32 | $stdin.getpass('Password:') 33 | end 34 | 35 | def self.secure(msg) 36 | $stdin.getpass(msg) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/cli/abstract.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/config' 4 | 5 | module Abide 6 | module CLI 7 | # @abstract 8 | class AbideCommand < CmdParse::Command 9 | include AbideDevUtils::Config 10 | 11 | def initialize(cmd_name, cmd_short, cmd_long, **opts) 12 | super(cmd_name, takes_commands: opts.fetch(:takes_commands, false)) 13 | @deprecated = opts.fetch(:deprecated, false) 14 | if @deprecated 15 | cmd_short = "[DEPRECATED] #{cmd_short}" 16 | cmd_long = "[DEPRECATED] #{cmd_long}" 17 | end 18 | short_desc(cmd_short) 19 | long_desc(cmd_long) 20 | add_command(CmdParse::HelpCommand.new, default: true) if opts[:takes_commands] 21 | end 22 | 23 | def on_after_add 24 | return unless super_command.respond_to?(:deprecated?) && super_command.deprecated? 25 | 26 | short_desc("[DEPRECATED BY PARENT] #{@short_desc}") 27 | long_desc("[DEPRECATED BY PARENT] #{@long_desc}") 28 | end 29 | 30 | def deprecated? 31 | @deprecated 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Heston Snodgrass 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'files' 4 | 5 | module AbideDevUtils 6 | module Config 7 | DEFAULT_PATH = "#{File.expand_path('~')}/.abide_dev.yaml" 8 | 9 | def self.to_h(path = DEFAULT_PATH) 10 | return {} unless File.file?(path) 11 | 12 | h = AbideDevUtils::Files::Reader.read(path) 13 | h.transform_keys(&:to_sym) 14 | end 15 | 16 | def to_h(path = DEFAULT_PATH) 17 | self.class.to_h(path) 18 | end 19 | 20 | def self.config_section(section, path = DEFAULT_PATH) 21 | h = to_h(path) 22 | s = h.fetch(section.to_sym, nil) 23 | return {} if s.nil? 24 | 25 | s.transform_keys(&:to_sym) 26 | end 27 | 28 | def config_section(section, path = DEFAULT_PATH) 29 | h = to_h(path) 30 | s = h.fetch(section.to_sym, nil) 31 | return {} if s.nil? 32 | 33 | s.transform_keys(&:to_sym) 34 | end 35 | 36 | def self.fetch(key, default = nil, path = DEFAULT_PATH) 37 | to_h(path).fetch(key, default) 38 | end 39 | 40 | def fetch(key, default = nil, path = DEFAULT_PATH) 41 | to_h(path).fetch(key, default) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/jira/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'dry_run' 4 | 5 | module AbideDevUtils 6 | module Jira 7 | class Helper 8 | extend DryRun 9 | 10 | dry_run_simple :add_issue_label 11 | dry_run_return_false :summary_exist? 12 | 13 | def initialize(client, dry_run: false) 14 | @client = client 15 | @dry_run = dry_run 16 | end 17 | 18 | # @param project [JIRA::Resource::Project, String] 19 | def all_project_issues_attrs(project) 20 | project = @client.find(:project, project) 21 | project.issues.collect(&:attrs) 22 | end 23 | 24 | # @param issue [JIRA::Resource::Issue, String] 25 | # @param label [String] 26 | def add_issue_label(issue, label) 27 | issue = @client.find(:issue, issue) 28 | return if issue.labels.include?(label) 29 | 30 | issue.labels << label 31 | issue.save 32 | end 33 | 34 | # @param summary [String] 35 | # @param issue_attrs [Array] 36 | def summary_exist?(summary, issue_attrs) 37 | issue_attrs.any? { |attrs| attrs['fields'].key?('summary') && attrs['fields']['summary'] == summary } 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rake' 4 | require "bundler/gem_tasks" 5 | require "rspec/core/rake_task" 6 | 7 | spec_task = RSpec::Core::RakeTask.new(:spec) 8 | spec_task.pattern = 'spec/abide_dev_utils_spec.rb,spec/abide_dev_utils/**/*_spec.rb' 9 | 10 | require "rubocop/rake_task" 11 | 12 | RuboCop::RakeTask.new 13 | 14 | task default: %i[spec rubocop] 15 | 16 | MODULES = %w[puppetlabs-cem_linux puppetlabs-sce_linux puppetlabs-cem_windows puppetlabs-sce_windows].freeze 17 | 18 | def modules_with_repos 19 | @modules_with_repos ||= MODULES.select do |mod| 20 | system("git ls-remote git@github.com:puppetlabs/#{mod}.git HEAD") 21 | end 22 | end 23 | 24 | namespace 'sce' do 25 | directory 'spec/fixtures' 26 | MODULES.each do |mod| 27 | directory "spec/fixtures/#{mod}" do 28 | sh "git clone git@github.com:puppetlabs/#{mod}.git spec/fixtures/#{mod}" 29 | end 30 | end 31 | 32 | task :fixture, [:sce_mod] do |_, args| 33 | mod_name = MODULES.find { |m| m.match?(/#{args.sce_mod}/) } 34 | raise "No fixture found matching #{args.sce_mod}" unless mod_name 35 | 36 | Rake::Task[mod_name].invoke 37 | end 38 | 39 | multitask fixtures: modules_with_repos.map { |m| "spec/fixtures/#{m}" } do 40 | puts "All fixtures are ready" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/validate/resource_data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/ppt' 4 | require 'abide_dev_utils/sce/benchmark' 5 | 6 | module AbideDevUtils 7 | module Sce 8 | module Validate 9 | # Validation methods for resource data 10 | module ResourceData 11 | class ControlsWithoutMapsError < StandardError; end 12 | 13 | def self.controls_without_maps(module_dir = Dir.pwd) 14 | pupmod = AbideDevUtils::Ppt::PuppetModule.new(module_dir) 15 | benchmarks = AbideDevUtils::Sce::Benchmark.benchmarks_from_puppet_module(pupmod) 16 | without_maps = benchmarks.each_with_object({}) do |benchmark, hsh| 17 | puts "Validating #{benchmark.title}..." 18 | hsh[benchmark.title] = benchmark.controls.each_with_object([]) do |ctrl, no_maps| 19 | no_maps << ctrl.id unless ctrl.valid_maps? 20 | end 21 | end 22 | err = ['Found controls in resource data without maps.'] 23 | without_maps.each do |key, val| 24 | next if val.empty? 25 | 26 | err << val.unshift("#{key}:").join("\n ") 27 | end 28 | raise ControlsWithoutMapsError, err.join("\n") unless without_maps.values.all?(&:empty?) 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/errors/ppt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/errors/base' 4 | 5 | module AbideDevUtils 6 | module Errors 7 | module Ppt 8 | class NotModuleDirError < GenericError 9 | @default = 'Path is not a Puppet module directory:' 10 | end 11 | 12 | class ObjClassPathError < GenericError 13 | @default = 'Invalid path for class:' 14 | end 15 | 16 | class CustomObjPathKeyError < GenericError 17 | @default = 'Custom Object value hash does not have :path key: ' 18 | end 19 | 20 | class CustomObjNotFoundError < GenericError 21 | @default = 'Could not find custom object in map:' 22 | end 23 | 24 | class TemplateNotFoundError < GenericError 25 | @default = 'Template does not exist at:' 26 | end 27 | 28 | class FailedToCreateFileError < GenericError 29 | @default = 'Failed to create file:' 30 | end 31 | 32 | class ClassFileNotFoundError < GenericError 33 | @default = 'Class file was not found:' 34 | end 35 | 36 | class ClassDeclarationNotFoundError < GenericError 37 | @default = 'Class declaration was not found:' 38 | end 39 | 40 | class InvalidClassNameError < GenericError 41 | @default = 'Not a valid Puppet class name:' 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /.github/workflows/mend_ruby.yaml: -------------------------------------------------------------------------------- 1 | name: mend_scan 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: checkout repo content 12 | uses: actions/checkout@v2 # checkout the repository content to github runner. 13 | with: 14 | fetch-depth: 1 15 | - name: setup ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: 2.7 19 | - name: create lock 20 | run: bundle lock 21 | # install java 22 | - uses: actions/setup-java@v3 23 | with: 24 | distribution: 'temurin' # See 'Supported distributions' for available options 25 | java-version: '17' 26 | # download mend 27 | - name: download_mend 28 | run: curl -o wss-unified-agent.jar https://unified-agent.s3.amazonaws.com/wss-unified-agent.jar 29 | - name: run mend 30 | run: java -jar wss-unified-agent.jar 31 | env: 32 | WS_APIKEY: ${{ secrets.MEND_API_KEY }} 33 | WS_WSS_URL: https://saas-eu.whitesourcesoftware.com/agent 34 | WS_USERKEY: ${{ secrets.MEND_TOKEN }} 35 | WS_PRODUCTNAME: 'content-and-tooling' # I think this apply for our repo 36 | WS_PROJECTNAME: ${{ github.event.repository.name }} 37 | WS_FILESYSTEMSCAN: true 38 | WS_CHECKPOLICIES: true 39 | WS_FORCEUPDATE: true -------------------------------------------------------------------------------- /lib/abide_dev_utils/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cmdparse' 4 | require 'abide_dev_utils/version' 5 | require 'abide_dev_utils/cli/sce' 6 | require 'abide_dev_utils/constants' 7 | require 'abide_dev_utils/cli/comply' 8 | require 'abide_dev_utils/cli/puppet' 9 | require 'abide_dev_utils/cli/xccdf' 10 | require 'abide_dev_utils/cli/test' 11 | require 'abide_dev_utils/cli/jira' 12 | 13 | module Abide 14 | module CLI 15 | include AbideDevUtils::CliConstants 16 | ROOT_CMD_NAME = 'abide' 17 | ROOT_CMD_BANNER = 'Developer tools for Abide' 18 | DEPRECATED_COMMANDS = %w[comply test].freeze 19 | 20 | def self.new_parser 21 | parser = CmdParse::CommandParser.new(handle_exceptions: true) 22 | parser.main_options.program_name = ROOT_CMD_NAME 23 | parser.main_options.version = AbideDevUtils::VERSION 24 | parser.main_options.banner = ROOT_CMD_BANNER 25 | parser.add_command(CmdParse::HelpCommand.new, default: true) 26 | parser.add_command(CmdParse::VersionCommand.new(add_switches: true)) 27 | parser.add_command(SceCommand.new) 28 | parser.add_command(ComplyCommand.new) 29 | parser.add_command(PuppetCommand.new) 30 | parser.add_command(XccdfCommand.new) 31 | parser.add_command(TestCommand.new) 32 | parser.add_command(JiraCommand.new) 33 | parser 34 | end 35 | 36 | def self.execute(argv = ARGV) 37 | parser = new_parser 38 | parser.parse(argv) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event.pull_request.number }} 14 | cancel-in-progress: true 15 | 16 | # Set a global environment variable for the PUPPET_AUTH_TOKEN for all jobs as well as the BUNDLE_RUBYGEMS___PUPPETCORE__PUPPET__COM 17 | env: 18 | PUPPET_AUTH_TOKEN: ${{ secrets.PUPPET_AUTH_TOKEN }} 19 | BUNDLE_RUBYGEMS___PUPPETCORE__PUPPET__COM: "forge-key:${{ secrets.PUPPET_AUTH_TOKEN }}" 20 | 21 | jobs: 22 | rspec: 23 | runs-on: ubuntu-latest 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | ruby_version: 28 | - '2.7' 29 | - '3.2' 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | 34 | - name: Set up Ruby 35 | uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: ${{ matrix.ruby_version }} 38 | bundler-cache: true 39 | 40 | - name: Print bundle environment 41 | run: | 42 | echo ::group::bundler environment 43 | bundle env 44 | echo ::endgroup:: 45 | 46 | - name: Set up SSH agent 47 | uses: webfactory/ssh-agent@v0.9.0 48 | with: 49 | ssh-private-key: | 50 | ${{ secrets.LINUX_FIXTURE_KEY }} 51 | ${{ secrets.WINDOWS_FIXTURE_KEY }} 52 | 53 | - name: Get fixtures 54 | run: bundle exec rake 'sce:fixtures' 55 | 56 | - name: Run RSpec 57 | run: bundle exec rake spec 58 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/xccdf/parser/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbideDevUtils 4 | module XCCDF 5 | module Parser 6 | module Helpers 7 | # Provides helper methods for working with XML xpaths 8 | module XPath 9 | def find_element 10 | FindElement 11 | end 12 | 13 | # Implements class methods to help with finding elements via XPath 14 | class FindElement 15 | def self.xpath(element, path) 16 | elem = namespace_safe_xpath(element, path) 17 | return named_xpath(element, path) if elem.nil? 18 | 19 | elem 20 | end 21 | 22 | def self.at_xpath(element, path) 23 | elem = namespace_safe_at_xpath(element, path) 24 | return named_at_xpath(element, path) if elem.nil? 25 | 26 | elem 27 | end 28 | 29 | def self.namespace_safe_xpath(element, path) 30 | element.xpath(path) 31 | rescue Nokogiri::XML::XPath::SyntaxError 32 | named_xpath(element, path) 33 | end 34 | 35 | def self.namespace_safe_at_xpath(element, path) 36 | element.at_xpath(path) 37 | rescue Nokogiri::XML::XPath::SyntaxError 38 | named_at_xpath(element, path) 39 | end 40 | 41 | def self.named_xpath(element, path) 42 | element.xpath("*[name()='#{path}']") 43 | end 44 | 45 | def self.named_at_xpath(element, path) 46 | element.at_xpath("*[name()='#{path}']") 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/hiera_data/mapping_data/mixins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbideDevUtils 4 | module Sce 5 | module HieraData 6 | module MappingData 7 | # Mixin module used by Mapper to implement CIS-specific mapping behavior 8 | module MixinCIS 9 | def get_map(control_id, level: nil, profile: nil, **_) 10 | identified_map_data(control_id, valid_types: CIS_TYPES).get(control_id, level: level, profile: profile) 11 | return unless imdata 12 | 13 | if level.nil? || profile.nil? 14 | map_data[mtype][mtop].each do |lvl, profile_hash| 15 | next if lvl == 'benchmark' || (level && level != lvl) 16 | 17 | profile_hash.each do |prof, control_hash| 18 | next if profile && profile != prof 19 | 20 | return control_hash[control_id] if control_hash.key?(control_id) 21 | end 22 | end 23 | else 24 | imdata[level][profile][control_id] 25 | end 26 | end 27 | end 28 | 29 | # Mixin module used by Mapper to implement STIG-specific mapping behavior 30 | module MixinSTIG 31 | def get_map(control_id, level: nil, **_) 32 | mtype, mtop = map_type_and_top_key(control_id) 33 | return unless STIG_TYPES.include?(mtype) 34 | return map_data[mtype][mtop][level][control_id] unless level.nil? 35 | 36 | map_data[mtype][mtop].each do |lvl, control_hash| 37 | next if lvl == 'benchmark' 38 | 39 | return control_hash[control_id] if control_hash.key?(control_id) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2021-01-15 17:47:56 UTC using RuboCop version 1.8.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 2 10 | # Cop supports --auto-correct. 11 | # Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. 12 | # Include: **/*.gemspec 13 | Gemspec/OrderedDependencies: 14 | Exclude: 15 | - 'abide_dev_utils.gemspec' 16 | 17 | # Offense count: 1 18 | # Cop supports --auto-correct. 19 | # Configuration parameters: EnforcedStyle. 20 | # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only 21 | Layout/EmptyLinesAroundClassBody: 22 | Exclude: 23 | - 'lib/abide_dev_utils/xccdf/parser.rb' 24 | 25 | # Offense count: 1 26 | # Configuration parameters: AllowComments. 27 | Lint/EmptyClass: 28 | Exclude: 29 | - 'lib/abide_dev_utils/xccdf/parser.rb' 30 | 31 | # Offense count: 1 32 | # Cop supports --auto-correct. 33 | RSpec/ExpectActual: 34 | Exclude: 35 | - 'spec/routing/**/*' 36 | - 'spec/abide_dev_utils_spec.rb' 37 | 38 | # Offense count: 20 39 | # Cop supports --auto-correct. 40 | # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. 41 | # SupportedStyles: single_quotes, double_quotes 42 | Style/StringLiterals: 43 | Exclude: 44 | - 'Gemfile' 45 | - 'Rakefile' 46 | - 'abide_dev_utils.gemspec' 47 | - 'lib/abide_dev_utils/version.rb' 48 | - 'spec/abide_dev_utils_spec.rb' 49 | - 'spec/spec_helper.rb' 50 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/errors/sce.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/errors/base' 4 | 5 | module AbideDevUtils 6 | module Errors 7 | # Raised by Benchmark when mapping data cannot be loaded 8 | class MappingFilesNotFoundError < GenericError 9 | @default = 'Mapping files not found using facts:' 10 | end 11 | 12 | # Raised by Benchmark when mapping files are not found for the specified framework 13 | class MappingDataFrameworkMismatchError < GenericError 14 | @default = 'Mapping data could not be found for the specified framework:' 15 | end 16 | 17 | # Raised by Benchmark when resource data cannot be loaded 18 | class ResourceDataNotFoundError < GenericError 19 | @default = 'Resource data not found using facts:' 20 | end 21 | 22 | # Raised by Control when it can't find mapping data for itself 23 | class NoMappingDataForControlError < GenericError 24 | @default = 'No mapping data found for control:' 25 | end 26 | 27 | # Raised by a control when it's given ID and framework are incompatible 28 | class ControlIdFrameworkMismatchError < GenericError 29 | @default = 'Control ID is invalid with the given framework:' 30 | end 31 | 32 | # Raised when a benchmark fails to load for a non-specific reason 33 | class BenchmarkLoadError < GenericError 34 | attr_accessor :framework, :osname, :major_version, :module_name, :original_error 35 | 36 | @default = 'Error loading benchmark:' 37 | 38 | def message 39 | [ 40 | "#{super} (#{original_error.class})", 41 | "Framework: #{framework}", 42 | "OS Name: #{osname}", 43 | "OS Version: #{major_version}", 44 | "Module Name: #{module_name}" 45 | ].join(', ') 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /new_diff.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'yaml' 5 | require 'pry' 6 | require 'abide_dev_utils/xccdf/diff/benchmark' 7 | 8 | xml_file1 = File.expand_path(ARGV[0]) 9 | xml_file2 = File.expand_path(ARGV[1]) 10 | legacy_config = ARGV.length > 2 ? YAML.load_file(File.expand_path(ARGV[2])) : nil 11 | 12 | def convert_legacy_config(config, num_title_diff, key_format: :hiera_num) 13 | nt_diff = num_title_diff.diff(key: :number) 14 | updated_config = config['config']['control_configs'].each_with_object({}) do |(key, value), h| 15 | next if value.nil? 16 | 17 | diff_key = key.to_s.gsub(/^c/, '').tr('_', '.') if key_format == :hiera_num 18 | if nt_diff.key?(diff_key) 19 | if nt_diff[diff_key][0][:diff] == :number 20 | new_key = "c#{nt_diff[diff_key][0][:other_number].to_s.tr('.', '_')}" 21 | h[new_key] = value 22 | puts "Converted #{key} to #{new_key}" 23 | elsif nt_diff[diff_key][0][:diff] == :title 24 | 25 | h[key] = value 26 | end 27 | else 28 | h[key] = value 29 | end 30 | end 31 | { 'config' => { 'control_configs' => updated_config } }.to_yaml 32 | end 33 | 34 | start_time = Time.now 35 | 36 | bm_diff = AbideDevUtils::XCCDF::Diff::BenchmarkDiff.new(xml_file1, xml_file2) 37 | self_nc_count, other_nc_count = bm_diff.numbered_children_counts 38 | puts "Benchmark numbered children count: #{self_nc_count}" 39 | puts "Other benchmark numbered children count: #{other_nc_count}" 40 | puts "Rule count difference: #{bm_diff.numbered_children_count_diff}" 41 | num_diff = bm_diff.number_title_diff 42 | binding.pry if legacy_config.nil? 43 | File.open('/tmp/legacy_converted.yaml', 'w') do |f| 44 | converted = convert_legacy_config(legacy_config, num_diff) 45 | f.write(converted) 46 | end 47 | 48 | puts "Computation time: #{Time.now - start_time}" 49 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/jira/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'client_builder' 4 | require_relative 'dry_run' 5 | require_relative 'finder' 6 | require_relative 'helper' 7 | require_relative 'issue_builder' 8 | require_relative '../config' 9 | require_relative '../errors/jira' 10 | 11 | module AbideDevUtils 12 | module Jira 13 | class Client 14 | extend DryRun 15 | 16 | dry_run :create, :find, :myself 17 | 18 | attr_accessor :default_project 19 | attr_reader :config 20 | 21 | def initialize(dry_run: false, **options) 22 | @dry_run = dry_run 23 | @options = options 24 | @config = AbideDevUtils::Config.config_section('jira') 25 | @default_project = @config[:default_project] 26 | @client = nil 27 | @finder = nil 28 | @issue_builder = nil 29 | @helper = nil 30 | end 31 | 32 | def myself 33 | @myself ||= finder.myself 34 | end 35 | 36 | def find(type, id) 37 | raise ArgumentError, "Invalid type #{type}" unless finder.respond_to?(type.to_sym) 38 | 39 | finder.send(type.to_sym, id) 40 | end 41 | 42 | def create(type, **fields) 43 | issue_builder.create(type, **fields) 44 | end 45 | 46 | def helper 47 | @helper ||= Helper.new(self, dry_run: @dry_run) 48 | end 49 | 50 | def translate_issue_custom_field(name) 51 | IssueBuilder::CUSTOM_FIELDS[name] || IssueBuilder::CUSTOM_FIELDS.invert[name] 52 | end 53 | 54 | private 55 | 56 | def client 57 | @client ||= ClientBuilder.new(@config, **@options).build 58 | end 59 | 60 | def finder 61 | @finder ||= Finder.new(client) 62 | end 63 | 64 | def issue_builder 65 | @issue_builder ||= IssueBuilder.new(client, finder) 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/jira/finder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../errors/jira' 4 | 5 | module AbideDevUtils 6 | module Jira 7 | class Finder 8 | def initialize(client) 9 | @client = client 10 | end 11 | 12 | def myself 13 | client.User.myself 14 | end 15 | 16 | # @param id [String] The project key or ID 17 | def project(id) 18 | return id if id.is_a?(client.Project.target_class) 19 | 20 | client.Project.find(id) 21 | end 22 | 23 | # @param id [String] The issue key or summary 24 | def issue(id) 25 | return id if id.is_a?(client.Issue.target_class) 26 | 27 | client.Issue.find(id) 28 | rescue URI::InvalidURIError 29 | iss = client.Issue.all.find { |i| i.summary == id } 30 | raise AbideDevUtils::Errors::Jira::FindIssueError, id if iss.nil? 31 | 32 | iss 33 | end 34 | 35 | # @param jql [String] The JQL query 36 | # @return [Array] 37 | def issues_by_jql(jql) 38 | client.Issue.jql(jql, max_results: 1000) 39 | end 40 | 41 | # @param id [String] The issuetype ID or name 42 | def issuetype(id) 43 | return id if id.is_a?(client.Issuetype.target_class) 44 | 45 | if id.match?(%r{^\d+$}) 46 | client.Issuetype.find(id) 47 | else 48 | client.Issuetype.all.find { |i| i.name == id } 49 | end 50 | end 51 | 52 | # @param id [String] The priority ID or name 53 | def priority(id) 54 | return id if id.is_a?(client.Priority.target_class) 55 | 56 | if id.match?(%r{^\d+$}) 57 | client.Priority.find(id) 58 | else 59 | client.Priority.all.find { |i| i.name == id } 60 | end 61 | end 62 | 63 | private 64 | 65 | attr_reader :client 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/code_gen/resource.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbideDevUtils 4 | module Ppt 5 | module CodeGen 6 | class Resource 7 | attr_reader :type, :title 8 | 9 | def initialize(type, title, **attributes) 10 | validate_type_and_title(type, title) 11 | @type = type 12 | @title = title 13 | @attributes = attributes 14 | end 15 | 16 | def reference 17 | "#{title.split('::').map(&:capitalize).join('::')}['#{title}']" 18 | end 19 | 20 | def to_s 21 | return "#{type} { '#{title}': }" if @attributes.empty? 22 | 23 | str_array = ["#{type} { '#{title}':"] 24 | @attributes.each do |key, val| 25 | str_array << " #{pad_attribute(key)} => #{val}," 26 | end 27 | str_array << '}' 28 | str_array.join("\n") 29 | end 30 | 31 | private 32 | 33 | def validate_type_and_title(type, title) 34 | raise 'Type / title must be String' unless type.is_a?(String) && title.is_a?(String) 35 | raise 'Type / title must not be empty' if type.empty? || title.empty? 36 | end 37 | 38 | def longest_attribute_length 39 | return @longest_attribute_length if defined?(@longest_attribute_length) 40 | 41 | longest = '' 42 | @attributes.each_key do |k| 43 | longest = k if k.length > longest.length 44 | end 45 | @longest_attribute_length = longest.length 46 | @longest_attribute_length 47 | end 48 | 49 | def pad_attribute(attribute) 50 | return attribute if attribute.length == longest_attribute_length 51 | 52 | attr_array = [attribute] 53 | (longest_attribute_length - attribute.length).times { attr_array << ' ' } 54 | attr_array.join 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/xccdf/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/files' 4 | require 'abide_dev_utils/xccdf/parser/objects' 5 | 6 | module AbideDevUtils 7 | module XCCDF 8 | # Contains methods and classes for parsing XCCDF files, 9 | module Parser 10 | def self.parse(file_path) 11 | doc = AbideDevUtils::Files::Reader.read(file_path) 12 | doc.remove_namespaces! 13 | benchmark = AbideDevUtils::XCCDF::Parser::Objects::Benchmark.new(doc) 14 | Linker.resolve_links(benchmark) 15 | benchmark 16 | end 17 | 18 | # Links XCCDF objects by reference. 19 | # Each link is resolved and then a bidirectional link is established 20 | # between the two objects. 21 | module Linker 22 | def self.resolve_links(benchmark) 23 | link_profile_rules(benchmark) 24 | link_rule_values(benchmark) 25 | end 26 | 27 | def self.link_profile_rules(benchmark) 28 | return unless benchmark.respond_to?(:profile) 29 | 30 | rules = benchmark.descendants.select { |d| d.label == 'rule' } 31 | benchmark.profile.each do |profile| 32 | profile.xccdf_select.each do |sel| 33 | rules.select { |rule| rule.id.value == sel.idref.value }.each do |rule| 34 | rule.add_link(profile) 35 | profile.add_link(rule) 36 | end 37 | end 38 | end 39 | end 40 | 41 | def self.link_rule_values(benchmark) 42 | return unless benchmark.respond_to?(:value) 43 | 44 | rules = benchmark.descendants.select { |d| d.label == 'rule' } 45 | benchmark.value.each do |value| 46 | rule = rules.find { |r| r.title.to_s == value.title.to_s } 47 | next unless rule 48 | 49 | rule.add_link(value) 50 | value.add_link(rule) 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/validate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/errors' 4 | 5 | module AbideDevUtils 6 | # Methods used for validating data 7 | module Validate 8 | def self.puppet_module_directory(path = Dir.pwd) 9 | raise AbideDevUtils::Errors::Ppt::NotModuleDirError, path unless File.file?(File.join(path, 'metadata.json')) 10 | end 11 | 12 | def self.filesystem_path(path) 13 | raise AbideDevUtils::Errors::FileNotFoundError, path unless File.exist?(path) 14 | end 15 | 16 | def self.file(path, extension: nil) 17 | filesystem_path(path) 18 | raise AbideDevUtils::Errors::PathNotFileError, path unless File.file?(path) 19 | return if extension.nil? 20 | 21 | file_ext = extension.match?(/^\.[A-Za-z0-9]+$/) ? extension : ".#{extension}" 22 | raise AbideDevUtils::Errors::FileExtensionIncorrectError, extension unless File.extname(path) == file_ext 23 | end 24 | 25 | def self.directory(path) 26 | filesystem_path(path) 27 | raise AbideDevUtils::Errors::PathNotDirectoryError, path unless File.directory?(path) 28 | end 29 | 30 | def self.populated_string?(thing) 31 | return false if thing.nil? 32 | return false unless thing.instance_of?(String) 33 | return false if thing.empty? 34 | 35 | true 36 | end 37 | 38 | def self.populated_string(thing) 39 | raise AbideDevUtils::Errors::NotPopulatedStringError, 'Object is nil' if thing.nil? 40 | 41 | unless thing.instance_of?(String) 42 | raise AbideDevUtils::Errors::NotPopulatedStringError, "Object is not a String. Type: #{thing.class}" 43 | end 44 | raise AbideDevUtils::Errors::NotPopulatedStringError, 'String is empty' if thing.empty? 45 | end 46 | 47 | def self.not_empty(thing, msg) 48 | raise AbideDevUtils::Errors::ObjectEmptyError, msg if thing.empty? 49 | end 50 | 51 | def self.hashable(obj) 52 | return if obj.respond_to?(:to_hash) || obj.respond_to?(:to_h) 53 | 54 | raise AbideDevUtils::Errors::NotHashableError, obj 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/puppet_module.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'yaml' 5 | require 'abide_dev_utils/validate' 6 | require 'abide_dev_utils/ppt/hiera' 7 | 8 | module AbideDevUtils 9 | module Ppt 10 | # Class for working with Puppet Modules 11 | class PuppetModule 12 | DEF_FILES = { 13 | metadata: 'metadata.json', 14 | readme: 'README.md', 15 | reference: 'REFERENCE.md', 16 | changelog: 'CHANGELOG.md', 17 | hiera_config: 'hiera.yaml', 18 | fixtures: '.fixtures.yml', 19 | rubocop: '.rubocop.yml', 20 | sync: '.sync.yml', 21 | pdkignore: '.pdkignore', 22 | gitignore: '.gitignore' 23 | }.freeze 24 | 25 | attr_reader :directory, :special_files 26 | 27 | def initialize(directory = Dir.pwd) 28 | AbideDevUtils::Validate.directory(directory) 29 | @directory = directory 30 | @special_files = DEF_FILES.dup.transform_values { |v| File.expand_path(File.join(@directory, v)) } 31 | end 32 | 33 | def name(strip_namespace: false) 34 | strip_namespace ? metadata['name'].split('-')[-1] : metadata['name'] 35 | end 36 | 37 | def metadata 38 | @metadata ||= JSON.parse(File.read(special_files[:metadata])) 39 | end 40 | 41 | def supported_os 42 | @supported_os ||= find_supported_os 43 | end 44 | 45 | def hiera_conf 46 | @hiera_conf ||= AbideDevUtils::Ppt::Hiera::Config.new(special_files[:hiera_config]) 47 | end 48 | 49 | private 50 | 51 | def find_supported_os 52 | return [] unless metadata['operatingsystem_support'] 53 | 54 | metadata['operatingsystem_support'].each_with_object([]) do |os, arr| 55 | os['operatingsystemrelease'].each do |r| 56 | arr << "#{os['operatingsystem']}::#{r}" 57 | end 58 | end 59 | end 60 | 61 | def in_dir 62 | return unless block_given? 63 | 64 | current = Dir.pwd 65 | if current == File.expand_path(directory) 66 | yield 67 | else 68 | Dir.chdir(directory) 69 | yield 70 | Dir.chdir(current) 71 | end 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/code_gen/data_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'puppet' 4 | 5 | module AbideDevUtils 6 | module Ppt 7 | module CodeGen 8 | module DataTypes 9 | def infer_data_type(data) 10 | Puppet::Pops::Types::TypeCalculator.infer(data).to_s 11 | end 12 | 13 | # Displays a Puppet type value as a string 14 | def display_value(val) 15 | if val.is_a?(Puppet::Pops::Model::LiteralUndef) 16 | 'undef' 17 | elsif val.respond_to?(:value) 18 | display_value(val.value) 19 | elsif val.respond_to?(:cased_value) 20 | display_value(val.cased_value) 21 | else 22 | val 23 | end 24 | end 25 | 26 | # Displays a Puppet type expression (type signature) as a string 27 | # @param param [Puppet::Pops::Model::Parameter] AST Parameter node of a parsed Puppet manifest 28 | def display_type_expr(param) 29 | te = param.respond_to?(:type_expr) ? param.type_expr : param 30 | if te.respond_to? :left_expr 31 | display_type_expr_with_left_expr(te) 32 | elsif te.respond_to? :entries 33 | display_type_expr_with_entries(te) 34 | elsif te.respond_to? :cased_value 35 | te.cased_value 36 | elsif te.respond_to? :value 37 | te.value 38 | end 39 | end 40 | 41 | # Used by #display_type_expr 42 | def display_type_expr_with_left_expr(te) 43 | cased = nil 44 | keys = nil 45 | cased = te.left_expr.cased_value if te.left_expr.respond_to? :cased_value 46 | keys = te.keys.map { |x| display_type_expr(x) }.to_s if te.respond_to? :keys 47 | keys.tr!('"', '') unless cased == 'Enum' 48 | "#{cased}#{keys}" 49 | end 50 | 51 | # Used by #display_type_expr 52 | def display_type_expr_with_entries(te) 53 | te.entries.each_with_object({}) do |x, hsh| 54 | key = nil 55 | val = nil 56 | key = display_value(x.key) if x.respond_to? :key 57 | val = display_type_expr(x.value) if x.respond_to? :value 58 | hsh[key] = val if key 59 | end 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/errors/general.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/errors/base' 4 | 5 | module AbideDevUtils 6 | module Errors 7 | # Raised when something is empty and it shouldn't be 8 | class ObjectEmptyError < GenericError 9 | @default = 'Object is empty and should not be:' 10 | end 11 | 12 | # Raised when something is not a string, or is an empty string 13 | class NotPopulatedStringError < GenericError 14 | @default = 'Object is either not a String or is empty:' 15 | end 16 | 17 | # Raised when an object is initialized with a nil param 18 | class NewObjectParamNilError < GenericError 19 | @default = 'Object init parameter is nil and should not be:' 20 | end 21 | 22 | # Raised when a file path does not exist 23 | class FileNotFoundError < GenericError 24 | @default = 'File not found:' 25 | end 26 | 27 | # Raised when a file path is not a regular file 28 | class PathNotFileError < GenericError 29 | @default = 'Path is not a regular file:' 30 | end 31 | 32 | # Raised when the path is not a directory 33 | class PathNotDirectoryError < GenericError 34 | @default = 'Path is not a directory:' 35 | end 36 | 37 | # Raised when a file extension is not correct 38 | class FileExtensionIncorrectError < GenericError 39 | @default = 'File extension does not match specified extension:' 40 | end 41 | 42 | # Raised when a searched for service is not found in the parser 43 | class ServiceNotFoundError < GenericError 44 | @default = 'Service not found:' 45 | end 46 | 47 | # Raised when getting an InetdConfConfig object that does not exist 48 | class ConfigObjectNotFoundError < GenericError 49 | @default = 'Config object not found:' 50 | end 51 | 52 | # Raised when adding an InetdConfConfig object that already exists 53 | class ConfigObjectExistsError < GenericError 54 | @default = 'Config object already exists:' 55 | end 56 | 57 | # Raised when an object should respond to :to_hash or :to_h and doesn't 58 | class NotHashableError < GenericError 59 | @default = 'Object does not respond to #to_hash or #to_h:' 60 | end 61 | 62 | # Raised when conflicting CLI options are specified for a command 63 | class CliOptionsConflict < GenericError 64 | @default = 'Console options conflict:' 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/xccdf' 4 | require 'abide_dev_utils/sce/generate' 5 | require 'abide_dev_utils/sce/validate' 6 | 7 | module AbideDevUtils 8 | # Methods for working with Security Compliance Enforcement (SCE) modules 9 | module Sce 10 | def self.xccdf 11 | return @xccdf if defined?(@xccdf) 12 | 13 | xccdf = Object.new 14 | xccdf.extend AbideDevUtils::XCCDF::Common 15 | @xccdf = xccdf 16 | @xccdf 17 | end 18 | 19 | def self.rule_id_format(rule_id) 20 | case rule_id 21 | when /^c[0-9_]+$/ 22 | :hiera_title_num 23 | when /^[a-z][a-z0-9_]+$/ 24 | :hiera_title 25 | when /^[0-9.]+$/ 26 | :number 27 | else 28 | :title 29 | end 30 | end 31 | 32 | def self.rule_identifiers(rule_id) 33 | { 34 | number: xccdf.control_parts(rule_id).first, 35 | hiera_title: xccdf.name_normalize_control(rule_id), 36 | hiera_title_num: xccdf.number_normalize_control(rule_id) 37 | } 38 | end 39 | 40 | def self.update_legacy_config_from_diff(config_hiera, diff) 41 | new_config_hiera = config_hiera.dup 42 | new_control_configs = {} 43 | change_report = [] 44 | changes = diff.select { |d| d[:type][0] == :number } 45 | config_hiera['config']['control_configs'].each do |key, val_hash| 46 | key_id_format = rule_id_format(key) 47 | changed = false 48 | changes.each do |change| 49 | if key_id_format == :title 50 | next unless change[:title] == key 51 | else 52 | next unless rule_identifiers(change[:self].id)[key_id_format] == key 53 | end 54 | 55 | changed = true 56 | new_key = if key_id_format == :title 57 | change[:other_title] 58 | else 59 | rule_identifiers(change[:other].id)[key_id_format] 60 | end 61 | new_control_configs[new_key] = val_hash 62 | change_report << { 63 | type: :identifier_update, 64 | from: key, 65 | to: new_key 66 | } 67 | end 68 | new_control_configs[key] = val_hash unless changed 69 | end 70 | new_config_hiera['config']['control_configs'] = new_control_configs 71 | [new_config_hiera, change_report] 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/abide_dev_utils/xccdf/parser/objects_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | # require 'abide_dev_utils/xccdf/parser' 5 | # require 'abide_dev_utils/xccdf/parser/objects' 6 | 7 | RSpec.describe AbideDevUtils::XCCDF::Parser::Objects do 8 | let(:file_path) { test_xccdf_files.find { |f| f.end_with?('v1.0.0-xccdf.xml') } } 9 | let(:benchmark) { AbideDevUtils::XCCDF::Parser.parse(file_path) } 10 | 11 | describe AbideDevUtils::XCCDF::Parser::Objects::Benchmark do 12 | describe 'class methods' do 13 | describe '#xpath' do 14 | it 'returns the xpath to the benchmark' do 15 | expect(described_class.xpath).to eq('Benchmark') 16 | end 17 | end 18 | end 19 | 20 | describe 'instance methods' do 21 | describe '#to_s' do 22 | it 'returns a string representation of the benchmark' do 23 | expect(benchmark.to_s).to eq('Test XCCDF 1.0.0') 24 | end 25 | end 26 | 27 | describe '#version' do 28 | it 'returns the benchmark version' do 29 | expect(benchmark.version.to_s).to eq('1.0.0') 30 | end 31 | end 32 | 33 | describe '#title' do 34 | it 'returns the benchmark title' do 35 | expect(benchmark.title.to_s).to eq('Test XCCDF') 36 | end 37 | end 38 | 39 | describe '#group' do 40 | it 'returns the correct number of groups' do 41 | expect(benchmark.group.count).to eq(2) 42 | end 43 | end 44 | 45 | describe '#value' do 46 | it 'returns the correct number of values' do 47 | expect(benchmark.value.count).to eq(6) 48 | end 49 | end 50 | 51 | describe 'profile' do 52 | it 'returns correct amount' do 53 | expect(benchmark.profile.count).to eq(2) 54 | end 55 | 56 | it 'returns profile objects with correct titles' do 57 | expect(benchmark.profile.map { |p| p.title.to_s }).to satisfy('correct titles included') do |t| 58 | t.include?('Level 1 (L1) - Profile 1') && t.include?('Level 2 (L2) - Profile 2') 59 | end 60 | end 61 | 62 | it 'returns profile objects with correct levels' do 63 | expect(benchmark.profile.map(&:level)).to satisfy('correct levels included') do |l| 64 | l.include?('Level 1') && l.include?('Level 2') 65 | end 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/abide_dev_utils/sce/benchmark_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe('AbideDevUtils::Sce::Benchmark') do 6 | { sce_linux: sce_linux_fixture, sce_windows: sce_windows_fixture }.each do |mname, fix| 7 | context "with #{mname}" do 8 | # Don't use :let or :let! here because, for some reason, the objects are not properly 9 | # memoized and are re-created for each test. This is a huge performance hit. 10 | test_objs = [] 11 | Dir.chdir(fix) do 12 | test_objs << AbideDevUtils::Ppt::PuppetModule.new 13 | test_objs << AbideDevUtils::Sce::BenchmarkLoader.benchmarks_from_puppet_module( 14 | ignore_framework_mismatch: true 15 | ) 16 | end 17 | 18 | context 'when supplied a PuppetModule' do 19 | it 'creates benchmark objects correctly' do 20 | expect(test_objs.last.empty?).not_to be_truthy 21 | end 22 | 23 | it 'creates the correct number of objects' do 24 | # We use greater than or equal to here because the number of benchmarks 25 | # should always be greater than or equal to the number of supported OSes 26 | # for the module. The reason it will be grater is because of supporting 27 | # multiple benchmarks for a single OS (e.g. STIG and CIS for RHEL) 28 | expect(test_objs.last.length).to be >= test_objs.first.supported_os.length 29 | end 30 | 31 | it 'creates objects with resource data' do 32 | expect(test_objs.last.all? { |b| !b.resource_data.nil? && !b.resource_data.empty? }).to be_truthy 33 | end 34 | 35 | it 'creates objects with mapping data' do 36 | expect(test_objs.last.all? { |b| !b.map_data.nil? && !b.map_data.empty? }).to be_truthy 37 | end 38 | 39 | it 'creates objects with title' do 40 | expect(test_objs.last.all? { |b| b.title.is_a?(String) && !b.title.empty? }).to be_truthy 41 | end 42 | 43 | it 'creates objects with version' do 44 | expect(test_objs.last.all? { |b| b.version.is_a?(String) && !b.version.empty? }).to be_truthy 45 | end 46 | 47 | it 'creates objects with title key' do 48 | expect(test_objs.last.all? { |b| b.title_key.is_a?(String) && !b.title_key.empty? }).to be_truthy 49 | end 50 | 51 | it 'creates objects with controls' do 52 | expect(test_objs.last.all? { |b| b.controls.is_a?(Array) && !b.controls.empty? }).to be_truthy 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/xccdf/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/validate' 4 | 5 | module AbideDevUtils 6 | module XCCDF 7 | module Utils 8 | # Class for working with directories that contain XCCDF files 9 | class FileDir 10 | CIS_FILE_NAME_PARTS_PATTERN = /^CIS_(?[A-Za-z0-9._()-]+)_Benchmark_v(?[0-9.]+)-xccdf$/.freeze 11 | def initialize(path) 12 | @path = File.expand_path(path) 13 | AbideDevUtils::Validate.directory(@path) 14 | end 15 | 16 | def files 17 | @files ||= Dir.glob(File.join(@path, '*-xccdf.xml')).map { |f| FileNameData.new(f) } 18 | end 19 | 20 | def fuzzy_find(label, value) 21 | files.find { |f| f.fuzzy_match?(label, value) } 22 | end 23 | 24 | def fuzzy_select(label, value) 25 | files.select { |f| f.fuzzy_match?(label, value) } 26 | end 27 | 28 | def fuzzy_reject(label, value) 29 | files.reject { |f| f.fuzzy_match?(label, value) } 30 | end 31 | 32 | def label?(label) 33 | files.select { |f| f.has?(label) } 34 | end 35 | 36 | def no_label?(label) 37 | files.reject { |f| f.has?(label) } 38 | end 39 | end 40 | 41 | # Parses XCCDF file names into labeled parts 42 | class FileNameData 43 | CIS_PATTERN = /^CIS_(?[A-Za-z0-9._()-]+?)(?_STIG)?_Benchmark_v(?[0-9.]+)-xccdf$/.freeze 44 | 45 | attr_reader :path, :name, :labeled_parts 46 | 47 | def initialize(path) 48 | @path = path 49 | @name = File.basename(path, '.xml') 50 | @labeled_parts = File.basename(name, '.xml').match(CIS_PATTERN)&.named_captures 51 | end 52 | 53 | def subject 54 | @subject ||= labeled_parts&.fetch('subject', nil) 55 | end 56 | 57 | def stig 58 | @stig ||= labeled_parts&.fetch('subject', nil) 59 | end 60 | 61 | def version 62 | @version ||= labeled_parts&.fetch('version', nil) 63 | end 64 | 65 | def has?(label) 66 | val = send(label.to_sym) 67 | !val.nil? && !val.empty? 68 | end 69 | 70 | def fuzzy_match?(label, value) 71 | return false unless has?(label) 72 | 73 | this_val = normalize_char_array(send(label.to_sym).chars) 74 | other_val = normalize_char_array(value.chars) 75 | other_val.each_with_index do |c, idx| 76 | return false unless this_val[idx] == c 77 | end 78 | true 79 | end 80 | 81 | private 82 | 83 | def normalize_char_array(char_array) 84 | char_array.grep_v(/[^A-Za-z0-9]/).map(&:downcase)[3..] 85 | end 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /abide_dev_utils.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "abide_dev_utils/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "abide_dev_utils" 9 | spec.version = AbideDevUtils::VERSION 10 | spec.authors = ["abide-team"] 11 | spec.email = ["abide-team@puppet.com"] 12 | 13 | spec.summary = "Helper utilities for developing compliance Puppet code" 14 | spec.description = "Provides a CLI with helpful utilities for developing compliance Puppet code" 15 | spec.homepage = "https://github.com/puppetlabs/abide_dev_utils" 16 | spec.license = "MIT" 17 | spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0") 18 | 19 | # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" 20 | 21 | spec.metadata["homepage_uri"] = spec.homepage 22 | spec.metadata["source_code_uri"] = spec.homepage 23 | spec.metadata["changelog_uri"] = spec.homepage 24 | spec.metadata['rubygems_mfa_required'] = 'true' 25 | 26 | # Specify which files should be added to the gem when it is released. 27 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 28 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 29 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } 30 | end 31 | spec.bindir = 'exe' 32 | spec.executables = ['abide'] 33 | spec.require_paths = ['lib'] 34 | 35 | # Prod dependencies 36 | spec.add_dependency 'nokogiri', '~> 1.13' 37 | spec.add_dependency 'cmdparse', '~> 3.0' 38 | spec.add_dependency 'puppet-strings', '>= 2.7' 39 | spec.add_dependency 'jira-ruby', '~> 2.2' 40 | spec.add_dependency 'ruby-progressbar', '~> 1.11' 41 | spec.add_dependency 'selenium-webdriver', '~> 4.0.0.beta4' 42 | spec.add_dependency 'google-cloud-storage', '~> 1.34' 43 | spec.add_dependency 'hashdiff', '~> 1.0' 44 | spec.add_dependency 'facterdb', '~> 2.1.0' 45 | spec.add_dependency 'metadata-json-lint', '~> 4.0' 46 | 47 | # Dev dependencies 48 | spec.add_development_dependency 'bundler' 49 | spec.add_development_dependency 'rake' 50 | spec.add_development_dependency 'console' 51 | spec.add_development_dependency 'github_changelog_generator' 52 | spec.add_development_dependency 'gem-release' 53 | spec.add_development_dependency 'pry' 54 | spec.add_development_dependency 'rspec', '~> 3.10' 55 | spec.add_development_dependency 'rubocop', '~> 1.8' 56 | spec.add_development_dependency 'rubocop-rspec', '~> 2.1' 57 | spec.add_development_dependency 'rubocop-ast', '~> 1.4' 58 | spec.add_development_dependency 'rubocop-performance', '~> 1.9' 59 | spec.add_development_dependency 'rubocop-i18n', '~> 3.0' 60 | spec.add_development_dependency 'fast_gettext', '>= 2.0' 61 | end 62 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/output.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | require 'pp' 5 | require 'yaml' 6 | require 'ruby-progressbar' 7 | require 'abide_dev_utils/validate' 8 | require 'abide_dev_utils/files' 9 | 10 | module AbideDevUtils 11 | module Output 12 | FWRITER = AbideDevUtils::Files::Writer.new 13 | def self.simple_section_separator(section_text, sepchar: '#', width: 60, **_) 14 | section_text = section_text.to_s 15 | section_text = section_text[0..width - 4] if section_text.length > width 16 | section_text = " #{section_text} " 17 | section_sep_line = sepchar * width 18 | [section_sep_line, section_text.center(width, sepchar), section_sep_line].join("\n") 19 | end 20 | 21 | def self.simple(msg, stream: $stdout, **_) 22 | case msg 23 | when Hash 24 | stream.puts JSON.pretty_generate(msg) 25 | else 26 | stream.puts msg 27 | end 28 | end 29 | 30 | def self.print(msg, stream: $stdout, **_) 31 | stream.print msg 32 | end 33 | 34 | def self.text(msg, console: false, file: nil, **_) 35 | simple(msg) if console 36 | FWRITER.write_text(msg, file: file) unless file.nil? 37 | end 38 | 39 | def self.json(in_obj, console: false, file: nil, pretty: true, **_) 40 | AbideDevUtils::Validate.hashable(in_obj) 41 | json_out = pretty ? JSON.pretty_generate(in_obj) : JSON.generate(in_obj) 42 | simple(json_out) if console 43 | FWRITER.write_json(json_out, file: file) unless file.nil? 44 | end 45 | 46 | def self.yaml(in_obj, console: false, file: nil, stringify: false, **_) 47 | yaml_out = if in_obj.is_a? String 48 | in_obj 49 | else 50 | AbideDevUtils::Validate.hashable(in_obj) 51 | if stringify 52 | JSON.parse(JSON.generate(in_obj)).to_yaml 53 | else 54 | # Use object's #to_yaml method if it exists, convert to hash if not 55 | in_obj.respond_to?(:to_yaml) ? in_obj.to_yaml : in_obj.to_h.to_yaml 56 | end 57 | end 58 | simple(yaml_out) if console 59 | FWRITER.write_yaml(yaml_out, file: file) unless file.nil? 60 | end 61 | 62 | def self.yml(in_obj, console: false, file: nil, **_) 63 | AbideDevUtils::Validate.hashable(in_obj) 64 | # Use object's #to_yaml method if it exists, convert to hash if not 65 | yml_out = in_obj.respond_to?(:to_yaml) ? in_obj.to_yaml : in_obj.to_h.to_yaml 66 | simple(yml_out) if console 67 | FWRITER.write_yml(yml_out, file: file) unless file.nil? 68 | end 69 | 70 | def self.progress(title: 'Progress', start: 0, total: 100, format: nil, **_) 71 | ProgressBar.create(title: title, starting_at: start, total: total, format: format) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/files.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/validate' 4 | 5 | module AbideDevUtils 6 | module Files 7 | class Reader 8 | def self.read(path, raw: false, safe: true, opts: {}) 9 | AbideDevUtils::Validate.file(path) 10 | return File.read(path) if raw 11 | 12 | extension = File.extname(path) 13 | case extension 14 | when /\.yaml|\.yml/ 15 | read_yaml(path, safe: safe, opts: opts) 16 | when '.json' 17 | require 'json' 18 | return JSON.parse(File.read(path), opts) if safe 19 | 20 | JSON.parse!(File.read(path), opts) 21 | when '.xml' 22 | require 'nokogiri' 23 | File.open(path, 'r') do |file| 24 | Nokogiri::XML.parse(file) do |config| 25 | config.strict.noblanks.norecover 26 | end 27 | end 28 | else 29 | File.read(path) 30 | end 31 | end 32 | 33 | def self.read_yaml(path, safe: true, opts: { permitted_classes: [Symbol] }) 34 | permitted_classes = opts[:permitted_classes] || [Symbol] 35 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0') 36 | require 'psych' 37 | return Psych.safe_load_file(path, permitted_classes: permitted_classes) if safe 38 | 39 | Psych.load_file(path) 40 | else 41 | require 'yaml' 42 | return YAML.safe_load(File.read(path), permitted_classes) if safe 43 | 44 | YAML.load(File.read(path)) # rubocop:disable Security/YAMLLoad 45 | end 46 | end 47 | end 48 | 49 | class Writer 50 | MSG_EXT_APPEND = 'Appending %s extension to file' 51 | 52 | def write(content, file: nil, add_ext: true, file_ext: nil) 53 | valid_file = add_ext ? append_ext(file, file_ext) : file 54 | File.open(valid_file, 'w') { |f| f.write(content) } 55 | verify_write(valid_file) 56 | end 57 | 58 | def method_missing(m, *args, **kwargs, &_block) 59 | if m.to_s.match?(/^write_/) 60 | ext = m.to_s.split('_')[-1] 61 | write(args[0], **kwargs, file_ext: ext) 62 | else 63 | super 64 | end 65 | end 66 | 67 | def respond_to_missing?(method_name, include_private = false) 68 | method_name.to_s.start_with?('write_') || super 69 | end 70 | 71 | def append_ext(file_path, ext) 72 | return file_path if ext.nil? 73 | 74 | s_ext = ".#{ext}" 75 | unless File.extname(file_path) == s_ext 76 | puts MSG_EXT_APPEND % s_ext 77 | file_path << s_ext 78 | end 79 | file_path 80 | end 81 | 82 | def verify_write(file_path) 83 | if File.file?(file_path) 84 | puts "Successfully wrote to #{file_path}" 85 | else 86 | puts "Something went wrong! Failed writing to #{file_path}!" 87 | end 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative File.expand_path('../lib/abide_dev_utils.rb', __dir__) 4 | Dir.glob(File.expand_path('../lib/abide_dev_utils/**/*.rb', __dir__)).sort.each do |f| 5 | require_relative f 6 | rescue LoadError => e 7 | puts "Error loading #{f}: #{e}" 8 | end 9 | 10 | module TestResources 11 | def fixtures_dir 12 | File.expand_path(File.join(__dir__, 'fixtures')) 13 | end 14 | 15 | def sce_linux_fixture 16 | sce_dir = File.join(fixtures_dir, 'puppetlabs-sce_linux') 17 | return sce_dir if Dir.exist?(sce_dir) && !Dir.empty?(sce_dir) 18 | 19 | File.join(fixtures_dir, 'puppetlabs-cem_linux') 20 | end 21 | 22 | def sce_windows_fixture 23 | sce_dir = File.join(fixtures_dir, 'puppetlabs-sce_windows') 24 | return sce_dir if Dir.exist?(sce_dir) && !Dir.empty?(sce_dir) 25 | 26 | File.join(fixtures_dir, 'puppetlabs-cem_windows') 27 | end 28 | 29 | def resources_dir 30 | File.expand_path(File.join(__dir__, "resources")) 31 | end 32 | 33 | def all_test_files 34 | Dir.glob(File.join(resources_dir, "test_files", "*")) 35 | end 36 | 37 | def test_xccdf_files 38 | all_test_files.select { |f| f.end_with?("-xccdf.xml") } 39 | end 40 | 41 | def lib_dir 42 | File.expand_path(File.join(__dir__, '..', 'lib')) 43 | end 44 | end 45 | 46 | module OutputHelpers 47 | def capture_stdout 48 | original_stdout = $stdout 49 | $stdout = StringIO.new 50 | yield 51 | $stdout.string 52 | ensure 53 | $stdout = original_stdout 54 | end 55 | 56 | def capture_stderr 57 | original_stderr = $stderr 58 | $stderr = StringIO.new 59 | yield 60 | $stderr.string 61 | ensure 62 | $stderr = original_stderr 63 | end 64 | 65 | def capture_stdout_stderr 66 | original_stdout = $stdout 67 | original_stderr = $stderr 68 | $stdout = StringIO.new 69 | $stderr = StringIO.new 70 | yield 71 | [$stdout.string, $stderr.string] 72 | ensure 73 | $stdout = original_stdout 74 | $stderr = original_stderr 75 | end 76 | end 77 | 78 | # If tests are slow, check this code out https://gist.github.com/palkan/73395cc201a565ecd3ff61aac44ad5ae 79 | # Just don't keep it in the repo because it's unlicensed 80 | 81 | RSpec.configure do |config| 82 | config.include TestResources 83 | config.extend TestResources 84 | config.include OutputHelpers 85 | config.extend OutputHelpers 86 | 87 | # Enable flags like --only-failures and --next-failure 88 | config.example_status_persistence_file_path = ".rspec_status" 89 | 90 | # Disable RSpec exposing methods globally on `Module` and `main` 91 | config.disable_monkey_patching! 92 | 93 | config.fail_fast = false 94 | 95 | config.expect_with :rspec do |c| 96 | c.syntax = :expect 97 | end 98 | 99 | config.mock_with :rspec do |mocks| 100 | mocks.verify_partial_doubles = true 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/code_gen/resource_types/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'securerandom' 4 | 5 | module AbideDevUtils 6 | module Ppt 7 | module CodeGen 8 | # Base class for all code gen objects 9 | class Base 10 | attr_accessor :title, :id 11 | 12 | def initialize 13 | @id = SecureRandom.hex(10) 14 | @supports_value = false 15 | @supports_children = false 16 | end 17 | 18 | def to_s 19 | "#{type} : value: #{@value}; children: #{@children}" 20 | end 21 | 22 | def reference 23 | raise NotImplementedError, "#{type} does not support having a reference" 24 | end 25 | 26 | def type 27 | self.class.to_s 28 | end 29 | 30 | def value 31 | raise NotImplementedError, "#{type} does not support having a value" unless @supports_value 32 | 33 | @value 34 | end 35 | 36 | def get_my(t, named: nil) 37 | if named.nil? 38 | children.each_with_object([]) do |(k, v), arr| 39 | arr << v if k.start_with?("#{t.to_s.capitalize}_") 40 | end 41 | else 42 | children["#{t.to_s.capitalize}_#{named}"] 43 | end 44 | end 45 | 46 | # Creates a new object of the given type and adds it to the current objects children 47 | # if the current object supports children. 48 | # Returns `self`. If a block is given, the new 49 | # object will be yielded before adding to children. 50 | def with_a(t, named: nil) 51 | obj = Object.const_get("AbideDevUtils::Ppt::CodeGen::#{t.to_s.capitalize}").new 52 | obj.title = named unless named.nil? || named.empty? 53 | 54 | yield obj if block_given? 55 | 56 | children["#{t.to_s.capitalize}_#{obj.id}"] = obj 57 | self 58 | end 59 | alias and_a with_a 60 | 61 | def has_a(t, named: nil) 62 | obj = Object.const_get("AbideDevUtils::Ppt::CodeGen::#{t.to_s.capitalize}").new 63 | obj.title = named unless named.nil? || named.empty? 64 | children["#{t.to_s.capitalize}_#{obj.id}"] = obj 65 | obj 66 | end 67 | alias and_has_a has_a 68 | alias that_has_a has_a 69 | 70 | # Sets the explicit value of the current object if the current object has an explicit value. 71 | def that_equals(val) 72 | self.value = val 73 | self 74 | end 75 | alias and_assign_a_value_of that_equals 76 | alias has_a_value_of that_equals 77 | alias that_has_a_value_of that_equals 78 | 79 | private 80 | 81 | def children 82 | raise NotImplementedError, "#{type} does not support children" unless @supports_children 83 | 84 | @children ||= {} 85 | end 86 | 87 | def value=(val) 88 | @value = val if @supports_value 89 | end 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/hiera_data/resource_data/.parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This should be unused, keeping around incase shit breaks and it's needed 4 | require 'set' 5 | 6 | module AbideDevUtils 7 | module Sce 8 | module HieraData 9 | module ResourceData 10 | class Parameters 11 | def initialize(*param_collections) 12 | @param_collections = param_collections 13 | end 14 | 15 | def exist? 16 | !@param_collections.nil? && !@param_collections.empty? 17 | end 18 | 19 | def to_h 20 | @to_h ||= { parameters: @param_collections.map { |x| collection_to_h(x) unless x.nil? || x.empty? } } 21 | end 22 | 23 | def to_puppet_code 24 | parray = to_h[:parameters].each_with_object([]) do |x, arr| 25 | x.each do |_, val| 26 | arr << param_to_code(**val[:display_value]) if val.respond_to?(:key) 27 | end 28 | end 29 | parray.reject { |x| x.nil? || x.empty? }.compact.join("\n") 30 | end 31 | 32 | def to_display_fmt 33 | to_h[:parameters].values.map { |x| x[:display_value] } 34 | end 35 | 36 | private 37 | 38 | def collection_to_h(collection) 39 | return no_params_display if collection == 'no_params' 40 | 41 | collection.each_with_object({}) do |(param, param_val), hsh| 42 | hsh[param] = { 43 | raw_value: param_val, 44 | display_value: param_display(param, param_val) 45 | } 46 | end 47 | end 48 | 49 | def param_display(param, param_val) 50 | { 51 | name: param, 52 | type: ruby_class_to_puppet_type(param_val.class.to_s), 53 | default: param_val 54 | } 55 | end 56 | 57 | def no_params_display 58 | { name: 'No parameters', type: nil, default: nil } 59 | end 60 | 61 | def param_to_code(name: nil, type: nil, default: nil) 62 | return if name.nil? 63 | return " #{name}," if default.nil? 64 | return " #{name} => #{default}," if %w[Boolean Integer Float].include?(type) 65 | return " #{name} => '#{default}'," if type == 'String' 66 | 67 | " #{name} => undef," 68 | end 69 | 70 | def ruby_class_to_puppet_type(class_name) 71 | pup_type = class_name.split('::').last.capitalize 72 | case pup_type 73 | when %r{(Trueclass|Falseclass)} 74 | 'Boolean' 75 | when %r{(String|Pathname)} 76 | 'String' 77 | when %r{(Integer|Fixnum)} 78 | 'Integer' 79 | when %r{(Float|Double)} 80 | 'Float' 81 | when %r{Nilclass} 82 | 'Optional' 83 | else 84 | pup_type 85 | end 86 | end 87 | end 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/jira/client_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'jira-ruby' 4 | require_relative '../prompt' 5 | 6 | module AbideDevUtils 7 | module Jira 8 | class ClientBuilder 9 | def initialize(config, **options) 10 | @options = options 11 | @config = config 12 | end 13 | 14 | def username 15 | find_option_value(:username) 16 | end 17 | 18 | def username=(username) 19 | @options[:username] = username 20 | end 21 | 22 | def password 23 | if find_option_value(:password) 24 | '********' 25 | else 26 | nil 27 | end 28 | end 29 | 30 | def password=(password) 31 | @options[:password] = password 32 | end 33 | 34 | def site 35 | find_option_value(:site) 36 | end 37 | 38 | def site=(site) 39 | @options[:site] = site 40 | end 41 | 42 | def context_path 43 | find_option_value(:context_path, default: '') 44 | end 45 | 46 | def context_path=(context_path) 47 | @options[:context_path] = context_path 48 | end 49 | 50 | def auth_type 51 | find_option_value(:auth_type, default: :basic) 52 | end 53 | 54 | def auth_type=(auth_type) 55 | @options[:auth_type] = auth_type 56 | end 57 | 58 | def http_debug 59 | find_option_value(:http_debug, default: false) 60 | end 61 | 62 | def http_debug=(http_debug) 63 | @options[:http_debug] = http_debug 64 | end 65 | 66 | def build 67 | JIRA::Client.new({ 68 | username: find_option_value(:username, prompt: true), 69 | password: find_option_value(:password, prompt: true), 70 | site: find_option_value(:site, prompt: 'Jira site URL'), 71 | context_path: find_option_value(:context_path, default: ''), 72 | auth_type: find_option_value(:auth_type, default: :basic), 73 | http_debug: find_option_value(:http_debug, default: false), 74 | }) 75 | end 76 | 77 | private 78 | 79 | def find_option_value(key, default: nil, prompt: nil) 80 | if prompt 81 | find_option_value_or_prompt(key, prompt) 82 | else 83 | find_option_value_or_default(key, default) 84 | end 85 | end 86 | 87 | def find_option_val(key) 88 | @options[key] || @config[key] || ENV["JIRA_#{key.to_s.upcase}"] 89 | end 90 | 91 | def find_option_value_or_prompt(key, prompt = 'Enter value') 92 | case key 93 | when /password/i 94 | find_option_val(key) || AbideDevUtils::Prompt.password 95 | when /username/i 96 | find_option_val(key) || AbideDevUtils::Prompt.username 97 | else 98 | find_option_val(key) || AbideDevUtils::Prompt.single_line(prompt) 99 | end 100 | end 101 | 102 | def find_option_value_or_default(key, default) 103 | find_option_val(key) || default 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/validate/strings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../ppt/strings' 4 | require_relative 'strings/puppet_class_validator' 5 | require_relative 'strings/puppet_defined_type_validator' 6 | 7 | module AbideDevUtils 8 | module Sce 9 | module Validate 10 | # Validation objects and methods for Puppet Strings 11 | module Strings 12 | # Convenience method to validate Puppet Strings of current module 13 | def self.validate(**opts) 14 | output = Validator.new(nil, **opts).validate 15 | output.transform_values do |results| 16 | results.select { |r| r[:errors].any? || r[:warnings].any? } 17 | end 18 | end 19 | 20 | # Holds various validation methods for a AbideDevUtils::Ppt::Strings object 21 | class Validator 22 | def initialize(puppet_strings = nil, **opts) 23 | unless puppet_strings.nil? || puppet_strings.is_a?(AbideDevUtils::Ppt::Strings) 24 | raise ArgumentError, 'If puppet_strings is supplied, it must be a AbideDevUtils::Ppt::Strings object' 25 | end 26 | 27 | puppet_strings = AbideDevUtils::Ppt::Strings.new(**opts) if puppet_strings.nil? 28 | @puppet_strings = puppet_strings 29 | end 30 | 31 | # Associate validators with each Puppet Strings object and calls #validate on each 32 | # @return [Hash] Hash of validation results 33 | def validate 34 | AbideDevUtils::Ppt::Strings::REGISTRY_TYPES.each_with_object({}) do |rtype, hsh| 35 | next unless rtype.to_s.start_with?('puppet_') && @puppet_strings.respond_to?(rtype) 36 | 37 | hsh[rtype] = @puppet_strings.send(rtype).map do |item| 38 | item.validator = validator_for(item) 39 | item.validate 40 | validation_output(item) 41 | end 42 | end 43 | end 44 | 45 | private 46 | 47 | # Returns the appropriate validator for a given Puppet Strings object 48 | def validator_for(item) 49 | case item.type 50 | when :puppet_class 51 | PuppetClassValidator.new(item) 52 | when :puppet_defined_type 53 | PuppetDefinedTypeValidator.new(item) 54 | else 55 | BaseValidator.new(item) 56 | end 57 | end 58 | 59 | def validation_output(item) 60 | { 61 | name: item.name, 62 | file: item.file, 63 | line: item.line, 64 | errors: item.errors, 65 | warnings: item.warnings 66 | } 67 | end 68 | 69 | # Validate Puppet Class strings hashes. 70 | # @return [Hash] Hash of class names and errors 71 | def validate_classes! 72 | @puppet_strings.puppet_classes.map! do |klass| 73 | klass.validator = PuppetClassValidator.new(klass) 74 | klass.validate 75 | klass 76 | end 77 | end 78 | end 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/dot_number_comparable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbideDevUtils 4 | # Module provides comparison methods for "dot numbers", numbers that 5 | # take the form of "1.1.1" as found in CIS benchmarks. Classes that 6 | # include this module must implement a method "number" that returns 7 | # their dot number representation. 8 | module DotNumberComparable 9 | include ::Comparable 10 | 11 | def <=>(other) 12 | return 0 if number_eq(number, other.number) 13 | return 1 if number_gt(number, other.number) 14 | return -1 if number_lt(number, other.number) 15 | end 16 | 17 | def number_eq(this_num, other_num) 18 | this_num == other_num 19 | end 20 | 21 | def number_parent_of?(this_num, other_num) 22 | return false if number_eq(this_num, other_num) 23 | 24 | # We split the numbers into parts and compare the resulting arrays 25 | num1_parts = this_num.to_s.split('.') 26 | num2_parts = other_num.to_s.split('.') 27 | # For this_num to be a parent of other_num, the number of parts in 28 | # this_num must be less than the number of parts in other_num. 29 | # Additionally, each part of this_num must be equal to the parts of 30 | # other_num at the same index. 31 | # Example: this_num = '1.2.3' and other_num = '1.2.3.4' 32 | # In this case, num1_parts = ['1', '2', '3'] and num2_parts = ['1', '2', '3', '4'] 33 | # So, this_num is a parent of other_num because at indexes 0, 1, and 2 34 | # of num1_parts and num2_parts, the parts are equal. 35 | num1_parts.length < num2_parts.length && 36 | num2_parts[0..(num1_parts.length - 1)] == num1_parts 37 | end 38 | 39 | def number_child_of?(this_num, other_num) 40 | number_parent_of?(other_num, this_num) 41 | end 42 | 43 | def number_gt(this_num, other_num) 44 | return false if number_eq(this_num, other_num) 45 | return true if number_parent_of?(this_num, other_num) 46 | 47 | num1_parts = this_num.to_s.split('.') 48 | num2_parts = other_num.to_s.split('.') 49 | num1_parts.zip(num2_parts).each do |num1_part, num2_part| 50 | next if num1_part == num2_part # we skip past equal parts 51 | 52 | # If num1_part is nil that means that we've had equal numbers so far. 53 | # Therfore, this_num is greater than other num because of the 54 | # hierarchical nature of the numbers. 55 | # Example: this_num = '1.2' and other_num = '1.2.3' 56 | # In this case, num1_part is nil and num2_part is '3' 57 | # So, this_num is greater than other_num 58 | return true if num1_part.nil? 59 | # If num2_part is nil that means that we've had equal numbers so far. 60 | # Therfore, this_num is less than other num because of the 61 | # hierarchical nature of the numbers. 62 | # Example: this_num = '1.2.3' and other_num = '1.2' 63 | # In this case, num1_part is '3' and num2_part is nil 64 | # So, this_num is less than other_num 65 | return false if num2_part.nil? 66 | 67 | return num1_part.to_i > num2_part.to_i 68 | end 69 | end 70 | 71 | def number_lt(this_num, other_num) 72 | number_gt(other_num, this_num) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/puppet_strings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'puppet-strings' 4 | require 'puppet-strings/yard' 5 | 6 | module AbideDevUtils 7 | # Puppet Strings reference object 8 | class PuppetStrings 9 | attr_reader :search_patterns 10 | 11 | def initialize(search_patterns: nil, opts: {}) 12 | check_yardoc_dir 13 | @search_patterns = search_patterns || ::PuppetStrings::DEFAULT_SEARCH_PATTERNS 14 | @debug = opts[:debug] 15 | @quiet = opts[:quiet] 16 | PuppetStrings::Yard.setup! 17 | YARD::CLI::Yardoc.run(*yard_args(@search_patterns, debug: @debug, quiet: @quiet)) 18 | end 19 | 20 | def debug? 21 | !!@debug 22 | end 23 | 24 | def quiet? 25 | !!@quiet 26 | end 27 | 28 | def find_resource(resource_name) 29 | to_h.each do |_, resources| 30 | res = resources.find { |r| r[:name] == resource_name.to_sym } 31 | return res if res 32 | end 33 | end 34 | 35 | def puppet_classes 36 | @puppet_classes ||= all_to_h YARD::Registry.all(:puppet_class) 37 | end 38 | 39 | def data_types 40 | @data_types ||= all_to_h YARD::Registry.all(:puppet_data_types) 41 | end 42 | 43 | def data_type_aliases 44 | @data_type_aliases ||= all_to_h YARD::Registry.all(:puppet_data_type_alias) 45 | end 46 | 47 | def defined_types 48 | @defined_types ||= all_to_h YARD::Registry.all(:puppet_defined_type) 49 | end 50 | 51 | def resource_types 52 | @resource_types ||= all_to_h YARD::Registry.all(:puppet_type) 53 | end 54 | 55 | def providers 56 | @providers ||= all_to_h YARD::Registry.all(:puppet_provider) 57 | end 58 | 59 | def puppet_functions 60 | @puppet_functions ||= all_to_h YARD::Registry.all(:puppet_function) 61 | end 62 | 63 | def puppet_tasks 64 | @puppet_tasks ||= all_to_h YARD::Registry.all(:puppet_task) 65 | end 66 | 67 | def puppet_plans 68 | @puppet_plans ||= all_to_h YARD::Registry.all(:puppet_plan) 69 | end 70 | 71 | def to_h 72 | { 73 | puppet_classes: puppet_classes, 74 | data_types: data_types, 75 | data_type_aliases: data_type_aliases, 76 | defined_types: defined_types, 77 | resource_types: resource_types, 78 | providers: providers, 79 | puppet_functions: puppet_functions, 80 | puppet_tasks: puppet_tasks, 81 | puppet_plans: puppet_plans, 82 | } 83 | end 84 | 85 | private 86 | 87 | def check_yardoc_dir 88 | yardoc_dir = File.expand_path('./.yardoc') 89 | return unless Dir.exist?(yardoc_dir) && !File.writable?(yardoc_dir) 90 | 91 | raise "yardoc directory permissions error. Ensure #{yardoc_dir} is writable by current user." 92 | end 93 | 94 | def all_to_h(objects) 95 | objects.sort_by(&:name).map(&:to_hash) 96 | end 97 | 98 | def yard_args(patterns, debug: false, quiet: false) 99 | args = ['doc', '--no-progress', '-n'] 100 | args << '--debug' if debug && !quiet 101 | args << '--backtrace' if debug && !quiet 102 | args << '-q' if quiet 103 | args << '--no-stats' if quiet 104 | args += patterns 105 | args 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | require: 3 | - rubocop/cop/internal_affairs 4 | - rubocop-performance 5 | - rubocop-rspec 6 | 7 | AllCops: 8 | NewCops: enable 9 | Exclude: 10 | - 'vendor/**/*' 11 | - 'spec/fixtures/**/*' 12 | - 'tmp/**/*' 13 | - '.git/**/*' 14 | - 'bin/*' 15 | TargetRubyVersion: 2.7 16 | SuggestExtensions: false 17 | 18 | Naming/PredicateName: 19 | # Method define macros for dynamically generated method. 20 | MethodDefinitionMacros: 21 | - define_method 22 | - define_singleton_method 23 | - def_node_matcher 24 | - def_node_search 25 | 26 | Style/AccessorGrouping: 27 | Exclude: 28 | - lib/rubocop/formatter/base_formatter.rb 29 | - lib/rubocop/cop/offense.rb 30 | 31 | Style/FormatStringToken: 32 | # Because we parse a lot of source codes from strings. Percent arrays 33 | # look like unannotated format string tokens to this cop. 34 | Exclude: 35 | - spec/**/* 36 | 37 | Style/IpAddresses: 38 | # The test for this cop includes strings that would cause offenses 39 | Exclude: 40 | - spec/rubocop/cop/style/ip_addresses_spec.rb 41 | 42 | Style/RegexpLiteral: 43 | Enabled: false 44 | 45 | Layout/EndOfLine: 46 | EnforcedStyle: lf 47 | 48 | Layout/ClassStructure: 49 | Enabled: true 50 | Categories: 51 | module_inclusion: 52 | - include 53 | - prepend 54 | - extend 55 | ExpectedOrder: 56 | - module_inclusion 57 | - constants 58 | - public_class_methods 59 | - initializer 60 | - instance_methods 61 | - protected_methods 62 | - private_methods 63 | 64 | Layout/TrailingWhitespace: 65 | AllowInHeredoc: false 66 | 67 | Lint/AmbiguousBlockAssociation: 68 | Exclude: 69 | - 'spec/**/*.rb' 70 | 71 | Layout/HashAlignment: 72 | EnforcedHashRocketStyle: 73 | - key 74 | - table 75 | EnforcedColonStyle: 76 | - key 77 | - table 78 | 79 | Layout/LineLength: 80 | Max: 120 81 | IgnoredPatterns: 82 | - !ruby/regexp /\A +(it|describe|context|shared_examples|include_examples|it_behaves_like) ["']/ 83 | 84 | Lint/InterpolationCheck: 85 | Exclude: 86 | - 'spec/**/*.rb' 87 | 88 | Lint/UselessAccessModifier: 89 | MethodCreatingMethods: 90 | - 'def_matcher' 91 | - 'def_node_matcher' 92 | 93 | Lint/BooleanSymbol: 94 | Enabled: false 95 | 96 | Metrics/AbcSize: 97 | Enabled: false 98 | 99 | Metrics/MethodLength: 100 | Enabled: false 101 | 102 | Metrics/BlockLength: 103 | Exclude: 104 | - 'Rakefile' 105 | - '**/*.rake' 106 | - 'spec/**/*.rb' 107 | - '**/*.gemspec' 108 | 109 | Metrics/ClassLength: 110 | Exclude: 111 | - lib/rubocop/config_obsoletion.rb 112 | 113 | Metrics/ModuleLength: 114 | Exclude: 115 | - 'spec/**/*.rb' 116 | 117 | RSpec/FilePath: 118 | Exclude: 119 | - spec/rubocop/formatter/junit_formatter_spec.rb 120 | 121 | RSpec/PredicateMatcher: 122 | EnforcedStyle: explicit 123 | 124 | RSpec/MessageSpies: 125 | EnforcedStyle: receive 126 | 127 | RSpec/NestedGroups: 128 | Max: 7 129 | 130 | RSpec/MultipleMemoizedHelpers: 131 | Enabled: false 132 | 133 | Performance/CollectionLiteralInLoop: 134 | Exclude: 135 | - 'Rakefile' 136 | - 'spec/**/*.rb' 137 | 138 | RSpec/StubbedMock: 139 | Enabled: false 140 | 141 | Convention/ExplicitBlockArgument: 142 | Enabled: false 143 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/hiera_data/mapping_data/map_data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbideDevUtils 4 | module Sce 5 | module HieraData 6 | module MappingData 7 | # Represents a single map data file 8 | class MapData 9 | def initialize(data) 10 | @raw_data = data 11 | end 12 | 13 | def method_missing(meth, *args, &block) 14 | if data.respond_to?(meth) 15 | data.send(meth, *args, &block) 16 | else 17 | super 18 | end 19 | end 20 | 21 | def respond_to_missing?(meth, include_private = false) 22 | data.respond_to?(meth) || super 23 | end 24 | 25 | def find(identifier, level: nil, profile: nil) 26 | levels.each do |lvl| 27 | next unless level.nil? || lvl != level 28 | 29 | data[lvl].each do |prof, prof_data| 30 | if prof_data.respond_to?(:keys) 31 | next unless profile.nil? || prof != profile 32 | 33 | return prof_data[identifier] if prof_data.key?(identifier) 34 | elsif prof == identifier 35 | return prof_data 36 | end 37 | end 38 | end 39 | end 40 | 41 | def get(identifier, level: nil, profile: nil) 42 | raise "Invalid level: #{level}" unless profile.nil? || levels.include?(level) 43 | raise "Invalid profile: #{profile}" unless profile.nil? || profiles.include?(profile) 44 | return find(identifier, level: level, profile: profile) if level.nil? || profile.nil? 45 | 46 | begin 47 | data.dig(level, profile, identifier) 48 | rescue TypeError 49 | data.dig(level, identifier) 50 | end 51 | end 52 | 53 | def module_name 54 | top_key_parts[0] 55 | end 56 | 57 | def framework 58 | top_key_parts[2] 59 | end 60 | 61 | def type 62 | top_key_parts[3] 63 | end 64 | 65 | def benchmark 66 | @raw_data[top_key]['benchmark'] 67 | end 68 | 69 | def levels_and_profiles 70 | @levels_and_profiles ||= find_levels_and_profiles 71 | end 72 | 73 | def levels 74 | levels_and_profiles[0] 75 | end 76 | 77 | def profiles 78 | levels_and_profiles[1] 79 | end 80 | 81 | def top_key 82 | @top_key ||= @raw_data.keys.first 83 | end 84 | 85 | private 86 | 87 | def top_key_parts 88 | @top_key_parts ||= top_key.split('::') 89 | end 90 | 91 | def data 92 | @data ||= @raw_data[top_key].reject { |k, _| k == 'benchmark' } 93 | end 94 | 95 | def find_levels_and_profiles 96 | lvls = [] 97 | profs = [] 98 | data.each do |lvl, prof_hash| 99 | lvls << lvl 100 | prof_hash.each do |prof, prof_data| 101 | profs << prof if prof_data.respond_to?(:keys) 102 | end 103 | end 104 | [lvls.flatten.compact.uniq, profs.flatten.compact.uniq] 105 | end 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/markdown.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tempfile' 4 | require 'fileutils' 5 | 6 | module AbideDevUtils 7 | # Formats text for output in markdown 8 | class Markdown 9 | def initialize(file, with_toc: true) 10 | @file = file 11 | @with_toc = with_toc 12 | @toc = ["## Table of Contents\n"] 13 | @body = [] 14 | @title = nil 15 | end 16 | 17 | def to_markdown 18 | toc = @toc.join("\n") 19 | body = @body.join("\n") 20 | "#{@title}\n#{toc}\n\n#{body}".encode(universal_newline: true) 21 | end 22 | alias to_s to_markdown 23 | 24 | def to_file 25 | this_markdown = to_markdown 26 | Tempfile.create('markdown') do |f| 27 | f.write(this_markdown) 28 | check_file_content(f.path, this_markdown) 29 | FileUtils.mv(f.path, @file) 30 | end 31 | end 32 | 33 | def method_missing(name, *args, **kwargs, &block) 34 | if name.to_s.start_with?('add_') 35 | add(name.to_s.sub('add_', '').to_sym, *args, **kwargs, &block) 36 | else 37 | super 38 | end 39 | end 40 | 41 | def respond_to_missing?(name, include_private = false) 42 | name.to_s.start_with?('add_') || super 43 | end 44 | 45 | def title(text) 46 | "# #{text}\n" 47 | end 48 | 49 | def h1(text) 50 | "## #{text}\n" 51 | end 52 | 53 | def h2(text) 54 | "### #{text}\n" 55 | end 56 | 57 | def h3(text) 58 | "#### #{text}\n" 59 | end 60 | 61 | def paragraph(text) 62 | "#{text}\n" 63 | end 64 | 65 | def ul(text, indent: 0) 66 | indented_text = [] 67 | indent.times { indented_text << ' ' } if indent.positive? 68 | 69 | indented_text << "* #{text}" 70 | indented_text.join 71 | end 72 | 73 | def bold(text) 74 | "**#{text}**" 75 | end 76 | 77 | def italic(text) 78 | "*#{text}*" 79 | end 80 | 81 | def link(text, url, anchor: false) 82 | url = anchor(url) if anchor 83 | "[#{text}](#{url.downcase})" 84 | end 85 | 86 | def code(text) 87 | "\`#{text}\`" 88 | end 89 | 90 | def code_block(text, language: nil) 91 | language.nil? ? "```\n#{text}\n```" : "```#{language}\n#{text}\n```" 92 | end 93 | 94 | def anchor(text) 95 | "##{text.downcase.gsub(%r{\s|_}, '-').tr('.,\'"()', '')}" 96 | end 97 | 98 | private 99 | 100 | def check_file_content(file, content) 101 | raise "File #{file} not found! Not saving to #{@file}" unless File.exist?(file) 102 | raise "File #{file} is empty! Not saving to #{@file}" if File.zero?(file) 103 | return if File.read(file).include?(content) 104 | 105 | raise "File #{file} does not contain correct content! Not saving to #{@file}" 106 | end 107 | 108 | def add(type, text, *args, **kwargs) 109 | @toc << ul(link(text, text, anchor: true), indent: 0) if @with_toc && type == :h1 110 | 111 | case type.to_sym 112 | when :title 113 | @title = title(text) 114 | when :ul 115 | @body << ul(text, indent: kwargs.fetch(:indent, 0)) 116 | when :link 117 | @body << link(text, args.first, anchor: kwargs.fetch(:anchor, false)) 118 | when :code_block 119 | @body << code_block(text, language: kwargs.fetch(:language, nil)) 120 | else 121 | @body << send(type, text) 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/code_introspection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'puppet_pal' 4 | require_relative 'code_gen/data_types' 5 | 6 | module AbideDevUtils 7 | module Ppt 8 | module CodeIntrospection 9 | class Manifest 10 | attr_reader :manifest_file 11 | 12 | def initialize(manifest_file) 13 | @compiler = Puppet::Pal::Compiler.new(nil) 14 | @manifest_file = File.expand_path(manifest_file) 15 | raise ArgumentError, "File #{@manifest_file} is not a file" unless File.file?(@manifest_file) 16 | end 17 | 18 | def ast 19 | @ast ||= non_validating_parse_file(manifest_file) 20 | end 21 | 22 | def declaration 23 | @declaration ||= Declaration.new(ast) 24 | end 25 | 26 | private 27 | 28 | # This method gets around the normal validation performed by the regular 29 | # Puppet::Pal::Compiler#parse_file method. This is necessary because, with 30 | # validation enabled, the parser will raise errors during parsing if the 31 | # file contains any calls to Facter. This is due to facter being disallowed 32 | # in Puppet when evaluating the code in a scripting context instead of catalog 33 | # compilation, which is what we are doing here. 34 | def non_validating_parse_file(file) 35 | @compiler.send(:internal_evaluator).parser.parse_file(file)&.model 36 | end 37 | end 38 | 39 | class Declaration 40 | include AbideDevUtils::Ppt::CodeGen::DataTypes 41 | attr_reader :ast 42 | 43 | def initialize(ast) 44 | @ast = ast.definitions.first 45 | end 46 | 47 | def parameters? 48 | ast.respond_to? :parameters 49 | end 50 | 51 | def parameters 52 | return unless parameters? 53 | 54 | @parameters ||= ast.parameters.map { |p| Parameter.new(p) } 55 | end 56 | end 57 | 58 | class Parameter 59 | include AbideDevUtils::Ppt::CodeGen::DataTypes 60 | attr_reader :ast 61 | 62 | def initialize(param_ast) 63 | @ast = param_ast 64 | end 65 | 66 | def to_a(raw: false) 67 | [type_expr(raw: raw), name(raw: raw), value(raw: raw)] 68 | end 69 | 70 | def to_h(raw: false) 71 | { 72 | type_expr: type_expr(raw: raw), 73 | name: name(raw: raw), 74 | value: value(raw: raw), 75 | } 76 | end 77 | 78 | def to_s(raw: false) 79 | stra = [type_expr(raw: raw), name(raw: raw)] 80 | stra << '=' if value? && !raw 81 | stra << value(raw: raw) 82 | stra.compact.join(' ') 83 | end 84 | 85 | def name(raw: false) 86 | return ast.name if raw 87 | 88 | "$#{ast.name}" 89 | end 90 | 91 | def value? 92 | ast.respond_to? :value 93 | end 94 | 95 | def value(raw: false) 96 | return unless value? 97 | return ast.value if raw 98 | 99 | display_value(ast) 100 | end 101 | 102 | def type_expr? 103 | ast.respond_to? :type_expr 104 | end 105 | 106 | def type_expr(raw: false) 107 | return unless type_expr? 108 | return ast.type_expr if raw 109 | 110 | display_type_expr(ast) 111 | end 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/xccdf/diff.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'parser' 4 | 5 | module AbideDevUtils 6 | module XCCDF 7 | # Contains methods and classes used to diff XCCDF-derived objects. 8 | module Diff 9 | def self.benchmark_diff(xml1, xml2, opts = {}) 10 | bd = BenchmarkDiff.new(xml1, xml2, opts) 11 | if opts[:raw] 12 | return bd.diff_rules_raw if opts[:rules_only] 13 | 14 | bd.diff_raw 15 | else 16 | return bd.diff_rules if opts[:rules_only] 17 | 18 | bd.diff 19 | end 20 | end 21 | 22 | # Class for benchmark diffs 23 | class BenchmarkDiff 24 | attr_reader :this, :other, :opts 25 | 26 | # @param xml1 [String] path to the first benchmark XCCDF xml file 27 | # @param xml2 [String] path to the second benchmark XCCDF xml file 28 | # @param opts [Hash] options hash 29 | def initialize(xml1, xml2, opts = {}) 30 | @this = new_benchmark(xml1) 31 | @other = new_benchmark(xml2) 32 | @opts = opts 33 | end 34 | 35 | def diff_raw 36 | @diff_raw ||= @this.diff(@other) 37 | end 38 | 39 | def diff 40 | warn 'Full benchmark diff is not yet implemented, return rules diff for now' 41 | diff_rules 42 | end 43 | 44 | def diff_rules_raw 45 | @diff_rules_raw ||= @this.diff_only_rules(@other) 46 | end 47 | 48 | def diff_rules 49 | return diff_rules_raw if opts[:raw] 50 | return [] if diff_rules_raw.all? { |r| r.type == :equal } 51 | 52 | diff_hash = { 53 | from: @this.to_s, 54 | to: @other.to_s, 55 | rules: {} 56 | } 57 | diff_rules_raw.each do |rule| 58 | diff_hash[:rules][rule.type] ||= [] 59 | case rule.type 60 | when :added 61 | diff_hash[:rules][rule.type] << { number: rule.new_value.number.to_s, title: rule.new_value.title.to_s } 62 | when :removed 63 | diff_hash[:rules][rule.type] << { number: rule.old_value.number.to_s, title: rule.old_value.title.to_s } 64 | else 65 | rd_hash = {} 66 | rd_hash[:from] = "#{rule.old_value&.number} #{rule.old_value&.title}" if rule.old_value 67 | rd_hash[:to] = "#{rule.new_value&.number} #{rule.new_value&.title}" if rule.new_value 68 | changes = rule.details.transform_values { |v| v.is_a?(Array) ? v.map(&:to_s) : v.to_s } 69 | if opts[:ignore_changed_properties] 70 | changes.delete_if { |k, _| opts[:ignore_changed_properties].include?(k.to_s) } 71 | next if changes.empty? # Skip entirely if all changed filtered out 72 | end 73 | rd_hash[:changes] = changes unless changes.empty? 74 | diff_hash[:rules][rule.type] << rd_hash 75 | end 76 | end 77 | unless opts[:no_stats] 78 | stats_hash = {} 79 | diff_hash[:rules].each do |type, rules| 80 | stats_hash[type] = rules.size 81 | end 82 | diff_hash[:stats] = stats_hash unless stats_hash.empty? 83 | end 84 | diff_hash 85 | end 86 | 87 | private 88 | 89 | # Returns a Benchmark object from a XCCDF xml file path 90 | # @param xml [String] path to a XCCDF xml file 91 | def new_benchmark(xml) 92 | AbideDevUtils::XCCDF::Parser.parse(xml) 93 | end 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/hiera_data/resource_data/.resource.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This should be unused, keeping around incase shit breaks and it's needed 4 | require 'set' 5 | require 'abide_dev_utils/errors' 6 | require 'abide_dev_utils/sce/hiera_data/resource_data/control' 7 | require 'abide_dev_utils/sce/hiera_data/resource_data/parameters' 8 | 9 | module AbideDevUtils 10 | module Sce 11 | module HieraData 12 | module ResourceData 13 | # Represents a resource data resource statement 14 | class Resource 15 | attr_reader :title, :type 16 | 17 | def initialize(title, data, framework, mapper) 18 | @title = title 19 | @data = data 20 | @type = data['type'] 21 | @framework = framework 22 | @mapper = mapper 23 | end 24 | 25 | def controls 26 | @controls ||= load_controls 27 | end 28 | 29 | def sce_options 30 | @sce_options ||= Parameters.new(data['sce_options']) 31 | end 32 | 33 | def sce_protected 34 | @sce_protected ||= Parameters.new(data['sce_protected']) 35 | end 36 | 37 | def to_stubbed_h 38 | { 39 | title: title, 40 | type: type, 41 | sce_options: sce_options.to_h, 42 | sce_protected: sce_protected.to_h, 43 | reference: to_reference 44 | } 45 | end 46 | 47 | def to_reference 48 | "#{type.split('::').map(&:capitalize).join('::')}['#{title}']" 49 | end 50 | 51 | def to_puppet_code 52 | parray = controls.map { |x| x.parameters.to_puppet_code if x.parameters.exist? }.flatten.compact.uniq 53 | return "#{type} { '#{title}': }" if parray.empty? || parray.all?(&:empty?) || parray.all?("\n") 54 | 55 | # if title == 'sce_linux::utils::packages::linux::auditd::time_change' 56 | # require 'pry' 57 | # binding.pry 58 | # end 59 | <<~EOPC 60 | #{type} { '#{title}': 61 | #{parray.join("\n")} 62 | } 63 | EOPC 64 | end 65 | 66 | private 67 | 68 | attr_reader :data, :framework, :mapper 69 | 70 | def load_controls 71 | if data['controls'].respond_to?(:keys) 72 | load_hash_controls(data['controls'], framework, mapper) 73 | elsif data['controls'].respond_to?(:each_with_index) 74 | load_array_controls(data['controls'], framework, mapper) 75 | else 76 | raise "Control type is invalid. Type: #{data['controls'].class}" 77 | end 78 | end 79 | 80 | def load_hash_controls(ctrls, framework, mapper) 81 | ctrls.each_with_object([]) do |(name, data), arr| 82 | ctrl = Control.new(name, data, to_stubbed_h, framework, mapper) 83 | arr << ctrl 84 | rescue AbideDevUtils::Errors::ControlIdFrameworkMismatchError, 85 | AbideDevUtils::Errors::NoMappingDataForControlError 86 | next 87 | end 88 | end 89 | 90 | def load_array_controls(ctrls, framework, mapper) 91 | ctrls.each_with_object([]) do |c, arr| 92 | ctrl = Control.new(c, 'no_params', to_stubbed_h, framework, mapper) 93 | arr << ctrl 94 | rescue AbideDevUtils::Errors::ControlIdFrameworkMismatchError, 95 | AbideDevUtils::Errors::NoMappingDataForControlError 96 | next 97 | end 98 | end 99 | end 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/jira/issue_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../errors/jira' 4 | 5 | module AbideDevUtils 6 | module Jira 7 | class IssueBuilder 8 | CUSTOM_FIELDS = { 9 | 'epic_link' => 'customfield_10014', 10 | 'epic_name' => 'customfield_10011', 11 | }.freeze 12 | 13 | FIELD_DEFAULTS = { 14 | 'issuetype' => 'Task', 15 | 'priority' => 'Medium', 16 | 'labels' => ['abide_dev_utils'], 17 | }.freeze 18 | 19 | REQUIRED_FIELDS = %w[project summary].freeze 20 | 21 | def initialize(client, finder) 22 | @client = client 23 | @finder = finder 24 | end 25 | 26 | def can_create?(type) 27 | respond_to?("create_#{type}".to_sym, true) 28 | end 29 | 30 | def create(type, **fields) 31 | type = type.to_s.downcase.to_sym 32 | raise ArgumentError, "Invalid type \"#{type}\"; no method \"create_#{type}\"" unless can_create?(type) 33 | 34 | fields = process_fields(fields) 35 | send("create_#{type}".to_sym, **fields) 36 | end 37 | 38 | private 39 | 40 | attr_reader :client 41 | 42 | def create_issue(**fields) 43 | iss = client.Issue.build 44 | iss.save({ 'fields' => fields }) 45 | iss 46 | rescue StandardError => e 47 | raise AbideDevUtils::Errors::Jira::CreateIssueError, e 48 | end 49 | 50 | def create_subtask(**fields) 51 | fields['parent'] = find_if_not_type(:issue, client.Issue.target_class, fields['parent']) 52 | issue_fields = fields['parent'].attrs['fields'] 53 | fields['project'] = issue_fields['project'] 54 | fields['issuetype'] = find_if_not_type(:issuetype, client.Issuetype.target_class, 'Sub-task') 55 | fields['priority'] = issue_fields['priority'] 56 | iss = client.Issue.build 57 | iss.save({ 'fields' => fields }) 58 | iss 59 | rescue StandardError => e 60 | raise AbideDevUtils::Errors::Jira::CreateSubtaskError, e 61 | end 62 | 63 | def process_fields(fields) 64 | fields = fields.dup 65 | normalize_field_keys!(fields) 66 | validate_required_fields!(fields) 67 | normalize_field_values(fields) 68 | end 69 | 70 | def validate_required_fields!(fields) 71 | missing = REQUIRED_FIELDS.reject { |f| fields.key?(f) } 72 | raise "Missing required field(s) \"#{missing}\"; present fields: \"#{fields.keys}\"" unless missing.empty? 73 | end 74 | 75 | def normalize_field_keys!(fields) 76 | fields.transform_keys! { |k| k.to_s.downcase } 77 | fields.transform_keys! { |k| CUSTOM_FIELDS[k] || k } 78 | end 79 | 80 | def normalize_field_values(fields) 81 | fields = FIELD_DEFAULTS.merge(fields).map do |k, v| 82 | v = case k 83 | when 'labels' 84 | v.is_a?(Array) ? v : [v] 85 | when 'issuetype' 86 | find_if_not_type(:issuetype, client.Issuetype.target_class, v) 87 | when 'parent' 88 | find_if_not_type(:issue, client.Issue.target_class, v) 89 | when 'priority' 90 | find_if_not_type(:priority, client.Priority.target_class, v) 91 | when 'epic_link', CUSTOM_FIELDS['epic_link'] 92 | find_if_not_type(:issue, client.Issue.target_class, v)&.key || v 93 | when 'project' 94 | find_if_not_type(:project, client.Project.target_class, v) 95 | else 96 | v 97 | end 98 | [k, v] 99 | end 100 | fields.to_h 101 | end 102 | 103 | def find_if_not_type(typesym, typeklass, obj) 104 | return obj if obj.is_a?(typeklass) 105 | 106 | @finder.send(typesym, obj) 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/cli/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/cli/abstract' 4 | 5 | module Abide 6 | module CLI 7 | class TestCommand < AbideCommand 8 | CMD_NAME = 'test' 9 | CMD_SHORT = 'Run test suites against a Puppet module' 10 | CMD_LONG = 'Run various test suites against a Puppet module. Requires PDK to be installed.' 11 | CMD_PDK = 'command -v pdk' 12 | CMD_LIT_BASE = 'bundle exec rake' 13 | 14 | def initialize 15 | super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: false, deprecated: true) 16 | argument_desc(SUITE: 'Test suite to run [all, validate, unit, limus]') 17 | options.on('-p', '--puppet-version', 'Set Puppet version for unit tests. Takes SemVer string') do |p| 18 | @data[:puppet] = p 19 | end 20 | options.on('-e', '--pe-version', 'Set PE version for unit tests. Takes SemVer String') { |e| @data[:pe] = e } 21 | options.on('-n', '--no-teardown', 'Do not tear down Litmus machines after tests') do |_| 22 | @data[:no_teardown] = true 23 | end 24 | options.on('-c [puppet[67]]', '--collection [puppet[67]]', 'Puppet collection to use with litmus tests') do |c| 25 | @data[:collection] = c 26 | end 27 | options.on('-l [LIST]', '--provision-list [LIST]', 'Set the provision list for Litmus') do |l| 28 | @data[:provision_list] = l 29 | end 30 | options.on('-M [PATH]', '--module-dir [PATH]', 31 | 'Set a different directory as the module dir (defaults to current dir)') do |m| 32 | @data[:module_dir] = m 33 | end 34 | # Declare and setup commands 35 | @validate = ['validate', '--parallel'] 36 | @unit = ['test', 'unit', '--parallel'] 37 | # Add unit args if they exist 38 | @unit << "--puppet-version #{@data[:puppet]}" unless @data[:puppet].nil? && !@data[:pe].nil? 39 | @unit << "--pe-version #{@data[:pe]}" unless @data[:pe].nil? 40 | # Get litmus args and supply defaults if necessary 41 | litmus_pl = @data[:provision_list].nil? ? 'default' : @data[:provision_list] 42 | litmus_co = @data[:collection].nil? ? 'puppet6' : @data[:collection] 43 | # Now we craft the litmus commands 44 | @litmus_pr = [CMD_LIT_BASE, "'litmus:provision_list[#{litmus_pl}]'"] 45 | @litmus_ia = [CMD_LIT_BASE, "'litmus:install_agent[#{litmus_co}]'"] 46 | @litmus_im = [CMD_LIT_BASE, "'litmus:install_module'"] 47 | @litmus_ap = [CMD_LIT_BASE, "'litmus:acceptance:parallel'"] 48 | @litmus_td = [CMD_LIT_BASE, "'litmus:tear_down'"] 49 | end 50 | 51 | def execute(suite) 52 | validate_env_and_opts 53 | case suite.downcase 54 | when /^a[A-Za-z]*/ 55 | run_command(@validate) 56 | run_command(@unit) 57 | run_litmus 58 | when /^v[A-Za-z]*/ 59 | run_command(@validate) 60 | when /^u[A-Za-z]*/ 61 | run_command(@unit) 62 | when /^l[A-Za-z]*/ 63 | run_litmus 64 | else 65 | Abide::CLI::OUTPUT.simple("Suite #{suite} in invalid!") 66 | Abide::CLI::OUTPUT.simple('Valid options for TEST are [a]ll, [v]alidate, [u]nit, [l]itmus') 67 | end 68 | end 69 | 70 | private 71 | 72 | def validate_env_and_opts 73 | Abide::CLI::VALIDATE.directory(@data[:module_dir]) unless @data[:module_dir].nil? 74 | Abide::CLI::VALIDATE.not_empty(`#{CMD_PDK}`, 'PDK is required for running test suites!') 75 | end 76 | 77 | def run_litmus 78 | run_command(@litmus_pr) 79 | run_command(@litmus_ia) 80 | run_command(@litmus_im) 81 | run_command(@litmus_ap) 82 | run_command(@litmus_td) unless @data[:no_teardown] 83 | end 84 | 85 | def run_command(*args) 86 | arg_str = args.join(' ') 87 | if @data[:module_dir] 88 | `cd #{@data[:module_dir]} && $(#{CMD_PDK}) #{arg_str}` 89 | else 90 | `$(#{CMD_PDK}) #{arg_str}` 91 | end 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/validate/strings/puppet_class_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'base_validator' 4 | require_relative '../../../validate' 5 | 6 | module AbideDevUtils 7 | module Sce 8 | module Validate 9 | module Strings 10 | # Validates a Puppet Class from a Puppet Strings hash 11 | class PuppetClassValidator < BaseValidator 12 | def validate_puppet_class 13 | check_text_or_summary 14 | check_params 15 | end 16 | 17 | # @return [Hash] Hash of basic class data to be used in findings 18 | def finding_data(**data) 19 | data 20 | end 21 | 22 | private 23 | 24 | # Checks if the class has a description or summary 25 | def check_text_or_summary 26 | valid_desc = AbideDevUtils::Validate.populated_string?(docstring) 27 | valid_summary = AbideDevUtils::Validate.populated_string?(find_tag_name('summary')&.text) 28 | return if valid_desc || valid_summary 29 | 30 | new_finding( 31 | :error, 32 | :no_description_or_summary, 33 | finding_data(valid_description: valid_desc, valid_summary: valid_summary) 34 | ) 35 | end 36 | 37 | # Checks if the class has parameters and if they are documented 38 | def check_params 39 | return if parameters.nil? || parameters.empty? # No params 40 | 41 | param_tags = select_tag_name('param') 42 | if param_tags.empty? 43 | new_finding(:error, :no_parameter_documentation, finding_data(class_parameters: parameters)) 44 | return 45 | end 46 | 47 | parameters.each do |param| 48 | param_name, def_val = param 49 | check_param(param_name, def_val, param_tags) 50 | end 51 | end 52 | 53 | # Checks if a parameter is documented properly and if it has a correct default value 54 | def check_param(param_name, def_val = nil, param_tags = select_tag_name('param')) 55 | param_tag = param_tags.find { |t| t.name == param_name } 56 | return unless param_documented?(param_name, param_tag) 57 | 58 | valid_param_description?(param_tag) 59 | valid_param_types?(param_tag) 60 | valid_param_default?(param_tag, def_val) 61 | end 62 | 63 | # Checks if a parameter is documented 64 | def param_documented?(param_name, param_tag) 65 | return true if param_tag 66 | 67 | new_finding(:error, :param_not_documented, finding_data(param: param_name)) 68 | false 69 | end 70 | 71 | # Checks if a parameter has a description 72 | def valid_param_description?(param) 73 | return true if AbideDevUtils::Validate.populated_string?(param.text) 74 | 75 | new_finding(:error, :param_missing_description, finding_data(param: param.name)) 76 | false 77 | end 78 | 79 | # Checks if a parameter is typed 80 | def valid_param_types?(param) 81 | unless param.types&.any? 82 | new_finding(:error, :param_missing_types, finding_data(param: param.name)) 83 | return false 84 | end 85 | true 86 | end 87 | 88 | # Checks if a parameter has a default value and if it is correct for the type 89 | def valid_param_default?(param, def_val) 90 | return true if def_val.nil? 91 | 92 | if param.types.first.start_with?('Optional[') && def_val != 'undef' 93 | new_finding(:error, :param_optional_without_undef_default, param: param.name, default_value: def_val, 94 | name: name, file: file) 95 | return false 96 | end 97 | true 98 | end 99 | end 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/benchmark_loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../ppt/puppet_module' 4 | require_relative 'benchmark' 5 | 6 | module AbideDevUtils 7 | module Sce 8 | # Namespace for classes and methods for loading benchmarks 9 | module BenchmarkLoader 10 | # Load benchmarks from a Puppet module 11 | # @param module_dir [String] the directory of the Puppet module 12 | # @param opts [Hash] options for loading the benchmarks 13 | # @option opts [Boolean] :ignore_all_errors ignore all errors when loading benchmarks 14 | # @option opts [Boolean] :ignore_framework_mismatch ignore errors when the framework doesn't match 15 | # @return [Array] the loaded benchmarks 16 | def self.benchmarks_from_puppet_module(module_dir = Dir.pwd, **opts) 17 | PupMod.new(module_dir, **opts).load 18 | end 19 | 20 | # Loads benchmark data for a Puppet module 21 | class PupMod 22 | attr_reader :pupmod, :load_errors, :load_warnings, :ignore_all_errors, :ignore_framework_mismatch 23 | 24 | def initialize(module_dir = Dir.pwd, **opts) 25 | @pupmod = AbideDevUtils::Ppt::PuppetModule.new(module_dir) 26 | @load_errors = [] 27 | @load_warnings = [] 28 | @ignore_all_errors = opts.fetch(:ignore_all_errors, false) 29 | @ignore_framework_mismatch = opts.fetch(:ignore_framework_mismatch, false) 30 | end 31 | 32 | # Load the benchmark from the Puppet module 33 | # @return [Array] the loaded benchmarks 34 | # @raise [AbideDevUtils::Errors::BenchmarkLoadError] if a benchmark fails to load 35 | def load 36 | clear_load_errors 37 | clear_load_warnings 38 | pupmod.supported_os.each_with_object([]) do |supp_os, ary| 39 | osname, majver = supp_os.split('::') 40 | if majver.is_a?(Array) 41 | majver.sort.each do |v| 42 | frameworks.each do |fw| 43 | ary << new_benchmark(osname, v, fw) 44 | rescue StandardError => e 45 | handle_load_error(e, fw, osname, v, pupmod.name(strip_namespace: true)) 46 | end 47 | end 48 | else 49 | frameworks.each do |fw| 50 | ary << new_benchmark(osname, majver, fw) 51 | rescue StandardError => e 52 | handle_load_error(e, fw, osname, majver, pupmod.name(strip_namespace: true)) 53 | end 54 | end 55 | end 56 | end 57 | 58 | private 59 | 60 | def clear_load_errors 61 | @load_errors = [] 62 | end 63 | 64 | def clear_load_warnings 65 | @load_warnings = [] 66 | end 67 | 68 | def frameworks 69 | @frameworks ||= pupmod.hiera_conf.local_hiera_files(hierarchy_name: 'Mapping Data').each_with_object([]) do |hf, ary| 70 | parts = hf.path.split(pupmod.hiera_conf.default_datadir)[-1].split('/') 71 | ary << parts[2] unless ary.include?(parts[2]) 72 | end 73 | end 74 | 75 | def new_benchmark(osname, majver, framework) 76 | benchmark = AbideDevUtils::Sce::Benchmark.new( 77 | osname, 78 | majver, 79 | pupmod.hiera_conf, 80 | pupmod.name(strip_namespace: true), 81 | framework: framework 82 | ) 83 | benchmark.controls 84 | benchmark 85 | end 86 | 87 | def handle_load_error(error, framework, osname, majver, module_name) 88 | err = AbideDevUtils::Errors::BenchmarkLoadError.new(error.message) 89 | err.set_backtrace(error.backtrace) 90 | err.framework = framework 91 | err.osname = osname 92 | err.major_version = majver 93 | err.module_name = module_name 94 | err.original_error = error 95 | if error.is_a?(AbideDevUtils::Errors::MappingDataFrameworkMismatchError) && ignore_framework_mismatch 96 | @load_warnings << err 97 | elsif ignore_all_errors 98 | @load_errors << err 99 | else 100 | @load_errors << err 101 | raise err 102 | end 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/jira/dry_run.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../output' 4 | 5 | module AbideDevUtils 6 | module Jira 7 | module DryRun 8 | def dry_run(*method_names) 9 | method_names.each do |method_name| 10 | proxy = Module.new do 11 | define_method(method_name) do |*args, **kwargs| 12 | if !!@dry_run 13 | case method_name 14 | when %r{^create} 15 | AbideDevUtils::Output.simple("DRY RUN: #{self.class.name}##{method_name}(#{args[0]}, #{kwargs.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')})") 16 | sleep 0.1 17 | return DummyIssue.new if args[0].match?(%r{^issue$}) 18 | return DummySubtask.new if args[0].match?(%r{^subtask$}) 19 | when %r{^find} 20 | AbideDevUtils::Output.simple("DRY RUN: #{self.class.name}##{method_name}(#{args[0]}, #{kwargs.inspect})") 21 | return DummyIssue.new if args[0].match?(%r{^issue$}) 22 | return DummySubtask.new if args[0].match?(%r{^subtask$}) 23 | return DummyProject.new if args[0].match?(%r{^project$}) 24 | return [] if args[0].match?(%r{^issues_by_jql$}) 25 | 26 | "Dummy #{args[0].capitalize}" 27 | else 28 | AbideDevUtils::Output.simple("DRY RUN: #{self.class.name}##{method_name}(#{args.map(&:inspect).join(', ')}, #{kwargs.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')})") 29 | end 30 | else 31 | super(*args, **kwargs) 32 | end 33 | end 34 | end 35 | self.prepend(proxy) 36 | end 37 | end 38 | 39 | def dry_run_simple(*method_names) 40 | method_names.each do |method_name| 41 | proxy = Module.new do 42 | define_method(method_name) do |*args, **kwargs| 43 | return if !!@dry_run 44 | 45 | super(*args, **kwargs) 46 | end 47 | end 48 | self.prepend(proxy) 49 | end 50 | end 51 | 52 | def dry_run_return_true(*method_names) 53 | method_names.each do |method_name| 54 | proxy = Module.new do 55 | define_method(method_name) do |*args, **kwargs| 56 | return true if !!@dry_run 57 | 58 | super(*args, **kwargs) 59 | end 60 | end 61 | self.prepend(proxy) 62 | end 63 | end 64 | 65 | def dry_run_return_false(*method_names) 66 | method_names.each do |method_name| 67 | proxy = Module.new do 68 | define_method(method_name) do |*args, **kwargs| 69 | return false if !!@dry_run 70 | 71 | super(*args, **kwargs) 72 | end 73 | end 74 | self.prepend(proxy) 75 | end 76 | end 77 | 78 | class Dummy 79 | attr_reader :dummy 80 | 81 | def initialize(*_args, **_kwargs) 82 | @dummy = true 83 | end 84 | end 85 | 86 | class DummyIssue < Dummy 87 | attr_reader :summary, :key 88 | 89 | def initialize(summary = 'Dummy Issue', key = 'DUM-111') 90 | super 91 | @summary = summary 92 | @key = key 93 | end 94 | 95 | def labels 96 | @labels ||= ['abide_dev_utils'] 97 | end 98 | 99 | def attrs 100 | { 101 | 'fields' => { 102 | 'project' => 'dummy', 103 | 'priority' => 'dummy', 104 | }, 105 | } 106 | end 107 | end 108 | 109 | class DummySubtask < DummyIssue 110 | def initialize(summary = 'Dummy Subtask', key = 'DUM-222') 111 | super(summary, key) 112 | end 113 | 114 | def attrs 115 | { 116 | 'fields' => { 117 | 'project' => 'dummy', 118 | 'priority' => 'dummy', 119 | 'parent' => DummyIssue.new, 120 | }, 121 | } 122 | end 123 | end 124 | 125 | class DummyProject < Dummy 126 | attr_reader :key, :issues 127 | 128 | def initialize(key = 'DUM', issues = [DummyIssue.new, DummySubtask.new]) 129 | super 130 | @key = 'DUM' 131 | @issues = [DummyIssue.new, DummySubtask.new] 132 | end 133 | end 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/hiera_data/resource_data/.control.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This should be unused, keeping around incase shit breaks and it's needed 4 | require 'abide_dev_utils/dot_number_comparable' 5 | require 'abide_dev_utils/errors' 6 | require 'abide_dev_utils/sce/hiera_data/mapping_data' 7 | require 'abide_dev_utils/sce/hiera_data/resource_data/parameters' 8 | 9 | module AbideDevUtils 10 | module Sce 11 | module HieraData 12 | module ResourceData 13 | # Represents a singular rule in a benchmark 14 | class Control 15 | include AbideDevUtils::DotNumberComparable 16 | attr_reader :id, :parameters, :resource, :framework 17 | 18 | def initialize(id, params, resource, framework, mapper) 19 | validate_id_with_framework(id, framework, mapper) 20 | @id = id 21 | @parameters = Parameters.new(params) 22 | @resource = resource 23 | @framework = framework 24 | @mapper = mapper 25 | raise AbideDevUtils::Errors::NoMappingDataForControlError, @id unless @mapper.get(id) 26 | end 27 | 28 | def alternate_ids(level: nil, profile: nil) 29 | id_map = @mapper.get(id, level: level, profile: profile) 30 | if display_title_type.to_s == @mapper.map_type(id) 31 | id_map 32 | else 33 | alt_ids = id_map.each_with_object([]) do |mapval, arr| 34 | arr << if display_title_type.to_s == @mapper.map_type(mapval) 35 | @mapper.get(mapval, level: level, profile: profile) 36 | else 37 | mapval 38 | end 39 | end 40 | alt_ids.flatten.uniq 41 | end 42 | end 43 | 44 | def id_map_type 45 | @mapper.map_type(id) 46 | end 47 | 48 | def display_title 49 | send(display_title_type) unless display_title_type.nil? 50 | end 51 | 52 | def levels 53 | levels_and_profiles[0] 54 | end 55 | 56 | def profiles 57 | levels_and_profiles[1] 58 | end 59 | 60 | def method_missing(meth, *args, &block) 61 | meth_s = meth.to_s 62 | if AbideDevUtils::Sce::HieraData::MappingData::ALL_TYPES.include?(meth_s) 63 | @mapper.get(id).find { |x| @mapper.map_type(x) == meth_s } 64 | else 65 | super 66 | end 67 | end 68 | 69 | def respond_to_missing?(meth, include_private = false) 70 | AbideDevUtils::Sce::HieraData::MappingData::ALL_TYPES.include?(meth.to_s) || super 71 | end 72 | 73 | def to_h 74 | { 75 | id: id, 76 | display_title: display_title, 77 | alternate_ids: alternate_ids, 78 | levels: levels, 79 | profiles: profiles, 80 | resource: resource 81 | }.merge(parameters.to_h) 82 | end 83 | 84 | private 85 | 86 | def display_title_type 87 | if (!vulnid.nil? && !vulnid.is_a?(String)) || !title.is_a?(String) 88 | nil 89 | elsif framework == 'stig' && vulnid 90 | :vulnid 91 | else 92 | :title 93 | end 94 | end 95 | 96 | def validate_id_with_framework(id, framework, mapper) 97 | mtype = mapper.map_type(id) 98 | return if AbideDevUtils::Sce::HieraData::MappingData::FRAMEWORK_TYPES[framework].include?(mtype) 99 | 100 | raise AbideDevUtils::Errors::ControlIdFrameworkMismatchError, [id, mtype, framework] 101 | end 102 | 103 | def map 104 | @map ||= @mapper.get(id) 105 | end 106 | 107 | def levels_and_profiles 108 | @levels_and_profiles ||= find_levels_and_profiles 109 | end 110 | 111 | def find_levels_and_profiles 112 | lvls = [] 113 | profs = [] 114 | @mapper.levels.each do |lvl| 115 | @mapper.profiles.each do |prof| 116 | unless @mapper.get(id, level: lvl, profile: prof).nil? 117 | lvls << lvl 118 | profs << prof 119 | end 120 | end 121 | end 122 | [lvls.flatten.compact.uniq, profs.flatten.compact.uniq] 123 | end 124 | end 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/validate/strings/base_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'validation_finding' 4 | 5 | module AbideDevUtils 6 | module Sce 7 | module Validate 8 | module Strings 9 | # Base class for validating Puppet Strings objects. This class can be used directly, but it is 10 | # recommended to use a subclass of this class to provide more specific validation logic. Each 11 | # subclass should implement a `validate_` method that will be called by the `validate` method 12 | # of this class. The `validate_` method should contain the validation logic for the 13 | # corresponding type of Puppet Strings object. 14 | class BaseValidator 15 | SAFE_OBJECT_METHODS = %i[ 16 | type 17 | name 18 | file 19 | line 20 | docstring 21 | tags 22 | parameters 23 | source 24 | ].freeze 25 | PDK_SUMMARY_REGEX = %r{^A short summary of the purpose.*}.freeze 26 | PDK_DESCRIPTION_REGEX = %r{^A description of what this.*}.freeze 27 | 28 | attr_reader :findings 29 | 30 | def initialize(strings_object) 31 | @object = strings_object 32 | @findings = [] 33 | # Define instance methods for each of the SAFE_OBJECT_METHODS 34 | SAFE_OBJECT_METHODS.each do |method| 35 | define_singleton_method(method) { safe_method_call(@object, method) } 36 | end 37 | end 38 | 39 | def errors 40 | @findings.select { |f| f.type == :error } 41 | end 42 | 43 | def warnings 44 | @findings.select { |f| f.type == :warning } 45 | end 46 | 47 | def errors? 48 | !errors.empty? 49 | end 50 | 51 | def warnings? 52 | !warnings.empty? 53 | end 54 | 55 | def find_tag_name(tag_name) 56 | tags&.find { |t| t.tag_name == tag_name } 57 | end 58 | 59 | def select_tag_name(tag_name) 60 | return [] if tags.nil? 61 | 62 | tags.select { |t| t.tag_name == tag_name } 63 | end 64 | 65 | def find_parameter(param_name) 66 | parameters&.find { |p| p[0] == param_name } 67 | end 68 | 69 | def validate 70 | license_tag? 71 | non_generic_summary? 72 | non_generic_description? 73 | send("validate_#{type}".to_sym) if respond_to?("validate_#{type}".to_sym) 74 | end 75 | 76 | # Checks if the object has a license tag and if it is formatted correctly. 77 | # Comparison is not case sensitive. 78 | def license_tag? 79 | see_tags = select_tag_name('see') 80 | if see_tags.empty? || see_tags.none? { |t| t.name.casecmp('LICENSE.pdf') && t.text.casecmp('for license') } 81 | new_finding( 82 | :error, 83 | :no_license_tag, 84 | remediation: 'Add "@see LICENSE.pdf for license" to the class documentation' 85 | ) 86 | return false 87 | end 88 | true 89 | end 90 | 91 | # Checks if the summary is not the default PDK summary. 92 | def non_generic_summary? 93 | summary = find_tag_name('summary')&.text 94 | return true if summary.nil? 95 | 96 | if summary.match?(PDK_SUMMARY_REGEX) 97 | new_finding(:warning, :generic_summary, remediation: 'Add a more descriptive summary') 98 | return false 99 | end 100 | true 101 | end 102 | 103 | # Checks if the description is not the default PDK description. 104 | def non_generic_description? 105 | description = docstring 106 | return true if description.nil? 107 | 108 | if description.match?(PDK_DESCRIPTION_REGEX) 109 | new_finding(:warning, :generic_description, remediation: 'Add a more descriptive description') 110 | return false 111 | end 112 | true 113 | end 114 | 115 | private 116 | 117 | def safe_method_call(obj, method, *args) 118 | obj.send(method, *args) 119 | rescue NoMethodError 120 | nil 121 | end 122 | 123 | def new_finding(type, title, **data) 124 | @findings << ValidationFinding.new(type, title.to_sym, data) 125 | end 126 | end 127 | end 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/xccdf/parser/objects/numbered_object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbideDevUtils 4 | module XCCDF 5 | module Parser 6 | module Objects 7 | # Methods for interacting with objects that have numbers (e.g. Group, Rule, etc.) 8 | # This module is included in the Benchmark class and Group / Rule classes 9 | module NumberedObject 10 | include ::Comparable 11 | 12 | def <=>(other) 13 | return 0 if number_eq(number, other.number) 14 | return 1 if number_gt(number, other.number) 15 | return -1 if number_lt(number, other.number) 16 | end 17 | 18 | def number_eq(this_num, other_num) 19 | this_num == other_num 20 | end 21 | 22 | def number_parent_of?(this_num, other_num) 23 | return false if number_eq(this_num, other_num) 24 | 25 | # We split the numbers into parts and compare the resulting arrays 26 | num1_parts = this_num.to_s.split('.') 27 | num2_parts = other_num.to_s.split('.') 28 | # For this_num to be a parent of other_num, the number of parts in 29 | # this_num must be less than the number of parts in other_num. 30 | # Additionally, each part of this_num must be equal to the parts of 31 | # other_num at the same index. 32 | # Example: this_num = '1.2.3' and other_num = '1.2.3.4' 33 | # In this case, num1_parts = ['1', '2', '3'] and num2_parts = ['1', '2', '3', '4'] 34 | # So, this_num is a parent of other_num because at indexes 0, 1, and 2 35 | # of num1_parts and num2_parts, the parts are equal. 36 | num1_parts.length < num2_parts.length && 37 | num2_parts[0..(num1_parts.length - 1)] == num1_parts 38 | end 39 | 40 | def number_child_of?(this_num, other_num) 41 | number_parent_of?(other_num, this_num) 42 | end 43 | 44 | def number_gt(this_num, other_num) 45 | return false if number_eq(this_num, other_num) 46 | return true if number_parent_of?(this_num, other_num) 47 | 48 | num1_parts = this_num.to_s.split('.') 49 | num2_parts = other_num.to_s.split('.') 50 | num1_parts.zip(num2_parts).each do |num1_part, num2_part| 51 | next if num1_part == num2_part # we skip past equal parts 52 | 53 | # If num1_part is nil that means that we've had equal numbers so far. 54 | # Therfore, this_num is greater than other num because of the 55 | # hierarchical nature of the numbers. 56 | # Example: this_num = '1.2' and other_num = '1.2.3' 57 | # In this case, num1_part is nil and num2_part is '3' 58 | # So, this_num is greater than other_num 59 | return true if num1_part.nil? 60 | # If num2_part is nil that means that we've had equal numbers so far. 61 | # Therfore, this_num is less than other num because of the 62 | # hierarchical nature of the numbers. 63 | # Example: this_num = '1.2.3' and other_num = '1.2' 64 | # In this case, num1_part is '3' and num2_part is nil 65 | # So, this_num is less than other_num 66 | return false if num2_part.nil? 67 | 68 | return num1_part.to_i > num2_part.to_i 69 | end 70 | end 71 | 72 | def number_lt(this_num, other_num) 73 | number_gt(other_num, this_num) 74 | end 75 | 76 | # This method will recursively walk the tree to find the first 77 | # child, grandchild, etc. that has a number method and returns the 78 | # matching number. 79 | # @param [String] number The number to find in the tree 80 | # @return [Group] The first child, grandchild, etc. that has a matching number 81 | # @return [Rule] The first child, grandchild, etc. that has a matching number 82 | # @return [nil] If no child, grandchild, etc. has a matching number 83 | def search_children_by_number(number) 84 | find_children_that_respond_to(:number).find do |child| 85 | if number_eq(child.number, number) 86 | child 87 | elsif number_parent_of?(child.number, number) 88 | # We recursively search the child for its child with the number 89 | # if our number is a parent of the child's number 90 | return child.search_children_by_number(number) 91 | end 92 | end 93 | end 94 | 95 | def find_child_by_number(number) 96 | find_children_that_respond_to(:number).find do |child| 97 | number_eq(child.number, number) 98 | end 99 | end 100 | end 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/abide_dev_utils/xccdf_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pathname' 4 | require 'yaml' 5 | # require 'abide_dev_utils/xccdf' 6 | # require 'abide_dev_utils/errors' 7 | require 'spec_helper' 8 | 9 | spec_dir = Pathname.new(__dir__).parent 10 | linux_xccdf = "#{spec_dir}/resources/cis/CIS_CentOS_Linux_7_Benchmark_v3.0.0-xccdf.xml" 11 | windows_xccdf = "#{spec_dir}/resources/cis/CIS_Microsoft_Windows_Server_2016_RTM_\(Release_1607\)_Benchmark_v1.2.0-xccdf.xml" 12 | 13 | benchmark_hash = { 14 | 'Linux' => { 15 | benchmark: nil, 16 | title: 'CIS CentOS Linux 7 Benchmark', 17 | normalized_title: 'cis_centos_linux_7_benchmark', 18 | ctrl_full: 'xccdf_org.cisecurity.benchmarks_rule_1.1.1.3_Ensure_mounting_of_udf_filesystems_is_disabled', 19 | ctrl_name_fmt: 'ensure_mounting_of_udf_filesystems_is_disabled', 20 | ctrl_num_fmt: 'c1_1_1_3' 21 | }, 22 | 'Windows' => { 23 | benchmark: nil, 24 | title: 'CIS Microsoft Windows Server 2016 RTM (Release 1607) Benchmark', 25 | normalized_title: 'cis_microsoft_windows_server_2016_rtm_release_1607_benchmark', 26 | ctrl_full: 'xccdf_org.cisecurity.benchmarks_rule_1.1.3_L1_Ensure_Minimum_password_age_is_set_to_1_or_more_days', 27 | ctrl_name_fmt: 'ensure_minimum_password_age_is_set_to_1_or_more_days', 28 | ctrl_num_fmt: 'c1_1_3' 29 | } 30 | } 31 | 32 | RSpec.describe 'AbideDevUtils::XCCDF' do 33 | it 'creates a Benchmark object from Linux XCCDF' do 34 | expect { AbideDevUtils::XCCDF::Benchmark.new(linux_xccdf) }.not_to raise_error 35 | end 36 | 37 | it 'creates a Benchmark object from Windows XCCDF' do 38 | expect { AbideDevUtils::XCCDF::Benchmark.new(windows_xccdf) }.not_to raise_error 39 | end 40 | 41 | it 'Creates control map from Windows XCCDF' do 42 | opts = { console: true, type: 'cis', parent_key_prefix: '' } 43 | expect { AbideDevUtils::XCCDF.gen_map(windows_xccdf, **opts) }.not_to raise_error 44 | end 45 | 46 | it 'Creates control map from Linux XCCDF' do 47 | opts = { console: true, type: 'cis', parent_key_prefix: '' } 48 | expect { AbideDevUtils::XCCDF.gen_map(linux_xccdf, **opts) }.not_to raise_error 49 | end 50 | 51 | it 'raises FileNotFoundError when creating object Benchmark object with bad file path' do 52 | expect { AbideDevUtils::XCCDF::Benchmark.new('/fake/path') }.to raise_error( 53 | AbideDevUtils::Errors::FileNotFoundError 54 | ) 55 | end 56 | 57 | context 'when using Benchmark object' do 58 | benchmark_hash['Linux'][:benchmark] = AbideDevUtils::XCCDF::Benchmark.new(linux_xccdf) 59 | benchmark_hash['Windows'][:benchmark] = AbideDevUtils::XCCDF::Benchmark.new(windows_xccdf) 60 | benchmark_hash.each do |os, data| 61 | context "from #{os} XCCDF" do 62 | let(:benchmark) { data[:benchmark] } 63 | let(:title) { data[:title] } 64 | let(:normalized_title) { data[:normalized_title] } 65 | let(:ctrl_full) { data[:ctrl_full] } 66 | let(:ctrl_name_fmt) { data[:ctrl_name_fmt] } 67 | let(:ctrl_num_fmt) { data[:ctrl_num_fmt] } 68 | 69 | it 'has correct title' do 70 | expect(benchmark.title).to eq(title) 71 | end 72 | 73 | it 'has correct normalized title' do 74 | expect(benchmark.normalized_title).to eq(normalized_title) 75 | end 76 | 77 | it 'returns and empty array on bad xpath query' do 78 | expect(benchmark.xpath('fake/xpath').empty?).to be true 79 | end 80 | 81 | it 'correctly trims non-alphanumeric character at end of string' do 82 | expect(benchmark.normalize_string('test_string.')).to eq 'test_string' 83 | end 84 | 85 | it 'correctly trims non-alpha character at start of string' do 86 | expect(benchmark.normalize_string('.test_string')).to eq 'test_string' 87 | end 88 | 89 | it 'correctly trims level 1 prefix at start of string' do 90 | expect(benchmark.normalize_string('l1_test_string')).to eq 'test_string' 91 | end 92 | 93 | it 'correctly trims level 2 prefix at start of string' do 94 | expect(benchmark.normalize_string('l2_test_string')).to eq 'test_string' 95 | end 96 | 97 | it 'correctly normalizes string' do 98 | expect(benchmark.normalize_string('.l2_test_string.')).to eq 'test_string' 99 | end 100 | 101 | it 'correctly formats control name by name' do 102 | expect(benchmark.normalize_control_name(ctrl_full, number_format: false)).to eq ctrl_name_fmt 103 | end 104 | 105 | it 'correctly formats control name by num' do 106 | expect(benchmark.normalize_control_name(ctrl_full, number_format: true)).to eq ctrl_num_fmt 107 | end 108 | 109 | it "correctly creates #{os} parent key" do 110 | expect(YAML.safe_load(benchmark.to_hiera).key?("#{normalized_title}::title")).to be_truthy 111 | end 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/sce/hiera_data/mapping_data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/sce/hiera_data/mapping_data/map_data' 4 | require 'abide_dev_utils/sce/hiera_data/mapping_data/mixins' 5 | 6 | module AbideDevUtils 7 | module Sce 8 | module HieraData 9 | module MappingData 10 | ALL_TYPES = %w[hiera_title_num number hiera_title vulnid title].freeze 11 | FRAMEWORK_TYPES = { 12 | 'cis' => %w[hiera_title_num number hiera_title title], 13 | 'stig' => %w[hiera_title_num number hiera_title vulnid title] 14 | }.freeze 15 | CIS_TYPES = %w[hiera_title_num number hiera_title title].freeze 16 | STIG_TYPES = %w[hiera_title_num number hiera_title vulnid title].freeze 17 | 18 | # Handles interacting with mapping data 19 | class Mapper 20 | attr_reader :module_name, :framework, :map_data 21 | 22 | def initialize(module_name, framework, map_data) 23 | @module_name = module_name 24 | @framework = framework 25 | load_framework(@framework) 26 | @map_data = map_data.map { |_, v| MapData.new(v) } 27 | @cache = {} 28 | @rule_cache = {} 29 | end 30 | 31 | def title 32 | @title ||= benchmark_data['title'] 33 | end 34 | 35 | def version 36 | @version ||= benchmark_data['version'] 37 | end 38 | 39 | def levels 40 | @levels ||= default_map_data.levels 41 | end 42 | 43 | def profiles 44 | @profiles ||= default_map_data.profiles 45 | end 46 | 47 | def each_like(identifier) 48 | identified_map_data(identifier)&.each { |key, val| yield key, val } 49 | end 50 | 51 | def each_with_array_like(identifier) 52 | identified_map_data(identifier)&.each_with_object([]) { |(key, val), ary| yield [key, val], ary } 53 | end 54 | 55 | def get(control_id, level: nil, profile: nil) 56 | identified_map_data(control_id)&.get(control_id, level: level, profile: profile) 57 | end 58 | 59 | def map_type(control_id) 60 | return control_id if ALL_TYPES.include?(control_id) 61 | 62 | case control_id 63 | when %r{^c[0-9_]+$} 64 | 'hiera_title_num' 65 | when %r{^[0-9][0-9.]*$} 66 | 'number' 67 | when %r{^[a-z][a-z0-9_]+$} 68 | 'hiera_title' 69 | when %r{^V-[0-9]{6}$} 70 | 'vulnid' 71 | else 72 | 'title' 73 | end 74 | end 75 | 76 | private 77 | 78 | def load_framework(framework) 79 | case framework.downcase 80 | when 'cis' 81 | self.class.include AbideDevUtils::Sce::HieraData::MappingData::MixinCIS 82 | extend AbideDevUtils::Sce::HieraData::MappingData::MixinCIS 83 | when 'stig' 84 | self.class.include AbideDevUtils::Sce::HieraData::MappingData::MixinSTIG 85 | extend AbideDevUtils::Sce::HieraData::MappingData::MixinSTIG 86 | else 87 | raise "Invalid framework: #{framework}" 88 | end 89 | end 90 | 91 | def map_data_by_type(map_type) 92 | found_map_data = map_data.find { |x| x.type == map_type } 93 | unless found_map_data 94 | raise "Failed to find map data with type #{map_type}; Meta: #{{ framework: framework, 95 | module_name: module_name }}" 96 | end 97 | 98 | found_map_data 99 | end 100 | 101 | def identified_map_data(identifier, valid_types: ALL_TYPES) 102 | mtype = map_type(identifier) 103 | return unless FRAMEWORK_TYPES[framework].include?(mtype) 104 | 105 | map_data_by_type(mtype) 106 | end 107 | 108 | def map_type_and_top_key(identifier) 109 | mtype = ALL_TYPES.include?(identifier) ? identifier : map_type(identifier) 110 | [mtype, map_top_key(mtype)] 111 | end 112 | 113 | def cached?(control_id, *args) 114 | @cache.key?(cache_key(control_id, *args)) 115 | end 116 | 117 | def cache_get(control_id, *args) 118 | ckey = cache_key(control_id, *args) 119 | @cache[ckey] if cached?(control_id, *args) 120 | end 121 | 122 | def cache_set(value, control_id, *args) 123 | @cache[cache_key(control_id, *args)] = value unless value.nil? 124 | end 125 | 126 | def default_map_type 127 | @default_map_type ||= (framework == 'stig' ? 'vulnid' : map_data.first.type) 128 | end 129 | 130 | def default_map_data 131 | @default_map_data ||= map_data.first 132 | end 133 | 134 | def benchmark_data 135 | @benchmark_data ||= default_map_data.benchmark 136 | end 137 | 138 | def cache_key(control_id, *args) 139 | args.unshift(control_id).compact.join('-') 140 | end 141 | 142 | def map_top_key(mtype) 143 | [module_name, 'mappings', framework, mtype].join('::') 144 | end 145 | end 146 | end 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /spec/abide_dev_utils/ppt/new_obj_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tempfile' 4 | 5 | def new_obj_cls_stubs(test_erb) 6 | allow(Dir).to receive(:exist?).and_call_original 7 | allow(FileTest).to receive(:file?).and_call_original 8 | allow(File).to receive(:read).and_call_original 9 | allow(File).to receive(:open).and_call_original 10 | allow(File).to receive(:file?).and_call_original 11 | allow(Dir).to receive(:exist?).with("#{Dir.pwd}/object_templates").and_return(true) 12 | allow(Dir).to receive(:entries).with("#{Dir.pwd}/object_templates").and_return(['test.erb']) 13 | allow(FileTest).to receive(:file?).with("#{Dir.pwd}/object_templates/test.erb").and_return(true) 14 | allow(File).to receive(:read).with("#{Dir.pwd}/object_templates/test.erb").and_return(test_erb) 15 | allow(File).to receive(:open).with("#{Dir.pwd}/manifests/new/object/name.pp", 'w').and_return(true) 16 | allow(File).to receive(:file?).with("#{Dir.pwd}/manifests/new/object/name.pp").and_return(true) 17 | end 18 | 19 | def new_obj_cust_stubs 20 | allow(Dir).to receive(:exist?).and_call_original 21 | allow(FileTest).to receive(:file?).and_call_original 22 | allow(File).to receive(:read).and_call_original 23 | allow(File).to receive(:open).and_call_original 24 | allow(File).to receive(:file?).and_call_original 25 | allow(Dir).to receive(:exist?).with("#{Dir.pwd}/custom_tmpl_dir").and_return(true) 26 | allow(Dir).to receive(:entries).with("#{Dir.pwd}/custom_tmpl_dir").and_return(['custom.erb']) 27 | allow(FileTest).to receive(:file?).with("#{Dir.pwd}/custom_tmpl_dir/custom.erb").and_return(true) 28 | end 29 | 30 | RSpec.describe 'AbideDevUtils::Ppt::NewObjectBuilder' do 31 | let(:new_obj_cls) do 32 | AbideDevUtils::Ppt::NewObjectBuilder.new( 33 | 'test', 34 | 'test::new::object::name', 35 | opts: { 36 | force: true 37 | } 38 | ) 39 | end 40 | let(:new_obj_cust) do 41 | AbideDevUtils::Ppt::NewObjectBuilder.new( 42 | 'test2', 43 | 'test::new::custom::name', 44 | opts: { 45 | tmpl_dir: 'custom_tmpl_dir', 46 | tmpl_name: 'custom.erb', 47 | force: true 48 | } 49 | ) 50 | end 51 | 52 | let(:test_erb) do 53 | <<~ERB 54 | # @api private 55 | class <%= @obj_name %> ( 56 | Boolean $enforced = true, 57 | Hash $config = {}, 58 | ) { 59 | if $enforced { 60 | warning('Class not implemented yet') 61 | } 62 | } 63 | 64 | ERB 65 | end 66 | 67 | let(:test_rendered_erb) do 68 | <<~ERB 69 | # @api private 70 | class test::new::object::name ( 71 | Boolean $enforced = true, 72 | Hash $config = {}, 73 | ) { 74 | if $enforced { 75 | warning('Class not implemented yet') 76 | } 77 | } 78 | 79 | ERB 80 | end 81 | 82 | let(:test_erb_file) do 83 | Tempfile.new('erb') 84 | end 85 | 86 | let(:test_tmpl_data) do 87 | { 88 | path: "#{Dir.pwd}/object_templates/d-test.rb.erb", 89 | fname: 'd-test.rb.erb', 90 | ext: '.rb', 91 | pfx: 'd-', 92 | spec_base: 'defines', 93 | obj_name: 'test.rb', 94 | spec_name: 'name_spec.rb', 95 | spec_path: "#{Dir.pwd}/spec/defines/new/object/name_spec.rb" 96 | } 97 | end 98 | 99 | it 'creates a builder object' do 100 | new_obj_cls_stubs(test_erb) 101 | expect(new_obj_cls).to exist 102 | end 103 | 104 | it 'creates a builder object of a custom type' do 105 | new_obj_cust_stubs 106 | expect(new_obj_cust).to exist 107 | end 108 | 109 | it 'has correct object path for class type' do 110 | new_obj_cls_stubs(test_erb) 111 | expect(new_obj_cls.obj_path).to eq "#{Dir.pwd}/manifests/new/object/name.pp" 112 | end 113 | 114 | it 'has correct object path for custom type' do 115 | new_obj_cust_stubs 116 | expect(new_obj_cust.obj_path).to eq "#{Dir.pwd}/manifests/new/custom/name.pp" 117 | end 118 | 119 | it 'has correct spec path for class type' do 120 | new_obj_cls_stubs(test_erb) 121 | puts new_obj_cls.tmpl_data 122 | expect(new_obj_cls.tmpl_data[:spec_path]).to eq "#{Dir.pwd}/spec/classes/new/object/name_spec.rb" 123 | end 124 | 125 | it 'correctly finds template file' do 126 | new_obj_cls_stubs(test_erb) 127 | allow(Dir).to receive(:entries).with("#{Dir.pwd}/object_templates").and_return(['test.pp.erb']) 128 | expect(new_obj_cls.send(:templates)).to eq ["#{Dir.pwd}/object_templates/test.pp.erb"] 129 | end 130 | 131 | it 'correctly filters invalid template file' do 132 | new_obj_cls_stubs(test_erb) 133 | allow(Dir).to receive(:entries).with("#{Dir.pwd}/object_templates").and_return(['test.pp.erb', 'test.pp']) 134 | expect(new_obj_cls.send(:templates)).to eq ["#{Dir.pwd}/object_templates/test.pp.erb"] 135 | end 136 | 137 | it 'correctly parses template data' do 138 | new_obj_cls_stubs(test_erb) 139 | allow(Dir).to receive(:entries).with("#{Dir.pwd}/object_templates").and_return(['d-test.rb.erb']) 140 | expect(new_obj_cls.send(:template_data, 'test')).to eq test_tmpl_data 141 | end 142 | 143 | it 'correctly handles rendering a template' do 144 | new_obj_cls_stubs(test_erb) 145 | expect(new_obj_cls.send(:render, "#{Dir.pwd}/object_templates/test.erb")).to eq test_rendered_erb 146 | end 147 | 148 | # TODO: Need to implement something like FakeFS to test this properly 149 | # it 'correctly handles building a template' do 150 | # new_obj_cls_stubs(test_erb) 151 | # expect(new_obj_cls.build).not_to raise_error 152 | # end 153 | end 154 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/strings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'puppet-strings' 4 | require 'puppet-strings/yard' 5 | 6 | module AbideDevUtils 7 | module Ppt 8 | # Puppet Strings reference object 9 | class Strings 10 | REGISTRY_TYPES = %i[ 11 | root 12 | module 13 | class 14 | puppet_class 15 | puppet_data_type 16 | puppet_data_type_alias 17 | puppet_defined_type 18 | puppet_type 19 | puppet_provider 20 | puppet_function 21 | puppet_task 22 | puppet_plan 23 | ].freeze 24 | 25 | attr_reader :search_patterns 26 | 27 | def initialize(search_patterns: nil, **opts) 28 | check_yardoc_dir 29 | @search_patterns = search_patterns || PuppetStrings::DEFAULT_SEARCH_PATTERNS 30 | @debug = opts[:debug] 31 | @quiet = opts[:quiet] 32 | PuppetStrings::Yard.setup! 33 | YARD::CLI::Yardoc.run(*yard_args(@search_patterns, debug: @debug, quiet: @quiet)) 34 | end 35 | 36 | def debug? 37 | !!@debug 38 | end 39 | 40 | def quiet? 41 | !!@quiet 42 | end 43 | 44 | def registry 45 | @registry ||= YARD::Registry.all(*REGISTRY_TYPES).map { |i| YardObjectWrapper.new(i) } 46 | end 47 | 48 | def find_resource(resource_name) 49 | to_h.each do |_, resources| 50 | res = resources.find { |r| r[:name] == resource_name.to_sym } 51 | return res if res 52 | end 53 | end 54 | 55 | def puppet_classes(hashed: false) 56 | reg_type(:puppet_class, hashed: hashed) 57 | end 58 | 59 | def data_types(hashed: false) 60 | reg_type(:puppet_data_types, hashed: hashed) 61 | end 62 | alias puppet_data_type data_types 63 | 64 | def data_type_aliases(hashed: false) 65 | reg_type(:puppet_data_type_alias, hashed: hashed) 66 | end 67 | alias puppet_data_type_alias data_type_aliases 68 | 69 | def defined_types(hashed: false) 70 | reg_type(:puppet_defined_type, hashed: hashed) 71 | end 72 | alias puppet_defined_type defined_types 73 | 74 | def resource_types(hashed: false) 75 | reg_type(:puppet_type, hashed: hashed) 76 | end 77 | alias puppet_type resource_types 78 | 79 | def providers(hashed: false) 80 | reg_type(:puppet_provider, hashed: hashed) 81 | end 82 | alias puppet_provider providers 83 | 84 | def puppet_functions(hashed: false) 85 | reg_type(:puppet_function, hashed: hashed) 86 | end 87 | alias puppet_function puppet_functions 88 | 89 | def puppet_tasks(hashed: false) 90 | reg_type(:puppet_task, hashed: hashed) 91 | end 92 | alias puppet_task puppet_tasks 93 | 94 | def puppet_plans(hashed: false) 95 | reg_type(:puppet_plan, hashed: hashed) 96 | end 97 | alias puppet_plan puppet_plans 98 | 99 | def to_h 100 | { 101 | puppet_classes: puppet_classes, 102 | data_types: data_types, 103 | data_type_aliases: data_type_aliases, 104 | defined_types: defined_types, 105 | resource_types: resource_types, 106 | providers: providers, 107 | puppet_functions: puppet_functions, 108 | puppet_tasks: puppet_tasks, 109 | puppet_plans: puppet_plans, 110 | } 111 | end 112 | 113 | private 114 | 115 | def check_yardoc_dir 116 | yardoc_dir = File.expand_path('./.yardoc') 117 | return unless Dir.exist?(yardoc_dir) && !File.writable?(yardoc_dir) 118 | 119 | raise "yardoc directory permissions error. Ensure #{yardoc_dir} is writable by current user." 120 | end 121 | 122 | def reg_type(reg_type, hashed: false) 123 | hashed ? hashes_for_reg_type(reg_type) : select_by_reg_type(reg_type) 124 | end 125 | 126 | def select_by_reg_type(reg_type) 127 | registry.select { |i| i.type == reg_type } 128 | end 129 | 130 | def hashes_for_reg_type(reg_type) 131 | all_to_h(select_by_reg_type(reg_type)) 132 | end 133 | 134 | def all_to_h(objects) 135 | objects.sort_by(&:name).map(&:to_hash) 136 | end 137 | 138 | def yard_args(patterns, debug: false, quiet: false) 139 | args = ['doc', '--no-progress', '-n'] 140 | args << '--debug' if debug && !quiet 141 | args << '--backtrace' if debug && !quiet 142 | args << '-q' if quiet 143 | args << '--no-stats' if quiet 144 | args += patterns 145 | args 146 | end 147 | end 148 | 149 | # Wrapper class for Yard objects that allows associating things like validators with them 150 | class YardObjectWrapper 151 | attr_accessor :validator 152 | attr_reader :object 153 | 154 | def initialize(object, validator: nil) 155 | @object = object 156 | @validator = validator 157 | end 158 | 159 | def method_missing(method, *args, &block) 160 | if object.respond_to?(method) 161 | object.send(method, *args, &block) 162 | elsif validator.respond_to?(method) 163 | validator.send(method, *args, &block) 164 | else 165 | super 166 | end 167 | end 168 | 169 | def respond_to_missing?(method, include_private = false) 170 | object.respond_to?(method) || validator.respond_to?(method) || super 171 | end 172 | 173 | def to_hash 174 | object.to_hash 175 | end 176 | alias to_h to_hash 177 | 178 | def to_s 179 | object.to_s 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/score_module.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pathname' 4 | require 'metadata-json-lint' 5 | require 'puppet-lint' 6 | require 'json' 7 | 8 | module AbideDevUtils 9 | module Ppt 10 | class ScoreModule 11 | attr_reader :module_name, :module_dir, :manifests_dir 12 | 13 | def initialize(module_dir) 14 | @module_name = module_dir.split(File::SEPARATOR)[-1] 15 | @module_dir = real_module_dir(module_dir) 16 | @manifests_dir = File.join(real_module_dir(module_dir), 'manifests') 17 | @metadata = JSON.parse(File.join(@module_dir, 'metadata.json')) 18 | end 19 | 20 | def lint 21 | linter_exit_code, linter_output = lint_manifests 22 | { 23 | exit_code: linter_exit_code, 24 | manifests: manifest_count, 25 | lines: line_count, 26 | linter_version: linter_version, 27 | output: linter_output 28 | }.to_json 29 | end 30 | 31 | # def metadata 32 | 33 | # end 34 | 35 | private 36 | 37 | def manifests 38 | @manifests ||= Dir["#{manifests_dir}/**/*.pp"] 39 | end 40 | 41 | def manifest_count 42 | @manifest_count ||= manifests.count 43 | end 44 | 45 | def line_count 46 | @line_count ||= manifests.each_with_object([]) { |x, ary| ary << File.readlines(x).size }.sum 47 | end 48 | 49 | def lint_manifests 50 | results = [] 51 | PuppetLint.configuration.with_filename = true 52 | PuppetLint.configuration.json = true 53 | PuppetLint.configuration.relative = true 54 | linter_exit_code = 0 55 | manifests.each do |manifest| 56 | next if PuppetLint.configuration.ignore_paths.any? { |p| File.fnmatch(p, manifest) } 57 | 58 | linter = PuppetLint.new 59 | linter.file = manifest 60 | linter.run 61 | linter_exit_code = 1 if linter.errors? || linter.warnings? 62 | results << linter.problems.reject { |x| x[:kind] == :ignored } 63 | end 64 | [linter_exit_code, JSON.generate(results)] 65 | end 66 | 67 | def lint_metadata 68 | results = { errors: [], warnings: [] } 69 | results[:errors] << metadata_schema_errors 70 | dep_errors, dep_warnings = metadata_validate_deps 71 | results[:errors] << dep_errors 72 | results[:warnings] << dep_warnings 73 | results[:errors] << metadata_deprecated_fields 74 | end 75 | 76 | def metadata_schema_errors 77 | MetadataJsonLint::Schema.new.validate(@metadata).each_with_object([]) do |err, ary| 78 | check = err[:field] == 'root' ? :required_fields : err[:field] 79 | ary << metadata_err(check, err[:message]) 80 | end 81 | end 82 | 83 | def metadata_validate_deps 84 | return [[], []] unless @metadata.key?('dependencies') 85 | 86 | errors, warnings = [] 87 | duplicates = metadata_dep_duplicates 88 | warnings << duplicates unless duplicates.empty? 89 | @metadata['dependencies'].each do |dep| 90 | e, w = metadata_dep_version_requirement(dep) 91 | errors << e unless e.nil? 92 | warnings << w unless w.nil? 93 | warnings << metadata_dep_version_range(dep['name']) if dep.key?('version_range') 94 | end 95 | [errors.flatten, warnings.flatten] 96 | end 97 | 98 | def metadata_deprecated_fields 99 | %w[types checksum].each_with_object([]) do |field, ary| 100 | next unless @metadata.key?(field) 101 | 102 | ary << metadata_err(:deprecated_fields, "Deprecated field '#{field}' found in metadata.json") 103 | end 104 | end 105 | 106 | def metadata_dep_duplicates 107 | results = [] 108 | duplicates = @metadata['dependencies'].detect { |x| @metadata['dependencies'].count(x) > 1 } 109 | return results if duplicates.empty? 110 | 111 | duplicates.each { |x| results << metadata_err(:dependencies, "Duplicate dependencies on #{x}") } 112 | results 113 | end 114 | 115 | def metadata_dep_version_requirement(dependency) 116 | unless dependency.key?('version_requirement') 117 | return [metadata_err(:dependencies, "Invalid 'version_requirement' field in metadata.json: #{e}"), nil] 118 | end 119 | 120 | ver_req = MetadataJsonLint::VersionRequirement.new(dependency['version_requirement']) 121 | return [nil, metadata_dep_open_ended(dependency['name'], dependency['version_requirement'])] if ver_req.open_ended? 122 | return [nil, metadata_dep_mixed_syntax(dependency['name'], dependency['version_requirement'])] if ver_req.mixed_syntax? 123 | 124 | [nil, nil] 125 | end 126 | 127 | def metadata_dep_open_ended(name, version_req) 128 | metadata_err(:dependencies, "Dependency #{name} has an open ended dependency version requirement #{version_req}") 129 | end 130 | 131 | def metadata_dep_mixed_syntax(name, version_req) 132 | msg = 'Mixing "x" or "*" version syntax with operators is not recommended in ' \ 133 | "metadata.json, use one style in the #{name} dependency: #{version_req}" 134 | metadata_err(:dependencies, msg) 135 | end 136 | 137 | def metadata_dep_version_range(name) 138 | metadata_err(:dependencies, "Dependency #{name} has a 'version_range' attribute which is no longer used by the forge.") 139 | end 140 | 141 | def metadata_err(check, msg) 142 | { check: check, msg: msg } 143 | end 144 | 145 | def linter_version 146 | PuppetLint::VERSION 147 | end 148 | 149 | def relative_manifests 150 | Dir.glob('manifests/**/*.pp') 151 | end 152 | 153 | def real_module_dir(path) 154 | return Pathname.pwd if path.nil? 155 | 156 | return Pathname.new(path).cleanpath(consider_symlink: true) if Dir.exist?(path) 157 | 158 | raise ArgumentError, "Path #{path} is not a directory" 159 | end 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/cli/comply.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/comply' 4 | require 'abide_dev_utils/cli/abstract' 5 | 6 | module Abide 7 | module CLI 8 | class ComplyCommand < AbideCommand 9 | CMD_NAME = 'comply' 10 | CMD_SHORT = 'Commands related to Puppet Comply' 11 | CMD_LONG = 'Namespace for commands related to Puppet Comply' 12 | def initialize 13 | super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: true, deprecated: true) 14 | add_command(ComplyReportCommand.new) 15 | add_command(ComplyCompareReportCommand.new) 16 | end 17 | end 18 | 19 | class ComplyReportCommand < AbideCommand 20 | CMD_NAME = 'report' 21 | CMD_SHORT = 'Generates a yaml report of Puppet Comply scan results' 22 | CMD_LONG = <<~LONGCMD 23 | Generates a yaml file that shows the scan results of all nodes in Puppet Comply. 24 | This command utilizes Selenium WebDriver and the Google Chrome browser to automate 25 | clicking through the Comply UI and building a report. In order to use this command, 26 | you MUST have Google Chrome installed and you MUST install the chromedriver binary. 27 | More info and instructions can be found here: 28 | https://www.selenium.dev/documentation/en/getting_started_with_webdriver/. 29 | LONGCMD 30 | CMD_COMPLY_URL = 'The URL (including https://) of Puppet Comply' 31 | CMD_COMPLY_PASSWORD = 'The password for Puppet Comply' 32 | OPT_TIMEOUT_DESC = <<~EOTO 33 | The number of seconds you would like requests to wait before timing out. Defaults 34 | to 10 seconds. 35 | EOTO 36 | OPT_STATUS_DESC = <<~EODESC 37 | A comma-separated list of check statuses to ONLY include in the report. 38 | Valid statuses are: pass, fail, error, notapplicable, notchecked, unknown, informational 39 | EODESC 40 | OPT_IGNORE_NODES = <<~EOIGN 41 | A comma-separated list of node certnames to ignore building reports for. This 42 | options is mutually exclusive with --only and, if both are set, --only will take precedence 43 | over this option. 44 | EOIGN 45 | OPT_ONLY_NODES = <<~EOONLY 46 | A comma-separated list of node certnames to ONLY build reports for. No other 47 | nodes will have reports built for them except the ones specified. This option 48 | is mutually exclusive with --ignore and, if both are set, this options will 49 | take precedence over --ignore. 50 | EOONLY 51 | def initialize 52 | super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: false) 53 | argument_desc(COMPLY_URL: CMD_COMPLY_URL, COMPLY_PASSWORD: CMD_COMPLY_PASSWORD) 54 | options.on('-o [FILE]', '--out-file [FILE]', 'Path to save the report') { |f| @data[:file] = f } 55 | options.on('-u [USERNAME]', '--username [USERNAME]', 'The username for Comply (defaults to comply)') do |u| 56 | @data[:username] = u 57 | end 58 | options.on('-t [SECONDS]', '--timeout [SECONDS]', OPT_TIMEOUT_DESC) do |t| 59 | @data[:timeout] = t 60 | end 61 | options.on('-s [X,Y,Z]', '--status [X,Y,Z]', 62 | %w[pass fail error notapplicable notchecked unknown informational], 63 | Array, 64 | OPT_STATUS_DESC) do |s| 65 | s&.map! { |i| i == 'notchecked' ? 'not checked' : i } 66 | @data[:status] = s 67 | end 68 | options.on('--only [X,Y,Z]', Array, OPT_ONLY_NODES) do |o| 69 | @data[:onlylist] = o 70 | end 71 | options.on('--ignore [X,Y,Z]', Array, OPT_IGNORE_NODES) do |i| 72 | @data[:ignorelist] = i 73 | end 74 | options.on('--page-source-on-error', 'Dump page source to file on error') do 75 | @data[:page_source_on_error] = true 76 | end 77 | end 78 | 79 | def help_arguments 80 | <<~ARGHELP 81 | Arguments: 82 | COMPLY_URL #{CMD_COMPLY_URL} 83 | COMPLY_PASSWORD #{CMD_COMPLY_PASSWORD} 84 | 85 | ARGHELP 86 | end 87 | 88 | def execute(comply_url = nil, comply_password = nil) 89 | Abide::CLI::VALIDATE.filesystem_path(`command -v chromedriver`.strip) 90 | conf = config_section('comply') 91 | comply_url = conf.fetch(:url) if comply_url.nil? 92 | comply_password = comply_password.nil? ? conf.fetch(:password, Abide::CLI::PROMPT.password) : comply_password 93 | report = AbideDevUtils::Comply.build_report(comply_url, comply_password, conf, **@data) 94 | outfile = @data.fetch(:file, nil).nil? ? conf.fetch(:report_path, 'comply_scan_report.yaml') : @data[:file] 95 | Abide::CLI::OUTPUT.yaml(report, file: outfile) 96 | end 97 | end 98 | 99 | class ComplyCompareReportCommand < AbideCommand 100 | CMD_NAME = 'compare-report' 101 | CMD_SHORT = 'Compare two Comply reports and get the differences.' 102 | CMD_LONG = 'Compare two Comply reports and get the differences. Report A is compared to report B, showing what changes it would take for A to equal B.' 103 | CMD_REPORT_A = 'The current Comply report yaml file' 104 | CMD_REPORT_B = 'The old Comply report yaml file name or full path' 105 | def initialize 106 | super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: false) 107 | argument_desc(REPORT_A: CMD_REPORT_A, REPORT_B: CMD_REPORT_B) 108 | options.on('-u', '--upload-new', 'If you want to upload the new scan report') { @data[:upload] = true } 109 | options.on('-s [STORAGE]', '--remote-storage [STORAGE]', 110 | 'Remote storage to upload the report to. (Only supports "gcloud")') do |x| 111 | @data[:remote_storage] = x 112 | end 113 | options.on('-r [NAME]', '--name [NAME]', 'The name to upload the report as') { |x| @data[:report_name] = x } 114 | end 115 | 116 | def execute(report_a, report_b) 117 | AbideDevUtils::Comply.compare_reports(report_a, report_b, @data) 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/cli/xccdf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'abide_dev_utils/cli/abstract' 4 | require 'abide_dev_utils/xccdf' 5 | 6 | module Abide 7 | module CLI 8 | class XccdfCommand < CmdParse::Command 9 | CMD_NAME = 'xccdf' 10 | CMD_SHORT = 'Commands related to XCCDF files' 11 | CMD_LONG = 'Namespace for commands related to XCCDF files' 12 | def initialize 13 | super(CMD_NAME, takes_commands: true) 14 | short_desc(CMD_SHORT) 15 | long_desc(CMD_LONG) 16 | add_command(CmdParse::HelpCommand.new, default: true) 17 | add_command(XccdfToHieraCommand.new) 18 | add_command(XccdfDiffCommand.new) 19 | add_command(XccdfGenMapCommand.new) 20 | end 21 | end 22 | 23 | class XccdfGenMapCommand < AbideCommand 24 | CMD_NAME = 'gen-map' 25 | CMD_SHORT = 'Generates mappings from XCCDF files' 26 | CMD_LONG = 'Generates mappings for SCE modules from 1 or more XCCDF files as YAML' 27 | CMD_XCCDF_FILES_ARG = 'One or more paths to XCCDF files' 28 | def initialize 29 | super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: false) 30 | argument_desc(XCCDF_FILES: CMD_XCCDF_FILES_ARG) 31 | options.on('-b [TYPE]', '--benchmark-type [TYPE]', 'XCCDF Benchmark type CIS by default') do |b| 32 | @data[:type] = b 33 | end 34 | options.on('-d [DIR]', '--files-output-directory [DIR]', 35 | 'Directory to save files data/mappings by default') do |d| 36 | @data[:dir] = d 37 | end 38 | options.on('-V', '--version-output-dir', 'If saving to a directory, version the output directory') do 39 | @data[:version_output_dir] = true 40 | end 41 | options.on('-q', '--quiet', 'Show no output in the terminal') { @data[:quiet] = true } 42 | options.on('-p [PREFIX]', '--parent-key-prefix [PREFIX]', 'A prefix to append to the parent key') do |p| 43 | @data[:parent_key_prefix] = p 44 | end 45 | end 46 | 47 | def execute(*xccdf_files) 48 | if @data[:quiet] && @data[:dir].nil? 49 | AbideDevUtils::Output.simple("I don\'t know how to quietly output to the console\n¯\\_(ツ)_/¯") 50 | exit 1 51 | end 52 | xccdf_files.each do |xccdf_file| 53 | other_kwarg_syms = %i[type dir quiet parent_key_prefix] 54 | other_kwargs = @data.reject { |k, _| other_kwarg_syms.include?(k) } 55 | hfile = AbideDevUtils::XCCDF.gen_map( 56 | File.expand_path(xccdf_file), 57 | dir: @data[:dir], 58 | type: @data.fetch(:type, 'cis'), 59 | parent_key_prefix: @data.fetch(:parent_key_prefix, ''), 60 | **other_kwargs 61 | ) 62 | mapping_dir = File.dirname(hfile.keys[0]) unless @data[:dir].nil? 63 | unless @data[:quiet] || @data[:dir].nil? || File.directory?(mapping_dir) 64 | AbideDevUtils::Output.simple("Creating directory #{mapping_dir}") 65 | end 66 | FileUtils.mkdir_p(mapping_dir) unless @data[:dir].nil? 67 | hfile.each do |key, val| 68 | file_path = @data[:dir].nil? ? nil : key 69 | AbideDevUtils::Output.yaml(val, console: @data[:dir].nil?, file: file_path) 70 | end 71 | end 72 | end 73 | end 74 | 75 | class XccdfToHieraCommand < AbideCommand 76 | CMD_NAME = 'to-hiera' 77 | CMD_SHORT = 'Generates control coverage report' 78 | CMD_LONG = 'Generates report of valid Puppet classes that match with Hiera controls' 79 | def initialize 80 | super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: false) 81 | options.on('-b [TYPE]', '--benchmark-type [TYPE]', 'XCCDF Benchmark type') { |b| @data[:type] = b } 82 | options.on('-o [FILE]', '--out-file [FILE]', 'Path to save file') { |f| @data[:file] = f } 83 | options.on('-p [PREFIX]', '--parent-key-prefix [PREFIX]', 'A prefix to append to the parent key') do |p| 84 | @data[:parent_key_prefix] = p 85 | end 86 | options.on('-N', '--number-fmt', 'Format Hiera control names based off of control number instead of name.') do 87 | @data[:num] = true 88 | end 89 | end 90 | 91 | def execute(xccdf_file) 92 | @data[:type] = 'cis' if @data[:type].nil? 93 | hfile = AbideDevUtils::XCCDF.to_hiera(xccdf_file, @data) 94 | AbideDevUtils::Output.yaml(hfile, console: @data[:file].nil?, file: @data[:file]) 95 | end 96 | end 97 | 98 | class XccdfDiffCommand < AbideCommand 99 | CMD_NAME = 'diff' 100 | CMD_SHORT = 'Generates a diff report between two XCCDF files' 101 | CMD_LONG = 'Generates a diff report between two XCCDF files' 102 | CMD_FILE1_ARG = 'path to first XCCDF file' 103 | CMD_FILE2_ARG = 'path to second XCCDF file' 104 | def initialize 105 | super(CMD_NAME, CMD_SHORT, CMD_LONG, takes_commands: false) 106 | argument_desc(FILE1: CMD_FILE1_ARG, FILE2: CMD_FILE2_ARG) 107 | options.on('-o [PATH]', '--out-file', 'Save the report as a yaml file') { |x| @data[:outfile] = x } 108 | options.on('-p [PROFILE]', '--profile', 109 | 'Only diff rules belonging to the matching profile. Takes a string that is treated as RegExp') do |x| 110 | @data[:profile] = x 111 | end 112 | options.on('-l [LEVEL]', '--level', 113 | 'Only diff rules belonging to the matching level. Takes a string that is treated as RegExp') do |x| 114 | @data[:level] = x 115 | end 116 | options.on('-i [PROPS]', '--ignore-changed-properties', 117 | 'Ignore changes to specified properties. Takes a comma-separated list.') do |x| 118 | @data[:ignore_changed_properties] = x.split(',') 119 | end 120 | options.on('-r', '--raw', 'Output the diff in raw format') { @data[:raw] = true } 121 | options.on('-q', '--quiet', 'Show no output in the terminal') { @data[:quiet] = false } 122 | end 123 | 124 | def execute(file1, file2) 125 | diffreport = AbideDevUtils::XCCDF.diff(file1, file2, @data) 126 | AbideDevUtils::Output.yaml(diffreport, console: @data.fetch(:quiet, true), file: @data.fetch(:outfile, nil)) 127 | end 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /spec/abide_dev_utils/cli_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | commands = Dir.glob(File.join(__dir__, '../../lib/abide_dev_utils/cli/*.rb')).map { |f| File.basename(f, '.rb') } 6 | commands.reject! { |f| f == 'abstract' } 7 | 8 | # rubocop:disable RSpec/FilePath 9 | # rubocop:disable RSpec/MultipleExpectations 10 | # rubocop:disable RSpec/ExampleLength 11 | RSpec.describe Abide::CLI do 12 | context 'with each command' do 13 | commands.each do |c| 14 | it "executes '#{c}' command with the help flag" do 15 | expect do 16 | output = capture_stdout { described_class.execute([c, '-h']) } 17 | expect(output).to match(/Developer tools for Abide/) 18 | end.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) } 19 | end 20 | end 21 | end 22 | 23 | context 'with Sce commands' do 24 | before do 25 | allow(AbideDevUtils::Output).to receive(:simple).and_return(nil) 26 | allow(AbideDevUtils::Output).to receive(:yaml).and_return(nil) 27 | allow(AbideDevUtils::Output).to receive(:json).and_return(nil) 28 | end 29 | 30 | it 'executes the "sce generate" command with the help flag' do 31 | expect do 32 | output = capture_stdout { described_class.execute(['sce', 'generate', '-h']) } 33 | expect(output).to match(/Developer tools for Abide/) 34 | end.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) } 35 | end 36 | 37 | it 'executes the "sce generate coverage-report" command with options' do 38 | expect(AbideDevUtils::Sce::Generate::CoverageReport).to( 39 | receive(:generate).with( 40 | { 41 | format_func: :to_h, 42 | opts: { 43 | benchmark: 'benchmark', 44 | profile: 'profile', 45 | level: 'level', 46 | ignore_benchmark_errors: true, 47 | xccdf_dir: 'xccdf_dir' 48 | } 49 | } 50 | ).and_return('Coverage report') 51 | ) 52 | expect(AbideDevUtils::Output).to receive(:simple).with('Saving coverage report to test_file...') 53 | expect(AbideDevUtils::Output).to receive(:json).with('Coverage report', console: false, file: 'test_file') 54 | capture_stdout_stderr do 55 | described_class.execute( 56 | [ 57 | 'sce', 58 | 'generate', 59 | 'coverage-report', 60 | '-b', 'benchmark', 61 | '-p', 'profile', 62 | '-l', 'level', 63 | '-X', 'xccdf_dir', 64 | '-o', 'test_file', 65 | '-f', 'json', 66 | '-I' 67 | ] 68 | ) 69 | end 70 | end 71 | 72 | it 'executes the "sce generate reference" command with options' do 73 | allow(AbideDevUtils::Validate).to receive(:puppet_module_directory).and_return(nil) 74 | expect(AbideDevUtils::Sce::Generate::Reference).to( 75 | receive(:generate).with( 76 | { 77 | out_file: 'test_file', 78 | format: 'markdown', 79 | debug: true, 80 | quiet: true, 81 | strict: true, 82 | select_profile: %w[profile1 profile2], 83 | select_level: %w[1 2] 84 | } 85 | ).and_return([[], []]) 86 | ) 87 | capture_stdout_stderr do 88 | described_class.execute( 89 | [ 90 | 'sce', 91 | 'generate', 92 | 'reference', 93 | '-o', 'test_file', 94 | '-f', 'markdown', 95 | '-v', 96 | '-q', 97 | '-s', 98 | '-p', 'profile1,profile2', 99 | '-l', '1,2' 100 | ] 101 | ) 102 | end 103 | end 104 | 105 | it 'executes the "sce update-config" command with the help flag' do 106 | expect do 107 | output = capture_stdout { described_class.execute(['sce', 'update-config', '-h']) } 108 | expect(output).to match(/Developer tools for Abide/) 109 | end.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) } 110 | end 111 | 112 | it 'executes the "sce update-config from-diff" command and gets warning' do 113 | expect do 114 | described_class.execute(%w[sce update-config from-diff config_file curr_xccdf new_xccdf]) 115 | end.to output(/^This command is currently non-functional/).to_stderr 116 | end 117 | 118 | it 'executes the "sce validate" command with the help flag' do 119 | expect do 120 | output = capture_stdout { described_class.execute(['sce', 'validate', '-h']) } 121 | expect(output).to match(/Developer tools for Abide/) 122 | end.to raise_error(SystemExit) { |e| expect(e.status).to eq(0) } 123 | end 124 | 125 | it 'executes the "sce validate puppet-strings" command with options' do 126 | ret_val = { 127 | one: [{ errors: [], warnings: [] }], 128 | two: [{ errors: [], warnings: [] }], 129 | three: [{ errors: [], warnings: [1] }], 130 | four: [{ errors: [], warnings: [2] }], 131 | five: [{ errors: [], warnings: [] }] 132 | } 133 | allow(AbideDevUtils::Validate).to receive(:puppet_module_directory).and_return(nil) 134 | expect(AbideDevUtils::Sce::Validate::Strings).to receive(:validate) 135 | .with( 136 | { 137 | format: 'json', 138 | verbose: true, 139 | quiet: true, 140 | out_file: 'test_file', 141 | strict: true 142 | } 143 | ).and_return(ret_val) 144 | expect(AbideDevUtils::Output).to receive(:json).with( 145 | ret_val, 146 | console: false, 147 | file: 'test_file', 148 | stringify: true 149 | ) 150 | expect do 151 | capture_stdout_stderr do 152 | described_class.execute( 153 | [ 154 | 'sce', 155 | 'validate', 156 | 'puppet-strings', 157 | '-f', 'json', 158 | '-o', 'test_file', 159 | '-v', 160 | '-q', 161 | '-s' 162 | ] 163 | ) 164 | end 165 | end.to raise_error(SystemExit) { |e| expect(e.success?).to be_falsey } 166 | end 167 | end 168 | end 169 | # rubocop:enable RSpec/FilePath 170 | # rubocop:enable RSpec/MultipleExpectations 171 | # rubocop:enable RSpec/ExampleLength 172 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/new_obj.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'erb' 4 | require 'pathname' 5 | require 'abide_dev_utils/output' 6 | require 'abide_dev_utils/prompt' 7 | require 'abide_dev_utils/errors/ppt' 8 | 9 | module AbideDevUtils 10 | module Ppt 11 | class NewObjectBuilder 12 | DEFAULT_EXT = '.pp' 13 | VALID_EXT = /(\.pp|\.rb)\.erb$/.freeze 14 | TMPL_PATTERN = /^[a-zA-Z][^\s]*\.erb$/.freeze 15 | OBJ_PREFIX = /^(c-|d-)/.freeze 16 | PREFIX_TEST_PATH = { 'c-' => 'classes', 'd-' => 'defines' }.freeze 17 | 18 | def initialize(obj_type, obj_name, opts: {}, vars: {}) 19 | @obj_type = obj_type 20 | @obj_name = namespace_format(obj_name) 21 | @opts = opts 22 | @vars = vars 23 | class_vars 24 | validate_class_vars 25 | @tmpl_data = template_data(@opts.fetch(:tmpl_name, @obj_type)) 26 | end 27 | 28 | attr_reader :obj_type, :obj_name, :root_dir, :tmpl_dir, :obj_path, :vars, :tmpl_data 29 | 30 | def build 31 | force = @opts.fetch(:force, false) 32 | obj_cont = force ? true : continue?(obj_path) 33 | spec_cont = force ? true : continue?(@tmpl_data[:spec_path]) 34 | write_file(obj_path, @tmpl_data[:path]) if obj_cont 35 | write_file(@tmpl_data[:spec_path], @spec_tmpl) if spec_cont 36 | end 37 | 38 | # If a method gets called on the Hiera object which is not defined, 39 | # this sends that method call to hash, then doc, then super. 40 | def method_missing(method, *args, &block) 41 | return true if ['exist?', 'exists?'].include?(method.to_s) 42 | 43 | return @hash.send(method, *args, &block) if @hash.respond_to?(method) 44 | 45 | return @doc.send(method, *args, &block) if @doc.respond_to?(method) 46 | 47 | super(method, *args, &block) 48 | end 49 | 50 | # Checks the respond_to? of hash, doc, or super 51 | def respond_to_missing?(method_name, include_private = false) 52 | return true if ['exist?', 'exists?'].include?(method_name.to_s) 53 | 54 | @hash || @doc || super 55 | end 56 | 57 | private 58 | 59 | def continue?(path) 60 | continue = if File.exist?(path) 61 | AbideDevUtils::Prompt.yes_no('File exists, would you like to overwrite?') 62 | else 63 | true 64 | end 65 | AbideDevUtils::Output.simple("Not overwriting file #{path}") unless continue 66 | 67 | continue 68 | end 69 | 70 | def write_file(path, tmpl_path) 71 | dir, = Pathname.new(path).split 72 | Pathname.new(dir).mkpath unless Dir.exist?(dir) 73 | content = render(tmpl_path) 74 | File.open(path, 'w') { |f| f.write(content) } unless content.empty? 75 | raise AbideDevUtils::Errors::Ppt::FailedToCreateFileError, path unless File.file?(path) 76 | 77 | AbideDevUtils::Output.simple("Created file #{path}") 78 | end 79 | 80 | def build_obj; end 81 | 82 | def class_vars 83 | @root_dir = Pathname.new(@opts.fetch(:root_dir, Dir.pwd)) 84 | @tmpl_dir = if @opts.fetch(:absolute_template_dir, false) 85 | @opts.fetch(:tmpl_dir) 86 | else 87 | "#{@root_dir}/#{@opts.fetch(:tmpl_dir, 'object_templates')}" 88 | end 89 | @obj_path = new_obj_path 90 | @spec_tmpl = @opts.fetch(:spec_template, File.expand_path(File.join(__dir__, '../resources/generic_spec.erb'))) 91 | end 92 | 93 | def validate_class_vars 94 | raise AbideDevUtils::Errors::PathNotDirectoryError, @root_dir unless Dir.exist? @root_dir 95 | raise AbideDevUtils::Errors::PathNotDirectoryError, @tmpl_dir unless Dir.exist? @tmpl_dir 96 | end 97 | 98 | def basename(obj_name) 99 | obj_name.split('::')[-1] 100 | end 101 | 102 | def prefix 103 | pfx = basename.match(OBJ_PREFIX) 104 | return pfx[1] unless pfx.empty? 105 | end 106 | 107 | def templates 108 | return [] if Dir.entries(tmpl_dir).empty? 109 | 110 | file_names = Dir.entries(tmpl_dir).select { |f| f.match?(TMPL_PATTERN) } 111 | file_names.map { |i| File.join(tmpl_dir, i) } 112 | end 113 | 114 | def template_data(query) 115 | raise AbideDevUtils::Errors::Ppt::TemplateNotFoundError, @tmpl_dir if Dir.entries(@tmpl_dir).empty? 116 | 117 | data = {} 118 | pattern = /#{Regexp.quote(query)}/ 119 | templates.each do |i| 120 | pn = Pathname.new(i) 121 | next unless pn.basename.to_s.match?(pattern) 122 | 123 | data[:path] = pn.to_s 124 | data[:fname] = pn.basename.to_s 125 | end 126 | raise AbideDevUtils::Errors::Ppt::TemplateNotFoundError, @tmpl_dir unless data.key?(:fname) 127 | 128 | data[:ext] = data[:fname].match?(VALID_EXT) ? data[:fname].match(VALID_EXT)[1] : '.pp' 129 | data[:pfx] = data[:fname].match?(OBJ_PREFIX) ? data[:fname].match(OBJ_PREFIX)[1] : 'c-' 130 | data[:spec_base] = PREFIX_TEST_PATH[data[:pfx]] 131 | data[:obj_name] = normalize_obj_name(data.dup) 132 | data[:spec_name] = "#{@obj_name.split('::')[-1]}_spec.rb" 133 | data[:spec_path] = spec_path(data[:spec_base], data[:spec_name]) 134 | data 135 | end 136 | 137 | def normalize_obj_name(data) 138 | new_name = data[:fname].slice(/^(?:#{Regexp.quote(data[:pfx])})?(?[^\s.]+)(?:#{Regexp.quote(data[:ext])})?\.erb$/, 'name') 139 | "#{new_name}#{data[:ext]}" 140 | end 141 | 142 | def render(path) 143 | ERB.new(File.read(path), 0, '<>-').result(binding) 144 | end 145 | 146 | def namespace_format(name) 147 | name.split(':').reject(&:empty?).join('::') 148 | end 149 | 150 | def new_obj_path 151 | parts = @obj_name.split('::')[1..-2] 152 | parts.insert(0, 'manifests') 153 | parts.insert(-1, "#{basename(@obj_name)}#{DEFAULT_EXT}") 154 | path = @root_dir + Pathname.new(parts.join('/')) 155 | path.to_s 156 | end 157 | 158 | def spec_path(base_dir, spec_name) 159 | parts = @obj_name.split('::')[1..-2] 160 | parts.insert(0, 'spec') 161 | parts.insert(1, base_dir) 162 | parts.insert(-1, spec_name) 163 | path = @root_dir + Pathname.new(parts.join('/')) 164 | path.to_s 165 | end 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'output' 4 | require_relative 'validate' 5 | require_relative 'errors' 6 | require_relative 'ppt/api' 7 | require_relative 'ppt/code_gen' 8 | require_relative 'ppt/code_introspection' 9 | require_relative 'ppt/class_utils' 10 | require_relative 'ppt/facter_utils' 11 | require_relative 'ppt/hiera' 12 | require_relative 'ppt/puppet_module' 13 | 14 | module AbideDevUtils 15 | module Ppt 16 | # Renames a Puppet class by renaming the class declaration and class file 17 | # @param from [String] fully-namespaced existing Puppet class name 18 | # @param to [String] fully-namespaced new Puppet class name 19 | def self.rename_puppet_class(from, to, **kwargs) 20 | from_path = ClassUtils.path_from_class_name(from) 21 | to_path = ClassUtils.path_from_class_name(to) 22 | file_path = kwargs.fetch(:declaration_in_to_file, false) ? to_path : from_path 23 | raise ClassFileNotFoundError, "Path:#{file_path}" if !File.file?(file_path) && kwargs.fetch(:validate_path, true) 24 | 25 | rename_puppet_class_declaration(from, to, file_path, **kwargs) 26 | AbideDevUtils::Output.simple("Renamed #{from} to #{to} at #{file_path}.") 27 | return unless kwargs.fetch(:declaration_only, false) 28 | 29 | rename_class_file(from_path, to_path, **kwargs) 30 | AbideDevUtils::Output.simple("Renamed file #{from_path} to #{to_path}.") 31 | end 32 | 33 | def self.audit_class_names(dir, **kwargs) 34 | mismatched = ClassUtils.find_all_mismatched_class_declarations(dir) 35 | outfile = kwargs.key?(:file) ? File.open(kwargs[:file], 'a') : nil 36 | quiet = kwargs.fetch(:quiet, false) 37 | mismatched.each do |class_file| 38 | AbideDevUtils::Output.simple("Mismatched class name in file #{class_file}") unless quiet 39 | outfile << "MISMATCHED_CLASS_NAME: #{class_file}\n" unless outfile.nil? 40 | end 41 | outfile&.close 42 | AbideDevUtils::Output.simple("Found #{mismatched.length} mismatched classes in #{dir}.") unless quiet 43 | ensure 44 | outfile&.close 45 | end 46 | 47 | def self.fix_class_names_file_rename(dir, **kwargs) 48 | mismatched = ClassUtils.find_all_mismatched_class_declarations(dir) 49 | progress = AbideDevUtils::Output.progress(title: 'Renaming files', total: mismatched.length) 50 | mismatched.each do |class_path| 51 | should = ClassUtils.path_from_class_name(class_name_from_declaration(class_path)) 52 | ClassUtils.rename_class_file(class_path, should, **kwargs) 53 | progress.increment 54 | AbideDevUtils::Output.simple("Renamed file #{class_path} to #{should}...") if kwargs.fetch(:verbose, false) 55 | end 56 | AbideDevUtils::Output.simple('Successfully fixed all classes.') 57 | end 58 | 59 | def self.fix_class_names_class_rename(dir, **kwargs) 60 | mismatched = ClassUtils.find_all_mismatched_class_declarations(dir) 61 | progress = AbideDevUtils::Output.progress(title: 'Renaming classes', total: mismatched.length) 62 | mismatched.each do |class_path| 63 | current = ClassUtils.class_name_from_declaration(class_path) 64 | should = ClassUtils.class_name_from_path(class_path) 65 | ClassUtils.rename_puppet_class_declaration(current, should, class_path, **kwargs) 66 | progress.increment 67 | AbideDevUtils::Output.simple("Renamed #{from} to #{to} at #{file_path}...") if kwargs.fetch(:verbose, false) 68 | end 69 | AbideDevUtils::Output.simple('Successfully fixed all classes.') 70 | end 71 | 72 | def self.build_new_object(type, name, opts) 73 | require 'abide_dev_utils/ppt/new_obj' 74 | AbideDevUtils::Ppt::NewObjectBuilder.new( 75 | type, 76 | name, 77 | opts: opts, 78 | vars: opts.fetch(:vars, '').split(',').map { |i| i.split('=') }.to_h # makes the str a hash 79 | ).build 80 | end 81 | 82 | def self.add_cis_comment(path, xccdf, number_format: false) 83 | require 'abide_dev_utils/xccdf' 84 | 85 | parsed_xccdf = AbideDevUtils::XCCDF::Benchmark.new(xccdf) 86 | return add_cis_comment_to_all(path, parsed_xccdf, number_format: number_format) if File.directory?(path) 87 | return add_cis_comment_to_single(path, parsed_xccdf, number_format: number_format) if File.file?(path) 88 | 89 | raise AbideDevUtils::Errors::FileNotFoundError, path 90 | end 91 | 92 | def self.add_cis_comment_to_single(path, xccdf, number_format: false) 93 | write_cis_comment_to_file( 94 | path, 95 | cis_recommendation_comment( 96 | path, 97 | xccdf, 98 | number_format 99 | ) 100 | ) 101 | end 102 | 103 | def self.add_cis_comment_to_all(path, xccdf, number_format: false) 104 | comments = {} 105 | Dir[File.join(path, '*.pp')].each do |puppet_file| 106 | comment = cis_recommendation_comment(puppet_file, xccdf, number_format) 107 | comments[puppet_file] = comment unless comment.nil? 108 | end 109 | comments.each do |key, value| 110 | write_cis_comment_to_file(key, value) 111 | end 112 | AbideDevUtils::Output.simple('Successfully added comments.') 113 | end 114 | 115 | def self.write_cis_comment_to_file(path, comment) 116 | require 'tempfile' 117 | tempfile = Tempfile.new 118 | begin 119 | File.open(tempfile, 'w') do |nf| 120 | nf.write("#{comment}\n") 121 | File.foreach(path) do |line| 122 | nf.write(line) unless line == "#{comment}\n" 123 | end 124 | end 125 | File.rename(path, "#{path}.old") 126 | tempfile.close 127 | File.rename(tempfile.path, path) 128 | File.delete("#{path}.old") 129 | AbideDevUtils::Output.simple("Added CIS recomendation comment to #{path}...") 130 | ensure 131 | tempfile.close 132 | tempfile.unlink 133 | end 134 | end 135 | 136 | def self.cis_recommendation_comment(puppet_file, xccdf, number_format) 137 | _, control = xccdf.find_cis_recommendation( 138 | File.basename(puppet_file, '.pp'), 139 | number_format: number_format 140 | ) 141 | if control.nil? 142 | AbideDevUtils::Output.simple("Could not find recommendation text for #{puppet_file}...") 143 | return nil 144 | end 145 | control_title = xccdf.resolve_control_reference(control).xpath('./xccdf:title').text 146 | "# #{control_title}" 147 | end 148 | 149 | def self.score_module(module_path, outfile: nil, quiet: false, checks: ['all'], **_) 150 | AbideDevUtils::Output.simple 'This command is not currently implemented' 151 | # require 'abide_dev_utils/ppt/score_module' 152 | # score = {} 153 | # score[:lint_check] = ScoreModule.lint if checks.include?('all') || checks.include?('lint') 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /lib/abide_dev_utils/ppt/api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'io/console' 4 | require 'json' 5 | require 'net/http' 6 | require 'openssl' 7 | 8 | module AbideDevUtils 9 | module Ppt 10 | class ApiClient 11 | attr_reader :hostname, :custom_ports 12 | attr_writer :auth_token, :tls_cert_verify 13 | attr_accessor :content_type 14 | 15 | CT_JSON = 'application/json' 16 | API_DEFS = { 17 | codemanager: { 18 | port: 8170, 19 | version: 'v1', 20 | base: 'code-manager', 21 | paths: [ 22 | { 23 | path: 'deploys', 24 | verbs: %w[post], 25 | x_auth: true 26 | } 27 | ] 28 | }, 29 | classifier1: { 30 | port: 4433, 31 | version: 'v1', 32 | base: 'classifier-api', 33 | paths: [ 34 | { 35 | path: 'groups', 36 | verbs: %w[get post], 37 | x_auth: true 38 | } 39 | ] 40 | }, 41 | orchestrator: { 42 | port: 8143, 43 | version: 'v1', 44 | base: 'orchestrator', 45 | paths: [ 46 | { 47 | path: 'command/deploy', 48 | verbs: %w[post], 49 | x_auth: true 50 | }, 51 | { 52 | path: 'command/task', 53 | verbs: %w[post], 54 | x_auth: true 55 | }, 56 | { 57 | path: 'jobs', 58 | verbs: %w[get], 59 | x_auth: true 60 | } 61 | ] 62 | } 63 | }.freeze 64 | 65 | def initialize(hostname, auth_token: nil, content_type: CT_JSON, custom_ports: {}, verbose: false) 66 | @hostname = hostname 67 | @auth_token = auth_token 68 | @content_type = content_type 69 | @custom_ports = custom_ports 70 | @verbose = verbose 71 | define_api_methods 72 | end 73 | 74 | def login(username, password: nil, lifetime: '1h', label: nil) 75 | label = "AbideDevUtils token for #{username} - lifetime #{lifetime}" if label.nil? 76 | password = IO.console.getpass 'Password: ' if password.nil? 77 | data = { 78 | 'login' => username, 79 | 'password' => password, 80 | 'lifetime' => lifetime, 81 | 'label' => label 82 | } 83 | uri = URI("https://#{@hostname}:4433/rbac-api/v1/auth/token") 84 | result = http_request(uri, post_request(uri, x_auth: false, **data), json_out: true) 85 | @auth_token = result['token'] 86 | log_verbose("Successfully logged in? #{auth_token?}") 87 | auth_token? 88 | end 89 | 90 | def auth_token? 91 | defined?(@auth_token) && !@auth_token.nil? && !@auth_token.empty? 92 | end 93 | 94 | def tls_cert_verify 95 | @tls_cert_verify = defined?(@tls_cert_verify) ? @tls_cert_verify : false 96 | end 97 | 98 | def verbose? 99 | @verbose 100 | end 101 | 102 | def no_verbose 103 | @verbose = false 104 | end 105 | 106 | def verbose! 107 | @verbose = true 108 | end 109 | 110 | private 111 | 112 | def define_api_methods 113 | api_method_data.each do |meth, data| 114 | case meth 115 | when /^get_.*/ 116 | self.class.define_method(meth) do |*args, **kwargs| 117 | uri = args.empty? ? data[:uri] : URI("#{data[:uri]}/#{args.join('/')}") 118 | req = get_request(uri, x_auth: data[:x_auth], **kwargs) 119 | http_request(data[:uri], req, json_out: true) 120 | end 121 | when /^post_.*/ 122 | self.class.define_method(meth) do |*args, **kwargs| 123 | uri = args.empty? ? data[:uri] : URI("#{data[:uri]}/#{args.join('/')}") 124 | req = post_request(uri, x_auth: data[:x_auth], **kwargs) 125 | http_request(data[:uri], req, json_out: true) 126 | end 127 | else 128 | raise "Cannot define method for #{meth}" 129 | end 130 | end 131 | end 132 | 133 | def api_method_data 134 | method_data = {} 135 | API_DEFS.each do |key, val| 136 | val[:paths].each do |path| 137 | method_names = api_method_names(key, path) 138 | method_names.each do |name| 139 | method_data[name] = { 140 | uri: api_method_uri(val[:port], val[:base], val[:version], path[:path]), 141 | x_auth: path[:x_auth] 142 | } 143 | end 144 | end 145 | end 146 | method_data 147 | end 148 | 149 | def api_method_names(api_name, path) 150 | path[:verbs].each_with_object([]) do |verb, ary| 151 | path_str = path[:path].split('/').join('_') 152 | ary << [verb, api_name.to_s, path_str].join('_') 153 | end 154 | end 155 | 156 | def api_method_uri(port, base, version, path) 157 | URI("https://#{@hostname}:#{port}/#{base}/#{version}/#{path}") 158 | end 159 | 160 | def get_request(uri, x_auth: true, **qparams) 161 | log_verbose('New GET request:') 162 | log_verbose("request_qparams?: #{!qparams.empty?}") 163 | uri.query = URI.encode_www_form(qparams) unless qparams.empty? 164 | headers = init_headers(x_auth: x_auth) 165 | log_verbose("request_headers: #{redact_headers(headers)}") 166 | Net::HTTP::Get.new(uri, headers) 167 | end 168 | 169 | def post_request(uri, x_auth: true, **data) 170 | log_verbose('New POST request:') 171 | log_verbose("request_data?: #{!data.empty?}") 172 | headers = init_headers(x_auth: x_auth) 173 | log_verbose("request_headers: #{redact_headers(headers)}") 174 | req = Net::HTTP::Post.new(uri, headers) 175 | req.body = data.to_json unless data.empty? 176 | req 177 | end 178 | 179 | def init_headers(x_auth: true) 180 | headers = { 'Content-Type' => @content_type } 181 | return headers unless x_auth 182 | 183 | raise 'Auth token not set!' unless auth_token? 184 | 185 | headers['X-Authentication'] = @auth_token 186 | headers 187 | end 188 | 189 | def http_request(uri, req, json_out: true) 190 | result = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, verify_mode: tls_verify_mode) do |http| 191 | log_verbose("use_ssl: true, verify_mode: #{tls_verify_mode}") 192 | http.request(req) 193 | end 194 | case result.code 195 | when '200', '201', '202' 196 | json_out ? JSON.parse(result.body) : result 197 | else 198 | jbody = JSON.parse(result.body) 199 | log_verbose("HTTP #{result.code} #{jbody['kind']} #{jbody['msg']} #{jbody['details']} #{uri}") 200 | raise "HTTP #{result.code} #{jbody['kind']} #{jbody['msg']} #{jbody['details']} #{uri}" 201 | end 202 | end 203 | 204 | def log_verbose(msg) 205 | puts msg if @verbose 206 | end 207 | 208 | def redact_headers(headers) 209 | r_headers = headers.dup 210 | r_headers['X-Authentication'] = 'XXXXX' if r_headers.key?('X-Authentication') 211 | r_headers 212 | end 213 | 214 | def tls_verify_mode 215 | tls_cert_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE 216 | end 217 | end 218 | end 219 | end 220 | --------------------------------------------------------------------------------