seccomp-tools 1.6.2 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +167 -0
  3. data/README.md +280 -42
  4. data/completions/_seccomp-tools +83 -0
  5. data/completions/seccomp-tools.bash +59 -0
  6. data/completions/seccomp-tools.fish +52 -0
  7. data/ext/ptrace/ptrace.c +2 -2
  8. data/lib/seccomp-tools/asm/asm.rb +9 -4
  9. data/lib/seccomp-tools/asm/compiler.rb +32 -2
  10. data/lib/seccomp-tools/asm/sasm.tab.rb +27 -19
  11. data/lib/seccomp-tools/asm/sasm.y +15 -7
  12. data/lib/seccomp-tools/asm/scalar.rb +50 -7
  13. data/lib/seccomp-tools/asm/scanner.rb +33 -1
  14. data/lib/seccomp-tools/asm/statement.rb +14 -5
  15. data/lib/seccomp-tools/asm/token.rb +19 -1
  16. data/lib/seccomp-tools/audit/catalog.rb +69 -0
  17. data/lib/seccomp-tools/audit/checks/arch_unchecked.rb +41 -0
  18. data/lib/seccomp-tools/audit/checks/dangerous_allow.rb +34 -0
  19. data/lib/seccomp-tools/audit/checks/orw_chain.rb +41 -0
  20. data/lib/seccomp-tools/audit/checks/permissive_default.rb +29 -0
  21. data/lib/seccomp-tools/audit/checks/syscall_alt_gap.rb +44 -0
  22. data/lib/seccomp-tools/audit/checks/x32_guard.rb +46 -0
  23. data/lib/seccomp-tools/audit/checks.rb +42 -0
  24. data/lib/seccomp-tools/audit/finding.rb +25 -0
  25. data/lib/seccomp-tools/audit/policy.rb +126 -0
  26. data/lib/seccomp-tools/audit/report.rb +98 -0
  27. data/lib/seccomp-tools/audit.rb +48 -0
  28. data/lib/seccomp-tools/bpf.rb +27 -17
  29. data/lib/seccomp-tools/cli/asm.rb +6 -2
  30. data/lib/seccomp-tools/cli/audit.rb +86 -0
  31. data/lib/seccomp-tools/cli/base.rb +51 -8
  32. data/lib/seccomp-tools/cli/cli.rb +9 -3
  33. data/lib/seccomp-tools/cli/completion.rb +40 -0
  34. data/lib/seccomp-tools/cli/disasm.rb +9 -5
  35. data/lib/seccomp-tools/cli/dump.rb +37 -52
  36. data/lib/seccomp-tools/cli/dumpable.rb +79 -0
  37. data/lib/seccomp-tools/cli/emu.rb +20 -5
  38. data/lib/seccomp-tools/cli/explain.rb +51 -0
  39. data/lib/seccomp-tools/cli/filter_input.rb +130 -0
  40. data/lib/seccomp-tools/const.rb +98 -15
  41. data/lib/seccomp-tools/consts/sys_arg.rb +43 -5
  42. data/lib/seccomp-tools/consts/sys_nr/aarch64.rb +52 -1
  43. data/lib/seccomp-tools/consts/sys_nr/amd64.rb +53 -1
  44. data/lib/seccomp-tools/consts/sys_nr/i386.rb +83 -2
  45. data/lib/seccomp-tools/consts/sys_nr/riscv64.rb +332 -0
  46. data/lib/seccomp-tools/consts/sys_nr/s390x.rb +30 -1
  47. data/lib/seccomp-tools/disasm/disasm.rb +30 -12
  48. data/lib/seccomp-tools/dumper.rb +67 -36
  49. data/lib/seccomp-tools/emulator.rb +40 -15
  50. data/lib/seccomp-tools/error.rb +4 -2
  51. data/lib/seccomp-tools/explain/analysis.rb +67 -0
  52. data/lib/seccomp-tools/explain/path_facts.rb +110 -0
  53. data/lib/seccomp-tools/explain/qword.rb +204 -0
  54. data/lib/seccomp-tools/explain/renderer.rb +128 -0
  55. data/lib/seccomp-tools/explain/summary.rb +218 -0
  56. data/lib/seccomp-tools/explain/verdict.rb +44 -0
  57. data/lib/seccomp-tools/explain.rb +38 -0
  58. data/lib/seccomp-tools/instruction/alu.rb +14 -9
  59. data/lib/seccomp-tools/instruction/base.rb +39 -10
  60. data/lib/seccomp-tools/instruction/jmp.rb +50 -23
  61. data/lib/seccomp-tools/instruction/ld.rb +46 -21
  62. data/lib/seccomp-tools/instruction/ldx.rb +4 -3
  63. data/lib/seccomp-tools/instruction/misc.rb +11 -9
  64. data/lib/seccomp-tools/instruction/ret.rb +12 -6
  65. data/lib/seccomp-tools/instruction/st.rb +15 -6
  66. data/lib/seccomp-tools/instruction/stx.rb +4 -3
  67. data/lib/seccomp-tools/logger.rb +14 -1
  68. data/lib/seccomp-tools/symbolic/constraint.rb +80 -0
  69. data/lib/seccomp-tools/symbolic/executor.rb +210 -0
  70. data/lib/seccomp-tools/symbolic/expr.rb +185 -0
  71. data/lib/seccomp-tools/symbolic/state.rb +82 -0
  72. data/lib/seccomp-tools/syscall.rb +73 -23
  73. data/lib/seccomp-tools/util.rb +76 -11
  74. data/lib/seccomp-tools/version.rb +1 -1
  75. data/lib/seccomp-tools.rb +10 -1
  76. metadata +36 -24
  77. data/lib/seccomp-tools/disasm/context.rb +0 -171
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'seccomp-tools/audit/finding'
4
+ require 'seccomp-tools/util'
5
+
6
+ module SeccompTools
7
+ class Audit
8
+ # The findings for one filter, rendered either as a human report or a JSON-ready hash.
9
+ class Report
10
+ # Severity => {Util.colorize} theme for the +[SEVERITY]+ tag.
11
+ SEVERITY_THEME = { high: :error, medium: :warn, low: :info }.freeze
12
+
13
+ # Widest a rendered line may get, so a finding reads without wrapping on a standard terminal.
14
+ WIDTH = 120
15
+
16
+ # @param [String?] source Label for the audited filter.
17
+ # @param [Array<String>] arches The architectures covered.
18
+ # @param [Array<Finding>] findings
19
+ # @param [Boolean] truncated Whether the symbolic walk was cut short.
20
+ def initialize(source:, arches:, findings:, truncated:)
21
+ @source = source
22
+ @arches = arches
23
+ @findings = findings.sort_by(&:rank)
24
+ @truncated = truncated
25
+ end
26
+
27
+ # @return [Array<Finding>]
28
+ attr_reader :findings
29
+ # @return [Array<String>] The architectures covered.
30
+ attr_reader :arches
31
+
32
+ # The human report.
33
+ # @return [String]
34
+ def to_s
35
+ out = +''
36
+ out << "Seccomp audit of #{@source}\n" if @source
37
+ out << "Architectures: #{@arches.join(', ')}\n" unless @arches.empty?
38
+ # A truncated walk can only hide weaknesses, so it qualifies the whole report rather than
39
+ # being a finding of its own.
40
+ out << "WARNING: analysis truncated (filter too large); results may be incomplete.\n" if @truncated
41
+ return out << "\nNo weaknesses found.\n" if @findings.empty?
42
+
43
+ @findings.each { |f| out << render(f) }
44
+ out
45
+ end
46
+
47
+ # @return [Hash] JSON-ready shape for one filter.
48
+ def to_h
49
+ { source: @source, arches: @arches, truncated: @truncated, findings: @findings.map(&:to_h) }
50
+ end
51
+
52
+ private
53
+
54
+ def render(finding)
55
+ tag = Util.colorize("[#{finding.severity.to_s.upcase}]", t: SEVERITY_THEME[finding.severity])
56
+ arch = finding.arch ? " (#{Util.colorize(finding.arch, t: :arch)})" : ''
57
+ names = finding.syscalls
58
+ out = "\n#{tag} #{highlight(finding.title, names)}#{arch}\n"
59
+ out << paragraph(finding.detail, names, ' ', ' ')
60
+ out << paragraph(finding.condition, names, ' when: ', ' ') if finding.condition
61
+ out << paragraph(finding.remediation, names, ' fix: ', ' ') if finding.remediation
62
+ out
63
+ end
64
+
65
+ # +text+ wrapped to {WIDTH} columns, its first line prefixed with +first+ and the rest with
66
+ # +hang+ so they read as one block. Wrapping is measured before coloring, so the invisible
67
+ # escape codes never count towards the width.
68
+ # @return [String]
69
+ def paragraph(text, names, first, hang)
70
+ lines = []
71
+ indent = first
72
+ line = nil
73
+ text.to_s.split(/\s+/).each do |word|
74
+ if line.nil?
75
+ line = word
76
+ elsif indent.size + line.size + 1 + word.size <= WIDTH
77
+ line = "#{line} #{word}"
78
+ else
79
+ lines << (indent + line)
80
+ indent = hang
81
+ line = word
82
+ end
83
+ end
84
+ lines << (indent + line) if line
85
+ lines.map { |l| "#{highlight(l, names)}\n" }.join
86
+ end
87
+
88
+ # Paints the syscall names a finding is about wherever they appear in +text+, in the same color
89
+ # disasm gives them. Whole words only, so +read+ leaves +process_vm_readv+ alone.
90
+ # @return [String]
91
+ def highlight(text, names)
92
+ Array(names).uniq.reduce(text) do |painted, name|
93
+ painted.gsub(/\b#{Regexp.escape(name)}\b/) { Util.colorize(name, t: :syscall) }
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'seccomp-tools/audit/checks'
4
+ require 'seccomp-tools/audit/policy'
5
+ require 'seccomp-tools/audit/report'
6
+ require 'seccomp-tools/explain/analysis'
7
+ require 'seccomp-tools/symbolic/executor'
8
+
9
+ module SeccompTools
10
+ # Assesses a seccomp filter for weaknesses / escape routes and reports them as a {Audit::Report}.
11
+ #
12
+ # It runs the generic {Symbolic::Executor} over the filter (the same walk {Explain} uses), splits
13
+ # the leaves per architecture with {Explain::Analysis}, and runs each {Checks} rule against the
14
+ # per-arch {Policy}. Every supported architecture is assessed; architecture-specific quirks (e.g.
15
+ # amd64's x32 ABI) are registered per arch in {Checks}, never special-cased inline.
16
+ #
17
+ # @example
18
+ # insts = SeccompTools::Disasm.to_bpf(raw, :amd64).map(&:inst)
19
+ # puts SeccompTools::Audit.new(insts, arch: :amd64, source: 'a.out').audit
20
+ class Audit
21
+ # @param [Array<Instruction::Base>] instructions
22
+ # The filter, as +SeccompTools::Disasm.to_bpf(raw, arch).map(&:inst)+.
23
+ # @param [Symbol] arch
24
+ # The architecture the filter is written for, used when it does not itself branch on +arch+.
25
+ # @param [String?] source
26
+ # A label for the filter (e.g. a filename) shown in the report.
27
+ def initialize(instructions, arch:, source: nil)
28
+ @instructions = instructions
29
+ @arch = arch
30
+ @source = source
31
+ end
32
+
33
+ # Walks the filter, runs every check, and returns the {Report}.
34
+ # @return [Report]
35
+ def audit
36
+ leaves, truncated = Symbolic::Executor.new(@instructions).run
37
+ analysis = Explain::Analysis.new(leaves)
38
+ policies = analysis.sections(@arch).map { |section| Policy.new(analysis, section) }
39
+
40
+ findings = Checks::FILTER.flat_map { |check| check.call(analysis) }
41
+ policies.each do |policy|
42
+ Checks.section_checks(policy.arch_sym).each { |check| findings.concat(check.call(policy)) }
43
+ end
44
+
45
+ Report.new(source: @source, arches: policies.map(&:arch_name), findings:, truncated:)
46
+ end
47
+ end
48
+ end
@@ -7,7 +7,10 @@ require 'seccomp-tools/const'
7
7
  require 'seccomp-tools/instruction/instruction'
8
8
 
9
9
  module SeccompTools
10
- # Define the +struct sock_filter+, while more powerful.
10
+ # One BPF instruction, i.e. a +struct sock_filter+.
11
+ #
12
+ # Beyond the four fields of the C struct, a {BPF} also carries the architecture it belongs to and
13
+ # its line number, which together allow it to be disassembled into readable assembly.
11
14
  class BPF
12
15
  # @return [Integer] Line number.
13
16
  attr_reader :line
@@ -21,12 +24,13 @@ module SeccompTools
21
24
  attr_reader :k
22
25
  # @return [Symbol] Architecture.
23
26
  attr_reader :arch
24
- # @return [Set<Context>] Possible contexts before this instruction.
25
- attr_accessor :contexts
27
+ # @return [Set<SeccompTools::Symbolic::State>] Possible states before this instruction.
28
+ attr_accessor :states
26
29
 
27
30
  # Instantiate a {BPF} object.
28
- # @param [String] raw
29
- # One +struct sock_filter+ in bytes, should be 8 bytes long.
31
+ # @param [String, {Symbol => Integer}] raw
32
+ # One +struct sock_filter+, either as 8 raw bytes or as a hash of the +:code+, +:jt+, +:jf+
33
+ # and +:k+ fields.
30
34
  # @param [Symbol] arch
31
35
  # Architecture, for showing constant names in decompile.
32
36
  # @param [Integer] line
@@ -47,7 +51,7 @@ module SeccompTools
47
51
  end
48
52
  @arch = arch
49
53
  @line = line
50
- @contexts = Set.new
54
+ @states = Set.new
51
55
  @disasm_setting = {
52
56
  code: true,
53
57
  arg_infer: true
@@ -56,8 +60,11 @@ module SeccompTools
56
60
 
57
61
  # Pretty display the disassemble result.
58
62
  # @param [{Symbol => Boolean}] options
59
- # Set display settings.
63
+ # Display settings, merged into the current ones. Supports +:code+, whether to show the raw
64
+ # +code+, +jt+, +jf+ and +k+ fields, and +:arg_infer+, whether to annotate the line with the
65
+ # inferred syscall argument.
60
66
  # @return [String]
67
+ # One line of disassembly, without a trailing newline.
61
68
  def disasm(**options)
62
69
  @disasm_setting.merge!(options)
63
70
  if show_code?
@@ -69,12 +76,14 @@ module SeccompTools
69
76
  end
70
77
  end
71
78
 
72
- # Whether needs to dump code, jt, jf, k.
79
+ # Whether the raw +code+, +jt+, +jf+, +k+ fields need to be dumped.
80
+ # @return [Boolean]
73
81
  def show_code?
74
82
  @disasm_setting[:code]
75
83
  end
76
84
 
77
- # Whether needs to infer the syscall argument names.
85
+ # Whether the syscall argument names need to be inferred.
86
+ # @return [Boolean]
78
87
  def show_arg_infer?
79
88
  @disasm_setting[:arg_infer]
80
89
  end
@@ -88,8 +97,8 @@ module SeccompTools
88
97
  end
89
98
 
90
99
  # Command according to +code+.
91
- # @return [Symbol]
92
- # See {Const::BPF::COMMAND} for list of commands.
100
+ # @return [Symbol?]
101
+ # See {Const::BPF::COMMAND} for the list of commands, +nil+ if +code+ is invalid.
93
102
  def command
94
103
  Const::BPF::COMMAND.invert[code & 7]
95
104
  end
@@ -101,15 +110,16 @@ module SeccompTools
101
110
  inst.decompile
102
111
  end
103
112
 
104
- # @param [Context] context
105
- # Current context.
113
+ # Yields every branch that may be taken after executing this instruction.
114
+ # @param [SeccompTools::Symbolic::State] state
115
+ # Current state.
106
116
  # @yieldparam [Integer] pc
107
117
  # Program counter after this instruction.
108
- # @yieldparam [Context] ctx
109
- # Context after this instruction.
118
+ # @yieldparam [SeccompTools::Symbolic::State] st
119
+ # State after this instruction.
110
120
  # @return [void]
111
- def branch(context, &)
112
- inst.branch(context).each(&)
121
+ def branch(state, &)
122
+ inst.branch(state).each(&)
113
123
  end
114
124
 
115
125
  # Corresponding instruction object.
@@ -12,6 +12,9 @@ module SeccompTools
12
12
  # Usage of this command.
13
13
  USAGE = "asm - #{SUMMARY}\n\nUsage: seccomp-tools asm IN_FILE [options]".freeze
14
14
 
15
+ # Instantiate an {Asm} object, defaulting the output format to +:inspect+.
16
+ #
17
+ # Takes the same arguments as {Base#initialize}.
15
18
  def initialize(*)
16
19
  super
17
20
  option[:format] = :inspect
@@ -19,10 +22,11 @@ module SeccompTools
19
22
 
20
23
  # Define option parser.
21
24
  # @return [OptionParser]
25
+ # The parser of this command's options.
22
26
  def parser
23
27
  @parser ||= OptionParser.new do |opt|
24
28
  opt.banner = usage
25
- opt.on('-o', '--output FILE', 'Output result into FILE instead of stdout.') do |o|
29
+ opt.on('-o', '--output FILE', 'Write output to FILE instead of stdout.') do |o|
26
30
  option[:ofile] = o
27
31
  end
28
32
 
@@ -36,7 +40,7 @@ module SeccompTools
36
40
  end
37
41
  end
38
42
 
39
- # Handle options.
43
+ # Assembles the input file and writes the result in the requested format.
40
44
  # @return [void]
41
45
  def handle
42
46
  return unless super
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ require 'seccomp-tools/audit'
6
+ require 'seccomp-tools/cli/base'
7
+ require 'seccomp-tools/cli/filter_input'
8
+ require 'seccomp-tools/disasm/disasm'
9
+ require 'seccomp-tools/logger'
10
+
11
+ module SeccompTools
12
+ module CLI
13
+ # Handle 'audit' command.
14
+ class Audit < Base
15
+ include FilterInput
16
+
17
+ # Summary of this command.
18
+ SUMMARY = 'Assess a seccomp filter for weaknesses and escape routes.'
19
+ # Usage of this command.
20
+ USAGE = "audit - #{SUMMARY}\n\nUsage: seccomp-tools audit [options] [BPF_FILE|EXEC]".freeze
21
+
22
+ # Instantiate an {Audit} object.
23
+ #
24
+ # Takes the same arguments as {Base#initialize}.
25
+ def initialize(*)
26
+ super
27
+ option[:format] = :human
28
+ end
29
+
30
+ # Define option parser.
31
+ # @return [OptionParser]
32
+ # The parser of this command's options.
33
+ def parser
34
+ @parser ||= OptionParser.new do |opt|
35
+ opt.banner = usage
36
+ option_filter_source(opt, 'audit')
37
+ option_arch(opt, 'With an executable or --pid the architecture is auto-detected instead.')
38
+
39
+ opt.on('-f', '--format FORMAT', %i[human json], 'Output format, one of <human|json>.',
40
+ 'Default: human') do |f|
41
+ option[:format] = f
42
+ end
43
+ end
44
+ end
45
+
46
+ # Reads the filter(s) from a BPF file, an executable, or an existing process, then reports the
47
+ # weaknesses of each.
48
+ # @return [void]
49
+ def handle
50
+ return unless super
51
+
52
+ filters = collect_filters
53
+ return if filters.empty?
54
+
55
+ option[:format] == :json ? emit_json(filters) : emit_human(filters)
56
+ end
57
+
58
+ private
59
+
60
+ # Prints each filter's report, warning first when several filters stack.
61
+ def emit_human(filters)
62
+ if filters.size > 1
63
+ Logger.warn("#{filters.size} filters are installed; they stack, so a syscall must pass every one " \
64
+ '(most restrictive wins). Each is audited separately below.')
65
+ end
66
+ each_report(filters) { |report| output { report.to_s } }
67
+ end
68
+
69
+ # Prints one JSON document describing every stacked filter.
70
+ def emit_json(filters)
71
+ reports = []
72
+ each_report(filters) { |report| reports << report.to_h }
73
+ output { "#{JSON.pretty_generate(stacked_filters: filters.size, reports:)}\n" }
74
+ end
75
+
76
+ # Yields the {Audit::Report} of each filter, labelling stacked filters like +explain+ does.
77
+ def each_report(filters)
78
+ filters.each_with_index do |(raw, arch, source), idx|
79
+ label = filters.size > 1 ? "#{source} (filter ##{idx})" : source
80
+ insts = SeccompTools::Disasm.to_bpf(raw, arch).map(&:inst)
81
+ yield SeccompTools::Audit.new(insts, arch:, source: label).audit
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -2,13 +2,19 @@
2
2
 
3
3
  require 'optparse'
4
4
 
5
+ require 'seccomp-tools/logger'
5
6
  require 'seccomp-tools/util'
6
7
 
7
8
  module SeccompTools
8
9
  module CLI
9
10
  # Base class for handlers.
11
+ #
12
+ # Each subcommand is a subclass that defines a +USAGE+ constant and a +parser+, and overrides
13
+ # {#handle} to do its work.
10
14
  class Base
11
- # @return [{Symbol => Object}] Options.
15
+ # @return [{Symbol => Object}]
16
+ # Options parsed from the command line. Common keys are +:arch+, +:ifile+, +:ofile+,
17
+ # +:format+, +:limit+, +:pid+ and +:verbose+, depending on the subcommand.
12
18
  attr_reader :option
13
19
  # @return [Array<String>] Arguments array.
14
20
  attr_reader :argv
@@ -23,14 +29,29 @@ module SeccompTools
23
29
 
24
30
  private
25
31
 
26
- # Handle show help message.
32
+ # Parses the common options, and shows the help message when asked to.
27
33
  # @return [Boolean]
28
- # For decestors to check whether need to continue.
34
+ # For descendants to check whether they need to continue. +false+ when the help message
35
+ # was shown and there is nothing left to do.
29
36
  def handle
30
- return CLI.show(parser.help) if argv.empty? || %w[-h --help].any? { |h| argv.include?(h) }
37
+ return CLI.show(parser.help) if argv.empty? || %w[-h --help].intersect?(argv)
31
38
 
32
39
  parser.parse!(argv)
33
- option[:arch] ||= Util.system_arch
40
+
41
+ # Fill in the architecture from the host when --arch was not given. A command that offers
42
+ # --arch needs a concrete architecture to name syscalls; if the host CPU is one seccomp-tools
43
+ # does not recognize, auto-detection cannot supply one, so fail with a clear message instead
44
+ # of letting :unknown reach a syscall-table lookup and raise deep down.
45
+ return true unless option[:arch].nil?
46
+
47
+ arch = Util.system_arch
48
+ if arch == :unknown && @arch_option_offered
49
+ Logger.error('Could not detect the host architecture; specify one with ' \
50
+ "--arch <#{Util.supported_archs.join('|')}>.")
51
+ return false
52
+ end
53
+
54
+ option[:arch] = arch
34
55
  true
35
56
  end
36
57
 
@@ -43,6 +64,9 @@ module SeccompTools
43
64
  end
44
65
 
45
66
  # Write data to stdout or file(s).
67
+ #
68
+ # When +option[:ofile]+ is set, each call writes to a new file named after it with an
69
+ # incrementing serial number, see {#file_of}. Colors are disabled while writing to a file.
46
70
  # @yieldreturn [String]
47
71
  # The data to be written.
48
72
  # @return [void]
@@ -65,7 +89,7 @@ module SeccompTools
65
89
  # @param [String] file
66
90
  # Filename.
67
91
  # @param [Integer] serial
68
- # serial number, starts from zero.
92
+ # Serial number, starts from zero.
69
93
  # @return [String]
70
94
  # Result filename.
71
95
  # @example
@@ -89,11 +113,30 @@ module SeccompTools
89
113
  self.class.const_get(:USAGE)
90
114
  end
91
115
 
92
- def option_arch(opt)
116
+ # Warns about positional arguments left over in {#argv} after the command has taken what it
117
+ # needs, e.g. an exec given together with +-c+, or anything after +--pid+. The command still
118
+ # proceeds.
119
+ # @return [void]
120
+ def warn_ignored_arguments
121
+ return if argv.empty?
122
+
123
+ Logger.warn("ignoring unused argument#{'s' if argv.size > 1}: #{argv.join(' ')}")
124
+ end
125
+
126
+ # Registers the common +--arch+ option on +opt+.
127
+ #
128
+ # @param [OptionParser] opt
129
+ # The parser to add the option to.
130
+ # @param [Array<String>] extra_desc
131
+ # Extra description lines appended after the default ones, for command-specific guidance.
132
+ # @return [void]
133
+ def option_arch(opt, *extra_desc)
134
+ @arch_option_offered = true
93
135
  supported = Util.supported_archs
94
136
  opt.on('-a', '--arch ARCH', supported, 'Specify architecture.',
95
137
  "Supported architectures are <#{supported.join('|')}>.",
96
- "Default: #{Util.system_arch}") do |a|
138
+ 'Default: auto-detected from the host machine.',
139
+ 'Set it when the filter targets an architecture other than the host.', *extra_desc) do |a|
97
140
  option[:arch] = a
98
141
  end
99
142
  end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'seccomp-tools/cli/asm'
4
+ require 'seccomp-tools/cli/audit'
5
+ require 'seccomp-tools/cli/completion'
4
6
  require 'seccomp-tools/cli/disasm'
5
7
  require 'seccomp-tools/cli/dump'
6
8
  require 'seccomp-tools/cli/emu'
9
+ require 'seccomp-tools/cli/explain'
7
10
  require 'seccomp-tools/version'
8
11
 
9
12
  module SeccompTools
@@ -12,9 +15,12 @@ module SeccompTools
12
15
  # Handled commands
13
16
  COMMANDS = {
14
17
  'asm' => SeccompTools::CLI::Asm,
18
+ 'audit' => SeccompTools::CLI::Audit,
19
+ 'completion' => SeccompTools::CLI::Completion,
15
20
  'disasm' => SeccompTools::CLI::Disasm,
16
21
  'dump' => SeccompTools::CLI::Dump,
17
- 'emu' => SeccompTools::CLI::Emu
22
+ 'emu' => SeccompTools::CLI::Emu,
23
+ 'explain' => SeccompTools::CLI::Explain
18
24
  }.freeze
19
25
 
20
26
  # Main usage message.
@@ -57,9 +63,9 @@ EOS
57
63
  COMMANDS[cmd].new(argv).handle
58
64
  end
59
65
 
60
- # Just write message to stdout.
66
+ # Writes a message to stdout, followed by a newline.
61
67
  # @param [String] msg
62
- # The message.
68
+ # The message to be written.
63
69
  # @return [void]
64
70
  def show(msg)
65
71
  puts msg
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'seccomp-tools/cli/base'
4
+ require 'seccomp-tools/logger'
5
+
6
+ module SeccompTools
7
+ module CLI
8
+ # Handle 'completion' command.
9
+ class Completion < Base
10
+ # Summary of this command.
11
+ SUMMARY = 'Print a shell completion script.'
12
+ # Usage of this command.
13
+ USAGE = "completion - #{SUMMARY}\n\nUsage: seccomp-tools completion <bash|zsh|fish>".freeze
14
+
15
+ # Maps a shell name to the completion script that ships with the gem.
16
+ SCRIPTS = { 'bash' => 'seccomp-tools.bash', 'zsh' => '_seccomp-tools', 'fish' => 'seccomp-tools.fish' }.freeze
17
+
18
+ # Define option parser.
19
+ # @return [OptionParser]
20
+ # The parser of this command's options.
21
+ def parser
22
+ @parser ||= OptionParser.new do |opt|
23
+ opt.banner = usage
24
+ end
25
+ end
26
+
27
+ # Writes the completion script for the requested shell to stdout.
28
+ # @return [void]
29
+ def handle
30
+ return unless super
31
+
32
+ shell = argv.shift
33
+ file = SCRIPTS[shell]
34
+ return CLI.show(parser.help) if file.nil?
35
+
36
+ output { File.read(File.join(__dir__, '..', '..', '..', 'completions', file)) }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -12,6 +12,9 @@ module SeccompTools
12
12
  # Usage of this command.
13
13
  USAGE = "disasm - #{SUMMARY}\n\nUsage: seccomp-tools disasm BPF_FILE [options]".freeze
14
14
 
15
+ # Instantiate a {Disasm} object, showing raw BPF and inferred arguments by default.
16
+ #
17
+ # Takes the same arguments as {Base#initialize}.
15
18
  def initialize(*)
16
19
  super
17
20
  option[:bpf] = true
@@ -20,10 +23,11 @@ module SeccompTools
20
23
 
21
24
  # Define option parser.
22
25
  # @return [OptionParser]
26
+ # The parser of this command's options.
23
27
  def parser
24
28
  @parser ||= OptionParser.new do |opt|
25
29
  opt.banner = usage
26
- opt.on('-o', '--output FILE', 'Output result into FILE instead of stdout.') do |o|
30
+ opt.on('-o', '--output FILE', 'Write output to FILE instead of stdout.') do |o|
27
31
  option[:ofile] = o
28
32
  end
29
33
  option_arch(opt)
@@ -35,9 +39,9 @@ module SeccompTools
35
39
  'Default: true') do |f|
36
40
  option[:arg_infer] = f
37
41
  end
38
- opt.on('--asm-able', 'Output with this flag is a valid input of "seccomp-tools asm".',
39
- 'By default, "seccomp-tools disasm" is in a human-readable format that easy for analysis.',
40
- 'Passing this flag can have the output be simplified to a valid input for "seccomp-tools asm".',
42
+ opt.on('--asm-able', 'Make the output valid input for "seccomp-tools asm".',
43
+ 'By default, "seccomp-tools disasm" outputs a human-readable format meant for analysis.',
44
+ 'With this flag the output is simplified so it can be fed back to "seccomp-tools asm".',
41
45
  'This flag implies "--no-bpf --no-arg-infer".',
42
46
  'Default: false') do |_f|
43
47
  option[:bpf] = false
@@ -46,7 +50,7 @@ module SeccompTools
46
50
  end
47
51
  end
48
52
 
49
- # Handle options.
53
+ # Disassembles the input file and writes the result.
50
54
  # @return [void]
51
55
  def handle
52
56
  return unless super