Class: InspecPlugins::Sign::Base

Inherits:
Object
  • Object
show all
Includes:
Inspec::Dist
Defined in:
lib/plugins/inspec-sign/lib/inspec-sign/base.rb

Constant Summary collapse

KEY_BITS =
2048
KEY_ALG =
OpenSSL::PKey::RSA
INSPEC_PROFILE_VERSION_1 =
"INSPEC-PROFILE-1".freeze
INSPEC_REPORT_VERSION_1 =
"INSPEC-REPORT-1".freeze
INSPEC_PROFILE_VERSION_2 =
"INSPEC-PROFILE-2".freeze
ARTIFACT_DIGEST =
OpenSSL::Digest::SHA512
ARTIFACT_DIGEST_NAME =
"SHA512".freeze
VALID_PROFILE_VERSIONS =
Set.new [INSPEC_PROFILE_VERSION_1, INSPEC_PROFILE_VERSION_2]
VALID_PROFILE_DIGESTS =
Set.new [ARTIFACT_DIGEST_NAME]
SIGNED_PROFILE_SUFFIX =
"iaf".freeze
SIGNED_REPORT_SUFFIX =
"iar".freeze

Constants included from Inspec::Dist

Inspec::Dist::AUTOMATE_PRODUCT_NAME, Inspec::Dist::COMPLIANCE_PRODUCT_NAME, Inspec::Dist::EXEC_NAME, Inspec::Dist::PRODUCT_NAME, Inspec::Dist::SERVER_PRODUCT_NAME

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.keygen(options) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/plugins/inspec-sign/lib/inspec-sign/base.rb', line 32

def self.keygen(options)
  key = KEY_ALG.new KEY_BITS

  # config_dir is the directory where the keys will be stored.
  # options["config_dir"] is passed explicitly only for testing purposes.
  config_dir = options["config_dir"] || Inspec.config_dir
  path = File.join(config_dir, "keys")
  FileUtils.mkdir_p(path)

  puts "Generating signing key in #{path}/#{options["keyname"]}.pem.key"
  # https://github.com/inspec/inspec/security/code-scanning/1
  # https://github.com/inspec/inspec/security/code-scanning/2
  # The following line was flagged by GitHub code scanning as a security vulnerability.
  # Update the code to eliminate the vulnerability.
  File.open("#{path}/#{options["keyname"]}.pem.key", "w") do |io|
    io.write key.to_pem
  end
  puts "Generating validation key in #{path}/#{options["keyname"]}.pem.pub"
  File.open("#{path}/#{options["keyname"]}.pem.pub", "w") do |io|
    io.write key.public_key.to_pem
  end
end

.profile_sign(profile_path, options) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/plugins/inspec-sign/lib/inspec-sign/base.rb', line 55

def self.profile_sign(profile_path, options)
  artifact = new

  # Writes the profile content id in the inspec.yml
  if options[:profile_content_id] && !options[:profile_content_id].strip.empty?
    artifact.write_profile_content_id(profile_path, options[:profile_content_id])
  end

  puts "Signing #{profile_path} with key #{options["keyname"]}"
  keypath = Inspec::IafFile.find_signing_key(options["keyname"], options["config_dir"])

  # Read name and version from metadata and use them to form the filename
  profile_md = artifact.(profile_path)

  # Behave same as archive filename for iaf filename
  slug = profile_md["name"].downcase.strip.tr(" ", "-").gsub(/[^\w-]/, "_")
  filename = "#{slug}-#{profile_md["version"]}"
  artifact_filename = "#{filename}.#{SIGNED_PROFILE_SUFFIX}"

  # Generating tar.gz file using archive method of Inspec Cli
  Inspec::InspecCLI.new.archive(profile_path, "error")
  tarfile = "#{filename}.tar.gz"
  # Update IO.binread with File.binread because of https://github.com/inspec/inspec/security/code-scanning/3
  tar_content = File.binread(tarfile)
  FileUtils.rm(tarfile)

  # Generate the signature
  signing_key = KEY_ALG.new File.read keypath
  sha = ARTIFACT_DIGEST.new
  signature = signing_key.sign sha, tar_content
  # convert the signature to Base64
  signature_base64 = Base64.encode64(signature)

  content = (format("%-100s", options[:keyname]) +
            format("%-20s", ARTIFACT_DIGEST_NAME) +
            format("%-370s", signature_base64)).gsub(" ", "\0").unpack("H*").pack("h*") + "#{tar_content}"

  File.open(artifact_filename, "wb") do |f|
    f.puts INSPEC_PROFILE_VERSION_2
    f.puts "Use \"inspec export\" to view this file"
    f.write(content)
  end
  puts "Successfully generated #{artifact_filename}"
rescue Inspec::Exceptions::ProfileValidationKeyNotFound => e
  $stderr.puts e.message
  Inspec::UI.new.exit(:usage_error)
end

.profile_verify(signed_profile_path, silent = false) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/plugins/inspec-sign/lib/inspec-sign/base.rb', line 103

def self.profile_verify(signed_profile_path, silent = false)
  file_to_verify = signed_profile_path
  puts "Verifying #{file_to_verify}" unless silent

  iaf_file = Inspec::IafFile.new(file_to_verify)
  if iaf_file.valid?
    # Signed profile verification is called from runner and not from CLI
    # Do not exit and do not print logs
    return if silent

    puts "Detected format version '#{iaf_file.version}'"
    puts "Attempting to verify using key '#{iaf_file.key_name}'"
    puts "Profile is valid."
    Inspec::UI.new.exit(:normal)
  else
    puts "Detected format version '#{iaf_file.version}'"
    puts "Attempting to verify using key '#{iaf_file.key_name}'" if iaf_file.key_name
    puts "Profile is invalid"
    Inspec::UI.new.exit(:bad_signature)
  end
rescue Inspec::Exceptions::ProfileValidationKeyNotFound => e
  $stderr.puts e.message
  Inspec::UI.new.exit(:usage_error)
end

Instance Method Details

#read_profile_metadata(profile_path) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/plugins/inspec-sign/lib/inspec-sign/base.rb', line 128

def (profile_path)
  begin
    p = Pathname.new(profile_path)
    p = p.join("inspec.yml")
    unless p.exist?
      raise "#{profile_path} doesn't appear to be a valid #{PRODUCT_NAME} profile"
    end

    yaml = YAML.load_file(p.to_s)
    yaml = yaml.to_hash

    unless yaml.key? "name"
      raise "Profile is invalid, name is not defined"
    end

    unless yaml.key? "version"
      raise "Profile is invalid, version is not defined"
    end
  rescue => e
    # rewrap it and pass it up to the CLI
    $stderr.puts "Error reading profile metadata file #{e.message}"
    Inspec::UI.new.exit(:usage_error)
  end

  yaml
end

#write_profile_content_id(profile_path, profile_content_id) ⇒ Object



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/plugins/inspec-sign/lib/inspec-sign/base.rb', line 155

def write_profile_content_id(profile_path, profile_content_id)
  p = Pathname.new(profile_path)
  p = p.join("inspec.yml")
  yaml = YAML.load_file(p.to_s)
  existing_profile_content_id = yaml["profile_content_id"]

  unless existing_profile_content_id.nil?
    ui = Inspec::UI.new
    ui.error("Cannot use --profile-content-id when profile_content_id already exists in metadata file.")
    ui.exit(:usage_error)
  end

  lines = File.readlines(p)
  lines << "\nprofile_content_id: #{profile_content_id}\n"

  File.open("#{p}", "w" ) do |f|
    f.puts lines
  end
end