152 lines
4.9 KiB
Python
Executable File
152 lines
4.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Copyright (C) 2021 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""A linter for the Minijail seccomp policy file."""
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
|
|
from typing import List, NamedTuple
|
|
|
|
# The syscalls we have determined are more dangerous and need justification
|
|
# for inclusion in a policy.
|
|
DANGEROUS_SYSCALLS = (
|
|
'clone',
|
|
'mount',
|
|
'setns',
|
|
'kill',
|
|
'execve',
|
|
'execveat',
|
|
'bpf',
|
|
'socket',
|
|
'ptrace',
|
|
'swapon',
|
|
'swapoff',
|
|
# TODO(b/193169195): Add argument granularity for the below syscalls.
|
|
'prctl',
|
|
'ioctl',
|
|
# 'mmap',
|
|
# 'mprotect',
|
|
# 'mmap2',
|
|
)
|
|
|
|
class CheckPolicyReturn(NamedTuple):
|
|
"""Represents a return value from check_seccomp_policy
|
|
|
|
Contains a message to print to the user and a list of errors that were
|
|
found in the file.
|
|
"""
|
|
message: str
|
|
errors: List[str]
|
|
|
|
def parse_args(argv):
|
|
"""Return the parsed CLI arguments for this tool."""
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
'--denylist',
|
|
action='store_true',
|
|
help='Check as a denylist policy rather than the default allowlist.')
|
|
parser.add_argument(
|
|
'--dangerous-syscalls',
|
|
action='store',
|
|
default=','.join(DANGEROUS_SYSCALLS),
|
|
help='Comma-separated list of dangerous sycalls (overrides default).'
|
|
)
|
|
parser.add_argument('policy',
|
|
help='The seccomp policy.',
|
|
type=argparse.FileType('r', encoding='utf-8'))
|
|
return parser.parse_args(argv), parser
|
|
|
|
def check_seccomp_policy(check_file, dangerous_syscalls):
|
|
"""Fail if the seccomp policy file has dangerous, undocumented syscalls.
|
|
|
|
Takes in a file object and a set of dangerous syscalls as arguments.
|
|
"""
|
|
|
|
found_syscalls = set()
|
|
errors = []
|
|
msg = ''
|
|
contains_dangerous_syscall = False
|
|
prev_line_comment = False
|
|
|
|
for line_num, line in enumerate(check_file):
|
|
if re.match(r'^\s*#', line):
|
|
prev_line_comment = True
|
|
elif re.match(r'^\s*$', line):
|
|
# Empty lines shouldn't reset prev_line_comment.
|
|
continue
|
|
else:
|
|
match = re.match(fr'^\s*(\w*)\s*:', line)
|
|
if match:
|
|
syscall = match.group(1)
|
|
if syscall in found_syscalls:
|
|
errors.append(f'{check_file.name}, line {line_num}: repeat '
|
|
f'syscall: {syscall}')
|
|
else:
|
|
found_syscalls.add(syscall)
|
|
for dangerous in dangerous_syscalls:
|
|
if dangerous == syscall:
|
|
# Dangerous syscalls must be preceded with a
|
|
# comment.
|
|
contains_dangerous_syscall = True
|
|
if not prev_line_comment:
|
|
errors.append(f'{check_file.name}, line '
|
|
f'{line_num}: {syscall} syscall '
|
|
'is a dangerous syscall so '
|
|
'requires a comment on the '
|
|
'preceding line')
|
|
prev_line_comment = False
|
|
else:
|
|
# This line is probably a continuation from the previous line.
|
|
# TODO(b/203216289): Support line breaks.
|
|
pass
|
|
|
|
if contains_dangerous_syscall:
|
|
msg = (f'seccomp: {check_file.name} contains dangerous syscalls, so'
|
|
' requires review from chromeos-security@')
|
|
else:
|
|
msg = (f'seccomp: {check_file.name} does not contain any dangerous'
|
|
' syscalls, so does not require review from'
|
|
' chromeos-security@')
|
|
|
|
if errors:
|
|
return CheckPolicyReturn(msg, errors)
|
|
|
|
return CheckPolicyReturn(msg, errors)
|
|
|
|
def main(argv=None):
|
|
"""Main entrypoint."""
|
|
|
|
if argv is None:
|
|
argv = sys.argv[1:]
|
|
|
|
opts, _arg_parser = parse_args(argv)
|
|
|
|
check = check_seccomp_policy(opts.policy,
|
|
set(opts.dangerous_syscalls.split(',')))
|
|
|
|
formatted_items = ''
|
|
if check.errors:
|
|
item_prefix = '\n * '
|
|
formatted_items = item_prefix + item_prefix.join(check.errors)
|
|
|
|
print('* ' + check.message + formatted_items)
|
|
|
|
return 1 if check.errors else 0
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main(sys.argv[1:]))
|