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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +167 -0
- data/README.md +280 -42
- data/completions/_seccomp-tools +83 -0
- data/completions/seccomp-tools.bash +59 -0
- data/completions/seccomp-tools.fish +52 -0
- data/ext/ptrace/ptrace.c +2 -2
- data/lib/seccomp-tools/asm/asm.rb +9 -4
- data/lib/seccomp-tools/asm/compiler.rb +32 -2
- data/lib/seccomp-tools/asm/sasm.tab.rb +27 -19
- data/lib/seccomp-tools/asm/sasm.y +15 -7
- data/lib/seccomp-tools/asm/scalar.rb +50 -7
- data/lib/seccomp-tools/asm/scanner.rb +33 -1
- data/lib/seccomp-tools/asm/statement.rb +14 -5
- data/lib/seccomp-tools/asm/token.rb +19 -1
- data/lib/seccomp-tools/audit/catalog.rb +69 -0
- data/lib/seccomp-tools/audit/checks/arch_unchecked.rb +41 -0
- data/lib/seccomp-tools/audit/checks/dangerous_allow.rb +34 -0
- data/lib/seccomp-tools/audit/checks/orw_chain.rb +41 -0
- data/lib/seccomp-tools/audit/checks/permissive_default.rb +29 -0
- data/lib/seccomp-tools/audit/checks/syscall_alt_gap.rb +44 -0
- data/lib/seccomp-tools/audit/checks/x32_guard.rb +46 -0
- data/lib/seccomp-tools/audit/checks.rb +42 -0
- data/lib/seccomp-tools/audit/finding.rb +25 -0
- data/lib/seccomp-tools/audit/policy.rb +126 -0
- data/lib/seccomp-tools/audit/report.rb +98 -0
- data/lib/seccomp-tools/audit.rb +48 -0
- data/lib/seccomp-tools/bpf.rb +27 -17
- data/lib/seccomp-tools/cli/asm.rb +6 -2
- data/lib/seccomp-tools/cli/audit.rb +86 -0
- data/lib/seccomp-tools/cli/base.rb +51 -8
- data/lib/seccomp-tools/cli/cli.rb +9 -3
- data/lib/seccomp-tools/cli/completion.rb +40 -0
- data/lib/seccomp-tools/cli/disasm.rb +9 -5
- data/lib/seccomp-tools/cli/dump.rb +37 -52
- data/lib/seccomp-tools/cli/dumpable.rb +79 -0
- data/lib/seccomp-tools/cli/emu.rb +20 -5
- data/lib/seccomp-tools/cli/explain.rb +51 -0
- data/lib/seccomp-tools/cli/filter_input.rb +130 -0
- data/lib/seccomp-tools/const.rb +98 -15
- data/lib/seccomp-tools/consts/sys_arg.rb +43 -5
- data/lib/seccomp-tools/consts/sys_nr/aarch64.rb +52 -1
- data/lib/seccomp-tools/consts/sys_nr/amd64.rb +53 -1
- data/lib/seccomp-tools/consts/sys_nr/i386.rb +83 -2
- data/lib/seccomp-tools/consts/sys_nr/riscv64.rb +332 -0
- data/lib/seccomp-tools/consts/sys_nr/s390x.rb +30 -1
- data/lib/seccomp-tools/disasm/disasm.rb +30 -12
- data/lib/seccomp-tools/dumper.rb +67 -36
- data/lib/seccomp-tools/emulator.rb +40 -15
- data/lib/seccomp-tools/error.rb +4 -2
- data/lib/seccomp-tools/explain/analysis.rb +67 -0
- data/lib/seccomp-tools/explain/path_facts.rb +110 -0
- data/lib/seccomp-tools/explain/qword.rb +204 -0
- data/lib/seccomp-tools/explain/renderer.rb +128 -0
- data/lib/seccomp-tools/explain/summary.rb +218 -0
- data/lib/seccomp-tools/explain/verdict.rb +44 -0
- data/lib/seccomp-tools/explain.rb +38 -0
- data/lib/seccomp-tools/instruction/alu.rb +14 -9
- data/lib/seccomp-tools/instruction/base.rb +39 -10
- data/lib/seccomp-tools/instruction/jmp.rb +50 -23
- data/lib/seccomp-tools/instruction/ld.rb +46 -21
- data/lib/seccomp-tools/instruction/ldx.rb +4 -3
- data/lib/seccomp-tools/instruction/misc.rb +11 -9
- data/lib/seccomp-tools/instruction/ret.rb +12 -6
- data/lib/seccomp-tools/instruction/st.rb +15 -6
- data/lib/seccomp-tools/instruction/stx.rb +4 -3
- data/lib/seccomp-tools/logger.rb +14 -1
- data/lib/seccomp-tools/symbolic/constraint.rb +80 -0
- data/lib/seccomp-tools/symbolic/executor.rb +210 -0
- data/lib/seccomp-tools/symbolic/expr.rb +185 -0
- data/lib/seccomp-tools/symbolic/state.rb +82 -0
- data/lib/seccomp-tools/syscall.rb +73 -23
- data/lib/seccomp-tools/util.rb +76 -11
- data/lib/seccomp-tools/version.rb +1 -1
- data/lib/seccomp-tools.rb +10 -1
- metadata +36 -24
- 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
|
data/lib/seccomp-tools/bpf.rb
CHANGED
|
@@ -7,7 +7,10 @@ require 'seccomp-tools/const'
|
|
|
7
7
|
require 'seccomp-tools/instruction/instruction'
|
|
8
8
|
|
|
9
9
|
module SeccompTools
|
|
10
|
-
#
|
|
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<
|
|
25
|
-
attr_accessor :
|
|
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
|
|
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
|
-
@
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
#
|
|
105
|
-
#
|
|
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 [
|
|
109
|
-
#
|
|
118
|
+
# @yieldparam [SeccompTools::Symbolic::State] st
|
|
119
|
+
# State after this instruction.
|
|
110
120
|
# @return [void]
|
|
111
|
-
def branch(
|
|
112
|
-
inst.branch(
|
|
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', '
|
|
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
|
-
#
|
|
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}]
|
|
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
|
-
#
|
|
32
|
+
# Parses the common options, and shows the help message when asked to.
|
|
27
33
|
# @return [Boolean]
|
|
28
|
-
# For
|
|
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].
|
|
37
|
+
return CLI.show(parser.help) if argv.empty? || %w[-h --help].intersect?(argv)
|
|
31
38
|
|
|
32
39
|
parser.parse!(argv)
|
|
33
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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', '
|
|
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', '
|
|
39
|
-
'By default, "seccomp-tools disasm"
|
|
40
|
-
'
|
|
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
|
-
#
|
|
53
|
+
# Disassembles the input file and writes the result.
|
|
50
54
|
# @return [void]
|
|
51
55
|
def handle
|
|
52
56
|
return unless super
|