Class: SeccompTools::Audit::Policy

Inherits:
Object
  • Object
show all
Defined in:
lib/seccomp-tools/audit/policy.rb

Overview

One architecture's view of a filter: given a syscall number, which actions can an attacker reach? Built from that arch's section leaves (already arch-filtered by Explain::Analysis).

A leaf is reachable for syscall nr when every plain sys_number fact on its path is satisfied by nr - this one predicate covers ==, ranges, and jset bit-tests (so it sees an x32 guard written either way). Argument and opaque facts are not consulted: arguments are attacker-controlled and the executor already dropped self-contradictory paths, so a surviving leaf is reachable under some argument choice. The stance is deliberately conservative - "could an attacker reach this action?".

Constant Summary collapse

SYS =
Const::BPF::SeccompData::SYS_NUMBER

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(analysis, section) ⇒ Policy

Returns a new instance of Policy.

Parameters:



34
35
36
37
38
39
# File 'lib/seccomp-tools/audit/policy.rb', line 34

def initialize(analysis, section)
  @analysis = analysis
  @arch_val, @arch_sym, @title, @leaves = section
  @fusion = @arch_sym && Explain::QwordFusion.new(@arch_sym)
  @renderer = @fusion && Explain::Renderer.new(@fusion)
end

Instance Attribute Details

#arch_symSymbol? (readonly)

Returns The architecture symbol whose syscall names apply (+nil+ when unknown).

Returns:

  • (Symbol?)

    The architecture symbol whose syscall names apply (+nil+ when unknown).



26
27
28
# File 'lib/seccomp-tools/audit/policy.rb', line 26

def arch_sym
  @arch_sym
end

#arch_valInteger? (readonly)

Returns The AUDIT_ARCH value (+nil+ when the filter never branches on arch).

Returns:

  • (Integer?)

    The AUDIT_ARCH value (+nil+ when the filter never branches on arch).



24
25
26
# File 'lib/seccomp-tools/audit/policy.rb', line 24

def arch_val
  @arch_val
end

#leavesArray<Symbolic::Executor::Leaf> (readonly)

Returns This section's leaves.

Returns:



30
31
32
# File 'lib/seccomp-tools/audit/policy.rb', line 30

def leaves
  @leaves
end

#titleObject (readonly)

Returns Display title for this section (arch symbol or 0x... (unknown)).

Returns:

  • (Object)

    Display title for this section (arch symbol or 0x... (unknown)).



28
29
30
# File 'lib/seccomp-tools/audit/policy.rb', line 28

def title
  @title
end

Instance Method Details

#arch_nameString

A display name for this section's architecture (+"amd64"+, or "0x... (unknown)").

Returns:

  • (String)


43
44
45
# File 'lib/seccomp-tools/audit/policy.rb', line 43

def arch_name
  (@arch_sym || @title).to_s
end

#condition_for(nr, action = 'ALLOW') ⇒ String?

The argument condition under which nr reaches action, rendered like explain (+"filename == 0x..."+), or nil when it is unconditional or the arch is unknown.

Parameters:

  • nr (Integer)
  • action (String) (defaults to: 'ALLOW')

Returns:

  • (String?)


100
101
102
103
104
105
106
107
# File 'lib/seccomp-tools/audit/policy.rb', line 100

def condition_for(nr, action = 'ALLOW')
  return nil unless @renderer

  ls = reachable_leaves(nr).select { |l| Explain::Verdict.label(l.ret) == action }
  conds = @fusion.merge_or(ls.map { |l| @analysis.facts(l).residual })
                 .map { |list| @renderer.conjunction(@fusion.fold(list), name_of(nr)) }.uniq
  conds.include?('') ? nil : conds.join(' or ')
end

#default_labelString?

The action of the catch-all (default) path, e.g. "ALLOW" / "ERRNO(5)".

Returns:

  • (String?)


67
68
69
# File 'lib/seccomp-tools/audit/policy.rb', line 67

def default_label
  @analysis.default_label(@leaves)
end

#explicitly_denied?(nr) ⇒ Boolean

Was syscall nr singled out for denial (a rule pins sys_number == nr to a non-+ALLOW+ action), as opposed to merely being absent from an allowlist? Distinguishes a real "blocked one equivalent but not another" gap from allowlist omissions.

Parameters:

  • nr (Integer)

Returns:

  • (Boolean)


90
91
92
93
# File 'lib/seccomp-tools/audit/policy.rb', line 90

def explicitly_denied?(nr)
  !reachable_as_allow?(nr) &&
    @leaves.any? { |l| @analysis.facts(l).sys_eq == nr && Explain::Verdict.label(l.ret) != 'ALLOW' }
end

#number(name) ⇒ Integer?

The number of syscall name on this arch, or nil if the arch lacks it.

Parameters:

  • name (Symbol)

Returns:

  • (Integer?)


61
62
63
# File 'lib/seccomp-tools/audit/policy.rb', line 61

def number(name)
  table && table[name]
end

#reachable_actions(nr) ⇒ Array<String>

The distinct actions reachable for syscall number nr.

Parameters:

  • nr (Integer)

Returns:

  • (Array<String>)


74
75
76
# File 'lib/seccomp-tools/audit/policy.rb', line 74

def reachable_actions(nr)
  reachable_leaves(nr).map { |l| Explain::Verdict.label(l.ret) }.uniq
end

#reachable_as_allow?(nr) ⇒ Boolean

Can syscall nr reach ALLOW?

Parameters:

  • nr (Integer)

Returns:

  • (Boolean)


81
82
83
# File 'lib/seccomp-tools/audit/policy.rb', line 81

def reachable_as_allow?(nr)
  reachable_leaves(nr).any? { |l| Explain::Verdict.label(l.ret) == 'ALLOW' }
end

#tableHash{Symbol=>Integer}?

This arch's name => number table, or nil when the section has no known architecture.

@arch_sym is always nil or a supported architecture (the CLI rejects an undetectable host before we get here), so the table lookup never fails.

Returns:

  • (Hash{Symbol=>Integer}?)


52
53
54
55
56
# File 'lib/seccomp-tools/audit/policy.rb', line 52

def table
  return @table if defined?(@table)

  @table = @arch_sym && Const::Syscall.const_get(@arch_sym.upcase)
end