Пример #1
0
    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()),
                        ]),
                ]))
Пример #2
0
 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))
Пример #3
0
    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()),
                                        ]),
                                ]))
Пример #4
0
 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()),
         ]))
Пример #6
0
 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))
Пример #7
0
 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()),
         ])
Пример #8
0
    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()),
                        ]),
                ]))
Пример #10
0
    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)
Пример #11
0
 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))
Пример #12
0
    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()),
                        ]),
                ]))
Пример #13
0
    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()),
                                        ]),
                                ]))
Пример #14
0
    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
Пример #15
0
    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()),
                                        ]),
                                ]))
Пример #16
0
 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