inspec/lib/utils/parser.rb

238 lines
7.1 KiB
Ruby
Raw Normal View History

2015-10-04 15:59:13 +00:00
# encoding: utf-8
2015-10-06 16:55:44 +00:00
# author: Christoph Hartmann
# author: Dominik Richter
2015-10-04 15:59:13 +00:00
2015-12-31 00:01:11 +00:00
module PasswdParser
# Parse /etc/passwd files.
#
# @param [String] content the raw content of /etc/passwd
# @return [Array] Collection of passwd entries
2015-10-04 15:59:13 +00:00
def parse_passwd(content)
2015-10-26 14:50:57 +00:00
content.to_s.split("\n").map do |line|
next if line[0] == '#'
2015-10-04 15:59:13 +00:00
parse_passwd_line(line)
end.compact
2015-10-04 15:59:13 +00:00
end
# Parse a line of /etc/passwd
#
# @param [String] line a line of /etc/passwd
# @return [Hash] Map of entries in this line
def parse_passwd_line(line)
x = line.split(':')
{
'user' => x.at(0),
'password' => x.at(1),
'uid' => x.at(2),
'gid' => x.at(3),
'desc' => x.at(4),
'home' => x.at(5),
'shell' => x.at(6),
}
end
2015-12-31 00:01:11 +00:00
end
2015-12-31 00:01:11 +00:00
module CommentParser
# Parse a line with a command. For example: `a = b # comment`.
# Retrieves the actual content.
#
# @param [String] raw the content lines you want to be parsed
# @param [Hash] opts optional configuration
# @return [Array] contains the actual line and the position of the line end
2015-10-06 11:50:25 +00:00
def parse_comment_line(raw, opts)
idx_nl = raw.index("\n")
idx_comment = raw.index(opts[:comment_char])
idx_nl = raw.length if idx_nl.nil?
idx_comment = idx_nl + 1 if idx_comment.nil?
line = ''
# is a comment inside this line
if idx_comment < idx_nl && idx_comment != 0
line = raw[0..(idx_comment - 1)]
# in case we don't allow comments at the end
# of an assignment/statement, ignore it and fall
# back to treating this as a regular line
if opts[:standalone_comments] && !is_empty_line(line)
line = raw[0..(idx_nl - 1)]
end
# if there is no comment in this line
elsif idx_comment > idx_nl && idx_nl != 0
line = raw[0..(idx_nl - 1)]
end
[line, idx_nl]
end
2015-10-04 15:59:13 +00:00
end
module MountParser
# this parses the output of mount command (only tested on linux)
# this method expects only one line of the mount output
def parse_mount_options(mount_line, compatibility = false)
mount = mount_line.scan(/\S+/)
# parse device and type
mount_options = { device: mount[0], type: mount[4] }
if compatibility == false
# parse options as array
mount_options[:options] = mount[5].gsub(/\(|\)/, '').split(',')
else
# parse options as serverspec uses it, tbis is deprecated
mount_options[:options] = {}
mount[5].gsub(/\(|\)/, '').split(',').each do |option|
name, val = option.split('=')
if val.nil?
val = true
2016-01-15 02:59:00 +00:00
elsif val =~ /^\d+$/
# parse numbers
2016-01-15 02:59:00 +00:00
val = val.to_i
end
mount_options[:options][name.to_sym] = val
end
end
mount_options
end
end
module SolarisNetstatParser
# takes this as a input and parses the values
# UDP: IPv4
# Local Address Remote Address State
# -------------------- -------------------- ----------
# *.* Unbound
def parse_netstat(content) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
return [] if content.nil? || content.empty?
protocol = nil
column_widths = nil
ports = []
cache_name_line = nil
content.each_line { |line|
# find header, its delimiter
if line =~ /TCP:|UDP:|SCTP:/
# get protocol
protocol = line.split(':')[0].chomp.strip.downcase
# determine version tcp, tcp6, udp, udp6
proto_version = line.split(':')[1].chomp.strip
protocol += '6' if proto_version == 'IPv6'
# reset names cache
column_widths = nil
cache_name_line = nil
names = nil
# calulate width of a column based on the horizontal line
elsif line =~ /^[- ]+$/
column_widths = columns(line)
# parse header values from line
elsif column_widths.nil? && !line.nil?
# we do not know the width at this point of time, therefore we need to cache
cache_name_line = line
# content line
elsif !column_widths.nil? && !line.nil? && !line.chomp.empty?
# default row
port = split_columns(column_widths, line).to_a.map { |v| v.chomp.strip }
# parse the header names
# TODO: names should be optional
names = split_columns(column_widths, cache_name_line).to_a.map { |v| v.chomp.strip.downcase.tr(' ', '-').gsub(/[^\w-]/, '_') }
info = {
'protocol' => protocol.downcase,
}
# generate hash for each line and use the names as keys
names.each_index { |i|
info[names[i]] = port[i] if i != 0
}
ports.push(info)
end
}
ports
end
private
# takes a line like "-------------------- -------------------- ----------"
# as input and calculates the length of each column
def columns(line)
# find all columns
m = line.scan(/-+/)
# calculate the length each column
m.map { |x| x.length } # rubocop:disable Style/SymbolProc
end
# takes a line and the width of the columns to extract the values
def split_columns(columns, line)
# generate regex based on columns
sep = '\\s'
length = columns.length
arr = columns.map.with_index { |x, i|
reg = "(.{#{x}})#{sep}" # add seperator between columns
reg = "(.{,#{x}})#{sep}" if i == length - 2 # make the pre-last one optional
reg = "(.{,#{x}})" if i == length - 1 # use , to say max value
reg
}
# extracts the columns
line.match(Regexp.new(arr.join))
end
end
2016-02-26 12:19:16 +00:00
# This parser for xinetd (extended Internet daemon) configuration files
2016-02-26 12:19:16 +00:00
module XinetdParser
def xinetd_include_dir(dir)
return [] if dir.nil?
unless inspec.file(dir).directory?
return skip_resource "Cannot read folder in #{dir}"
end
files = inspec.command("find #{dir} -type f").stdout.split("\n")
files.map { |file| parse_xinetd(read_content(file)) }
end
def parse_xinetd(raw) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
return {} if raw.nil?
res = {}
cur_group = nil
simple_conf = []
rest = raw
until rest.empty?
# extract content line
2016-02-26 12:19:16 +00:00
nl = rest.index("\n") || (rest.length-1)
comment = rest.index('#') || (rest.length-1)
dst_idx = (comment < nl) ? comment : nl
inner_line = (dst_idx == 0) ? '' : rest[0..dst_idx-1].strip
# update unparsed content
2016-02-26 12:19:16 +00:00
rest = rest[nl+1..-1]
next if inner_line.empty?
if inner_line == '}'
res[cur_group] = SimpleConfig.new(simple_conf.join("\n"))
cur_group = nil
elsif rest.lstrip[0] == '{'
cur_group = inner_line
simple_conf = []
rest = rest[rest.index("\n")+1..-1]
elsif cur_group.nil?
# parse all included files
2016-02-26 12:19:16 +00:00
others = xinetd_include_dir(inner_line[/includedir (.+)/, 1])
# complex merging of included configurations, as multiple services
# may be defined with the same name but different configuration
others.each { |ores|
ores.each { |k, v|
res[k] ||= []
res[k].push(v)
}
}
else
simple_conf.push(inner_line)
end
end
res
end
end