File generate_spec.rb of Package smar-apparmor-profiles
#!/usr/bin/env ruby
# Generates spec so manual repetition is not necessary
require "fileutils"
require "pathname"
require "yaml"
require_relative "systemd_override"
def set_log_level(&block)
# Set to true to enable trace messages.
trace = false
#trace = true
# Set to true to enable debug messages.
debug = false
debug = true if trace
#debug = true
block.call trace, debug
end
set_log_level do |trace, debug|
TRACE = trace
DEBUG = debug
end
def verb(message)
puts "[INFO] #{message}"
end
def debug(message)
puts "[DEBUG] #{message}" if DEBUG
end
def trace(message)
puts "[TRACE] #{message}" if TRACE
end
class GenerateSpec
INSTALL_RULES_TARGET_LINE = "___INSTALL_RULES_HERE___"
PACKAGES_TARGET_LINE = "___PACKAGES_HERE___"
INPUT_FILE = "smar-apparmor-profiles.spec.in"
OUTPUT_FILE = "smar-apparmor-profiles.spec"
def initialize
@packages = load_packages
@systemd_override = SystemdOverride.new
end
def generate
install_line_rows = generate_install_lines
package_rows = generate_package_rows
lines = File.readlines INPUT_FILE
lines.each_with_index do |line, line_index|
line.strip!
if line == INSTALL_RULES_TARGET_LINE
# Replace target first to avoid infinite loop.
lines[line_index] = "## Generated install commands starts."
lines[line_index+=1] = ""
install_line_rows.each_with_index do |row, row_index|
lines.insert((line_index + row_index), row)
end
end
end
# Loop twice so that line indices are correct.
lines.each_with_index do |line, line_index|
line.strip!
if line == PACKAGES_TARGET_LINE
# Replace target first to avoid infinite loop.
lines[line_index] = "## Generated packages starts."
lines[line_index+=1] = ""
package_rows.each_with_index do |package, index|
trace "index: #{index}, package: “#{package}”"
lines.insert((line_index + index), package)
# Append newline after package rows.
line_index += 1
lines.insert((line_index + index), "\n")
end
end
end
File.open OUTPUT_FILE, "w" do |file|
file.puts lines
end
end
def finalize
@systemd_override.finalize
end
private def generate_install_lines
out = []
@packages.each do |package|
definition = generate_install_line package
out << definition.join("\n")
end
out
end
private def generate_install_line package
trace "Generating install line for #{package}"
definition = []
package["files"]&.each do |file|
definition << "mv profiles/#{file} %{buildroot}%{_sysconfdir}/apparmor.d"
end
package["local"]&.each do |file|
definition << "mv profiles/local/#{file} %{buildroot}%{_sysconfdir}/apparmor.d/local"
end
package["namespaces"]&.each do |file|
unless File.symlink? file
definition << "mv namespaces/#{file} %{buildroot}%{_sysconfdir}/apparmor.d/namespaces.d"
else
# If a file is a symlink, it should be added manually
# by the package where it is a symlink to.
warn "File #{file} needs to be owned manually (1)."
end
end
package["namespace_directories"]&.each do |directory|
definition << "mv namespaces/#{directory}/ %{buildroot}%{_sysconfdir}/apparmor.d/namespaces.d"
end
package["included_abstractions"]&.each do |file|
definition << "mv abstractions/#{file} %{buildroot}%{_sysconfdir}/apparmor.d/abstractions"
end
package["included_tunables"]&.each do |file|
definition << "mv tunables/#{file} %{buildroot}%{_sysconfdir}/apparmor.d/tunables"
end
package["extra_files"]&.each do |directory, files|
extra_files_line definition, directory, files
end
package["load_profile_by_systemd"]&.each do |service_name, profile_name|
@systemd_override.install_lines_for_load_profile_by_systemd definition, package, service_name, profile_name
end
package["in_directory"]&.each do |target, data|
in_directory_lines definition, target, data, parent_package: package
end
definition
end
private def in_directory_lines definition, target, package, parent_package:
unless package.respond_to? :each_pair
STDERR.puts "Syntax error in a YAML config."
STDERR.puts "Package “#{parent_package["name"]}” has invalid content in block “in_directory”:"
STDERR.puts
STDERR.puts package.inspect
STDERR.puts
STDERR.puts "Maybe you’re missing “files:” keyword to denote a hash?"
exit 20
end
package["files"]&.each do |file|
definition << "mv profiles/#{target}/#{file} %{buildroot}%{_sysconfdir}/apparmor.d"
end
package["local"]&.each do |file|
definition << "mv profiles/#{target}/local/#{file} %{buildroot}%{_sysconfdir}/apparmor.d/local"
end
package["included_abstractions"]&.each do |file|
definition << "mv profiles/#{target}/abstractions/#{file} %{buildroot}%{_sysconfdir}/apparmor.d/abstractions"
end
package["included_tunables"]&.each do |file|
definition << "mv profiles/#{target}/tunables/#{file} %{buildroot}%{_sysconfdir}/apparmor.d/tunables"
end
if package["extra_files"]
extra_files_line definition, "profiles/#{target}", package["extra_files"]
end
if package["rpm_scriptlets_symlinks"]
rpm_scriptlets_symlinks definition, package["rpm_scriptlets_symlinks"]
end
end
private def extra_files_line definition, directory, files
case directory
when "ssh"
directory = "profiles/security/ssh"
files.each do |file|
destination_dir = File.dirname file
definition << "mkdir -p '%{buildroot}%{_sysconfdir}/apparmor.d/#{destination_dir}'"
definition << "mv #{directory}/#{file} '%{buildroot}%{_sysconfdir}/apparmor.d/#{destination_dir}'"
end
when "gcc"
directory = "profiles/compilation"
files.each do |file|
destination_dir = File.dirname file
definition << "mkdir -p '%{buildroot}%{_sysconfdir}/apparmor.d/#{destination_dir}'"
definition << "mv #{directory}/#{file} '%{buildroot}%{_sysconfdir}/apparmor.d/#{destination_dir}'"
end
when "shells"
directory = "profiles/system/shells"
files.each do |file|
destination_dir = File.dirname file
definition << "mkdir -p '%{buildroot}%{_sysconfdir}/apparmor.d/#{destination_dir}'"
definition << "mv #{directory}/#{file} '%{buildroot}%{_sysconfdir}/apparmor.d/#{destination_dir}'"
end
else
add_lines = lambda do |file|
if file.respond_to? :to_hash
# Special case.
unless file.size == 1
raise "Unrecognized hash value: #{file.inspect}"
end
source_file, target_file = file.each_pair.next
destination_dir = File.dirname target_file
definition << "mkdir -p '%{buildroot}#{destination_dir}'"
definition << "mv #{directory}/#{source_file} '%{buildroot}#{target_file}'"
else
# Normal case.
destination_dir = File.dirname file
definition << "mkdir -p '%{buildroot}%{_sysconfdir}/apparmor.d/#{destination_dir}'"
definition << "mv #{directory}/#{file} '%{buildroot}%{_sysconfdir}/apparmor.d/#{destination_dir}'"
end
end
# This check allows similar style to what in_directory
# block’s extra_files allows.
if files.nil?
file = directory
directory = "." # Root of the repository.
add_lines.call file
return
end
files.each do |file|
add_lines.call file
end
end
end
private def rpm_scriptlets_symlinks definition, files
files.each do |file|
rpm_scriptlets_d = "%{buildroot}%{_sysconfdir}/apparmor.d/namespaces.d/rpm-scriptlets.d"
profile_name = File.basename file
definition << %{ln -t '#{rpm_scriptlets_d}' -s '../../#{profile_name}'}
end
end
# Generates the package rows that are inserted to template spec.
private def generate_package_rows
out = []
@packages.each do |package|
definition = <<~EOF
# #{package["name"]}
%package -n #{package["name"]}-profiles
Summary: AppArmor profiles for #{package["name"]}
Supplements: #{package["name"]}
BuildArch: noarch
EOF
package["provides"]&.each do |provided|
definition += "Provides: #{provided}-profiles\n"
end
package["abstractions"]&.each do |abstraction|
definition += "Requires: smar-apparmor-profiles-#{abstraction}-abstractions\n"
end
package["supplements"]&.each do |supplement|
definition += "Supplements: #{supplement}\n"
end
package["requires"]&.each do |required|
name = "#{required}-profiles"
name = "smar-apparmor-profiles-common" if required == "common"
definition += "Requires: #{name}\n"
end
package["recommends"]&.each do |recommended|
name = "#{recommended}-profiles"
name = "smar-apparmor-profiles-common" if recommended == "common"
definition += "Recommends: #{name}\n"
end
package["suggests"]&.each do |suggested|
name = "#{suggested}-profiles"
name = "smar-apparmor-profiles-common" if suggested == "common"
definition += "Suggests: #{name}\n"
end
file_list_clause = %|%files -n #{package["name"]}-profiles|
if package["namespace_directories"] && !package["namespace_directories"].empty?
raise "This is for now only hacked for rpm-scriptlets. I think." unless package["name"] == "rpm"
file_list_clause = %|#{file_list_clause} -f namespace_files.#{package["name"]}|
end
definition += <<~EOF
%description -n #{package["name"]}-profiles
AppArmor profiles for #{package["name"]} from project smar-apparmor-profiles.
#{file_list_clause}
EOF
definition = generate_file_rows definition, package
out << definition
end
out
end
private def generate_file_rows definition, package
package["files"]&.each do |file|
basename = File.basename file
definition += "%config %{_sysconfdir}/apparmor.d/#{basename}\n"
end
package["local"]&.each do |file|
basename = File.basename file
definition += "%config(noreplace) %{_sysconfdir}/apparmor.d/local/#{basename}\n"
end
package["namespaces"]&.each do |file|
unless File.symlink? file
basename = File.basename file
definition += "%config %{_sysconfdir}/apparmor.d/namespaces.d/#{basename}\n"
else
# If a file is a symlink, it should be added manually
# by the package where it is a symlink to.
warn "File #{file} needs to be owned manually (2)."
end
end
package["namespace_directories"]&.each do |directory|
basename = File.basename directory
definition += "%dir %{_sysconfdir}/apparmor.d/namespaces.d/#{basename}\n"
#definition += "%config %{_sysconfdir}/apparmor.d/namespaces.d/#{basename}\n"
end
package["included_abstractions"]&.each do |file|
basename = File.basename file
definition += "%config(noreplace) %{_sysconfdir}/apparmor.d/abstractions/#{basename}\n"
end
package["included_tunables"]&.each do |file|
basename = File.basename file
definition += "%config(noreplace) %{_sysconfdir}/apparmor.d/tunables/#{basename}\n"
end
package["extra_directories"]&.each do |directory|
if directory.respond_to? :to_hash
# Special case.
unless directory.size == 1
raise "Unrecognized hash value for extra_directories: #{directory.inspect}"
end
tag, target_directory = directory.each_pair.next
unless tag == "absolute_path"
raise "Only absolute_path is supported for tag value for extra_directories. directory: #{directory.inspect}"
end
definition += "%dir #{target_directory}\n"
else
# Normal case.
definition += "%dir %{_sysconfdir}/apparmor.d/#{directory}\n"
end
end
package["extra_files"]&.each do |directory, files|
config_tag = lambda do |filename|
if filename[0..5] == "local/"
"%config(noreplace)"
else
"%config"
end
end
append_to_definition = lambda do |file|
unless file.respond_to? :to_hash
# Normal case.
definition += "#{config_tag.call(file)} %{_sysconfdir}/apparmor.d/#{file}\n"
return
end
# Special case.
unless file.size == 1
raise "Unrecognized hash value for extra_files: #{file.inspect}"
end
source_file, target_file = file.each_pair.next
definition += "#{config_tag.call(file)} #{target_file}\n"
end
if files.nil?
file = directory
append_to_definition.call file
next
end
files.each do |file|
append_to_definition.call file
end
end
package["load_profile_by_systemd"]&.each do |service_name, profile_name|
definition += load_profile_by_systemd_for_profile service_name, profile_name
end
package["rpm_scriptlets_symlinks"]&.each do |file|
basename = File.basename file
definition += "%config %{_sysconfdir}/apparmor.d/namespaces.d/rpm-scriptlets.d/#{basename}\n"
end
package["in_directory"]&.each do |directory, hashes|
hashes.each do |name, values|
hash = { name => values }
definition = generate_file_rows definition, hash
end
end
definition
end
private def load_profile_by_systemd_for_profile service_name, profile_name
definition = ""
if [ "system", "system_conditional" ].include? service_name
profile_name.each do |service_name, profile_name|
if profile_name.respond_to? :each_pair
profile_name = profile_name["profile"]
end
definition += "%dir %{_unitdir}/#{service_name}.service.d\n"
definition += "%{_unitdir}/#{service_name}.service.d/#{profile_name}.conf\n"
end
return definition
end
#if [ "user", "user_conditional" ].include? service_name
if service_name == "user"
profile_name.each do |service_name, profile_name|
if profile_name.respond_to? :each_pair
profile_name = profile_name["profile"]
end
definition += "%dir %{_userunitdir}/#{service_name}.service.d\n"
definition += "%{_userunitdir}/#{service_name}.service.d/#{profile_name}.conf\n"
end
return definition
end
definition += "%dir %{_unitdir}/#{service_name}.service.d\n"
definition += "%{_unitdir}/#{service_name}.service.d/#{profile_name}.conf\n"
definition
end
# WARNING: This is not too safe to use, so trust the yaml files.
private def load_packages
main = YAML.load_file "main.yaml"
packages = []
main["includes"].each do |file|
packages += YAML.load_file(file)
end
packages.sort do |a, b|
# rpm needs to be first as some other profiles requires
# namespaces.d/rpm-scriptlets.d/ dir it creates.
next -1 if a["name"] == "rpm"
next 1 if b["name"] == "rpm"
a["name"] <=> b["name"]
end
end
end
generate_spec = GenerateSpec.new
generate_spec.generate
generate_spec.finalize