def test_parse_simple_with_arg(self): """Allow simple denylist policy files.""" path = self._write_file( 'test.policy', """ # Comment. @denylist read: return ENOSYS write: arg0 == 0 ; return ENOSYS """) self.assertEqual( self.parser.parse_file(path), parser.ParsedPolicy( default_action=bpf.Allow(), filter_statements=[ parser.FilterStatement( syscall=parser.Syscall('read', 0), frequency=1, filters=[ parser.Filter( None, bpf.ReturnErrno( self.arch.constants['ENOSYS'])), ]), parser.FilterStatement( syscall=parser.Syscall('write', 1), frequency=1, filters=[ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.ReturnErrno( self.arch.constants['ENOSYS'])), parser.Filter(None, bpf.Allow()), ]), ]))
def test_parse_metadata(self): """Accept valid filter statements with metadata.""" self.assertEqualIgnoringToken( self.parser.parse_filter_statement( self._tokenize('read[arch=test]: arg0 == 0')), parser.ParsedFilterStatement( syscalls=( parser.Syscall('read', 0), ), filters=[ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()), ], token=None)) self.assertEqualIgnoringToken( self.parser.parse_filter_statement( self._tokenize( '{read, nonexistent[arch=nonexistent]}: arg0 == 0')), parser.ParsedFilterStatement( syscalls=( parser.Syscall('read', 0), ), filters=[ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()), ], token=None))
def test_parse_simple_grouped(self): """Allow simple policy files.""" path = self._write_file( 'test.policy', """ # Comment. {read, write}: allow """) self.assertEqual( self.parser.parse_file(path), parser.ParsedPolicy(default_action=bpf.KillProcess(), filter_statements=[ parser.FilterStatement( syscall=parser.Syscall('read', 0), frequency=1, filters=[ parser.Filter(None, bpf.Allow()), ]), parser.FilterStatement( syscall=parser.Syscall('write', 1), frequency=1, filters=[ parser.Filter(None, bpf.Allow()), ]), ]))
def parse_action(self, tokens): if not tokens: self._parser_state.error('missing action') action_token = tokens.pop(0) if action_token.type == 'ACTION': if action_token.value == 'allow': return bpf.Allow() if action_token.value == 'kill': return self._kill_action if action_token.value == 'kill-process': return bpf.KillProcess() if action_token.value == 'kill-thread': return bpf.KillThread() if action_token.value == 'trap': return bpf.Trap() if action_token.value == 'trace': return bpf.Trace() if action_token.value == 'log': return bpf.Log() elif action_token.type == 'NUMERIC_CONSTANT': constant = self._parse_single_constant(action_token) if constant == 1: return bpf.Allow() elif action_token.type == 'RETURN': if not tokens: self._parser_state.error('missing return value') return bpf.ReturnErrno(self._parse_single_constant(tokens.pop(0))) return self._parser_state.error('invalid action', token=action_token)
def test_parse_filter_statement(self): """Accept valid filter statements.""" self.assertEqual( self.parser.parse_filter_statement( self._tokenize('read: arg0 == 0')), parser.ParsedFilterStatement((parser.Syscall('read', 0), ), [ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()), ])) self.assertEqual( self.parser.parse_filter_statement( self._tokenize('{read, write}: arg0 == 0')), parser.ParsedFilterStatement(( parser.Syscall('read', 0), parser.Syscall('write', 1), ), [ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()), ])) self.assertEqual( self.parser.parse_filter_statement( self._tokenize('io@libc: arg0 == 0')), parser.ParsedFilterStatement(( parser.Syscall('read', 0), parser.Syscall('write', 1), ), [ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()), ])) self.assertEqual( self.parser.parse_filter_statement( self._tokenize('file-io@systemd: arg0 == 0')), parser.ParsedFilterStatement(( parser.Syscall('read', 0), parser.Syscall('write', 1), ), [ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()), ]))
def test_parse_filter_statement(self): """Accept valid filter statements.""" self.assertEqualIgnoringToken( self.parser.parse_filter_statement( self._tokenize('read: arg0 == 0')), parser.ParsedFilterStatement( syscalls=(parser.Syscall('read', 0), ), filters=[ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()), ], token=None)) self.assertEqualIgnoringToken( self.parser.parse_filter_statement( self._tokenize('{read, write}: arg0 == 0')), parser.ParsedFilterStatement(syscalls=( parser.Syscall('read', 0), parser.Syscall('write', 1), ), filters=[ parser.Filter( [[parser.Atom(0, '==', 0)]], bpf.Allow()), ], token=None)) self.assertEqualIgnoringToken( self.parser.parse_filter_statement( self._tokenize('io@libc: arg0 == 0')), parser.ParsedFilterStatement(syscalls=( parser.Syscall('read', 0), parser.Syscall('write', 1), ), filters=[ parser.Filter( [[parser.Atom(0, '==', 0)]], bpf.Allow()), ], token=None)) self.assertEqualIgnoringToken( self.parser.parse_filter_statement( self._tokenize('file-io@systemd: arg0 == 0')), parser.ParsedFilterStatement(syscalls=( parser.Syscall('read', 0), parser.Syscall('write', 1), ), filters=[ parser.Filter( [[parser.Atom(0, '==', 0)]], bpf.Allow()), ], token=None)) self.assertEqualIgnoringToken( self.parser.parse_filter_statement( self._tokenize('kill: arg0 == 0')), parser.ParsedFilterStatement( syscalls=(parser.Syscall('kill', 62), ), filters=[ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()), ], token=None))
def test_parse_filter(self): """Accept valid filters.""" self.assertEqual( self.parser.parse_filter(self._tokenize('arg0 == 0')), [ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()), ]) self.assertEqual( self.parser.parse_filter(self._tokenize('kill-process')), [ parser.Filter(None, bpf.KillProcess()), ]) self.assertEqual( self.parser.parse_filter(self._tokenize('kill-thread')), [ parser.Filter(None, bpf.KillThread()), ]) self.assertEqual( self.parser.parse_filter(self._tokenize('trap')), [ parser.Filter(None, bpf.Trap()), ]) self.assertEqual( self.parser.parse_filter(self._tokenize('return ENOSYS')), [ parser.Filter(None, bpf.ReturnErrno(self.arch.constants['ENOSYS'])), ]) self.assertEqual( self.parser.parse_filter(self._tokenize('trace')), [ parser.Filter(None, bpf.Trace()), ]) self.assertEqual( self.parser.parse_filter(self._tokenize('user-notify')), [ parser.Filter(None, bpf.UserNotify()), ]) self.assertEqual( self.parser.parse_filter(self._tokenize('log')), [ parser.Filter(None, bpf.Log()), ]) self.assertEqual( self.parser.parse_filter(self._tokenize('allow')), [ parser.Filter(None, bpf.Allow()), ]) self.assertEqual( self.parser.parse_filter(self._tokenize('1')), [ parser.Filter(None, bpf.Allow()), ]) self.assertEqual( self.parser.parse_filter( self._tokenize( '{ arg0 == 0, arg0 == 1; return ENOSYS, trap }')), [ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()), parser.Filter([[parser.Atom(0, '==', 1)]], bpf.ReturnErrno(self.arch.constants['ENOSYS'])), parser.Filter(None, bpf.Trap()), ])
def compile_file(self, policy_filename, *, optimization_strategy, kill_action, include_depth_limit=10, override_default_action=None, denylist=False, ret_log=False): """Return a compiled BPF program from the provided policy file.""" policy_parser = parser.PolicyParser( self._arch, kill_action=kill_action, include_depth_limit=include_depth_limit, override_default_action=override_default_action, denylist=denylist, ret_log=ret_log) parsed_policy = policy_parser.parse_file(policy_filename) entries = [ self.compile_filter_statement(filter_statement, kill_action=kill_action, denylist=denylist) for filter_statement in parsed_policy.filter_statements ] visitor = bpf.FlatteningVisitor(arch=self._arch, kill_action=kill_action) if denylist: accept_action = kill_action reject_action = bpf.Allow() else: accept_action = bpf.Allow() reject_action = parsed_policy.default_action if entries: if optimization_strategy == OptimizationStrategy.BST: next_action = _compile_entries_bst(entries, accept_action, reject_action) else: next_action = _compile_entries_linear(entries, accept_action, reject_action) next_action.accept(bpf.ArgFilterForwardingVisitor(visitor)) reject_action.accept(visitor) accept_action.accept(visitor) bpf.ValidateArch(next_action).accept(visitor) else: reject_action.accept(visitor) bpf.ValidateArch(reject_action).accept(visitor) return visitor.result
def test_parse_frequency(self): """Allow including frequency files.""" self._write_file( 'test.frequency', """ read: 2 write: 3 """) path = self._write_file( 'test.policy', """ @frequency ./test.frequency read: allow """) self.assertEqual( self.parser.parse_file(path), parser.ParsedPolicy( default_action=bpf.KillProcess(), filter_statements=[ parser.FilterStatement( syscall=parser.Syscall('read', 0), frequency=2, filters=[ parser.Filter(None, bpf.Allow()), ]), ]))
def parse_action(self, tokens): if not tokens: self._parser_state.error('missing action') action_token = tokens.pop(0) # denylist policies must specify a return for every line. if self._denylist: if action_token.type != 'RETURN': self._parser_state.error('invalid denylist policy') if action_token.type == 'ACTION': if action_token.value == 'allow': return bpf.Allow() if action_token.value == 'kill': return self._kill_action if action_token.value == 'kill-process': return bpf.KillProcess() if action_token.value == 'kill-thread': return bpf.KillThread() if action_token.value == 'trap': return bpf.Trap() if action_token.value == 'trace': return bpf.Trace() if action_token.value == 'user-notify': return bpf.UserNotify() if action_token.value == 'log': return bpf.Log() elif action_token.type == 'NUMERIC_CONSTANT': constant = self._parse_single_constant(action_token) if constant == 1: return bpf.Allow() elif action_token.type == 'RETURN': if not tokens: self._parser_state.error('missing return value') if self._ret_log: tokens.pop(0) return bpf.Log() else: return bpf.ReturnErrno( self._parse_single_constant(tokens.pop(0))) return self._parser_state.error('invalid action', token=action_token)
def _parse_single_filter(self, tokens): if not tokens: self._parser_state.error('missing filter') if tokens[0].type == 'ARGUMENT': # Only argument expressions can start with an ARGUMENT token. argument_expression = self.parse_argument_expression(tokens) if tokens and tokens[0].type == 'SEMICOLON': tokens.pop(0) action = self.parse_action(tokens) else: action = bpf.Allow() return Filter(argument_expression, action) else: return Filter(None, self.parse_action(tokens))
def test_parse_include(self): """Allow including policy files.""" path = self._write_file( 'test.include.policy', """ {read, write}: arg0 == 0; allow """) path = self._write_file( 'test.policy', """ @include ./test.include.policy read: return ENOSYS """) self.assertEqual( self.parser.parse_file(path), parser.ParsedPolicy( default_action=bpf.KillProcess(), filter_statements=[ parser.FilterStatement( syscall=parser.Syscall('read', 0), frequency=1, filters=[ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()), parser.Filter( None, bpf.ReturnErrno( self.arch.constants['ENOSYS'])), ]), parser.FilterStatement( syscall=parser.Syscall('write', 1), frequency=1, filters=[ parser.Filter([[parser.Atom(0, '==', 0)]], bpf.Allow()), parser.Filter(None, bpf.KillProcess()), ]), ]))
def test_parse_default(self): """Allow defining a default action.""" path = self._write_file( 'test.policy', """ @default kill-thread read: allow """) self.assertEqual( self.parser.parse_file(path), parser.ParsedPolicy(default_action=bpf.KillThread(), filter_statements=[ parser.FilterStatement( syscall=parser.Syscall('read', 0), frequency=1, filters=[ parser.Filter(None, bpf.Allow()), ]), ]))
def compile_filter_statement(self, filter_statement, *, kill_action, denylist=False): """Compile one parser.FilterStatement into BPF.""" policy_entry = SyscallPolicyEntry(filter_statement.syscall.name, filter_statement.syscall.number, filter_statement.frequency) # In each step of the way, the false action is the one that is taken if # the immediate boolean condition does not match. This means that the # false action taken here is the one that applies if the whole # expression fails to match. false_action = filter_statement.filters[-1].action if not denylist and false_action == bpf.Allow(): return policy_entry # We then traverse the list of filters backwards since we want # the root of the DAG to be the very first boolean operation in # the filter chain. for filt in filter_statement.filters[:-1][::-1]: for disjunction in filt.expression: # This is the jump target of the very last comparison in the # conjunction. Given that any conjunction that succeeds should # make the whole expression succeed, make the very last # comparison jump to the accept action if it succeeds. true_action = filt.action for atom in disjunction: block = bpf.Atom(atom.argument_index, atom.op, atom.value, true_action, false_action) true_action = block false_action = true_action policy_filter = false_action # Lower all Atoms into WideAtoms. lowering_visitor = bpf.LoweringVisitor(arch=self._arch) policy_filter = lowering_visitor.process(policy_filter) # Flatten the IR DAG into a single BasicBlock. flattening_visitor = bpf.FlatteningVisitor(arch=self._arch, kill_action=kill_action) policy_filter.accept(flattening_visitor) policy_entry.filter = flattening_visitor.result return policy_entry
def test_parse_other_arch(self): """Allow entries that only target another architecture.""" path = self._write_file( 'test.policy', """ # Comment. read[arch=nonexistent]: allow write: allow """) self.assertEqual( self.parser.parse_file(path), parser.ParsedPolicy(default_action=bpf.KillProcess(), filter_statements=[ parser.FilterStatement( syscall=parser.Syscall('write', 1), frequency=1, filters=[ parser.Filter(None, bpf.Allow()), ]), ]))
def __init__(self, arch, *, kill_action, include_depth_limit=10, override_default_action=None, denylist=False, ret_log=False): self._parser_states = [ParserState("<memory>")] self._kill_action = kill_action self._include_depth_limit = include_depth_limit if denylist: self._default_action = bpf.Allow() else: self._default_action = self._kill_action self._override_default_action = override_default_action self._frequency_mapping = collections.defaultdict(int) self._arch = arch self._denylist = denylist self._ret_log = ret_log