aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src/bad-attrs
blob: 4683540b8a1907bd7df20323d21dcb1747c7b30a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env python3

# Copyright (c) 2023 Egor Tensin <Egor.Tensin@gmail.com>
# This file is part of the "audit-scripts" project.
# For details, see https://github.com/egor-tensin/audit-scripts.
# Distributed under the MIT License.

import argparse
import array
from contextlib import contextmanager
import errno
import fcntl
import logging
import os
import stat
import sys


@contextmanager
def setup_logging():
    logging.basicConfig(
        format='%(asctime)s | %(levelname)s | %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S',
        level=logging.DEBUG)
    try:
        yield
    except Exception as e:
        logging.exception(e)
        raise


def scandir(dir_path):
    try:
        entry_it = os.scandir(dir_path)
    except (PermissionError, FileNotFoundError) as e:
        logging.warning('%s', e)
        return
    with entry_it:
        yield from entry_it


def traverse_tree(root):
    queue = [root]
    while queue:
        for entry in scandir(queue.pop(0)):
            if entry.is_dir(follow_symlinks=False):
                if os.path.ismount(entry.path):
                    continue
                queue.append(entry.path)
            yield entry


def skip_leaf(entry):
    if entry.is_dir(follow_symlinks=False):
        return False
    if entry.is_file(follow_symlinks=False):
        return False
    return True


@contextmanager
def low_level_open(path, flags):
    fd = os.open(path, flags)
    try:
        yield fd
    finally:
        os.close(fd)


FS_IOC_GETFLAGS = 0x80086601

FS_IMMUTABLE_FL = 0x00000010
FS_APPEND_FL    = 0x00000020

BAD_FLAGS = [FS_IMMUTABLE_FL, FS_APPEND_FL]


def flags_contain_bad_flags(flags):
    return any([flags & bad_flag for bad_flag in BAD_FLAGS])


def fd_get_flags(fd):
    a = array.array('L', [0])
    fcntl.ioctl(fd, FS_IOC_GETFLAGS, a, True)
    return a[0]


def path_get_flags(path):
    with low_level_open(path, os.O_RDONLY) as fd:
        return fd_get_flags(fd)


def path_has_bad_flags(path):
    try:
        flags = path_get_flags(path)
    except OSError as e:
        if e.errno == errno.ENOTTY or e.errno == errno.EPERM:
            # Either one of:
            #     Inappropriate ioctl for device
            #     Permission denied
            # It's relied upon that fcntl throws OSError instead of
            # PermissionError.
            logging.warning('%s: %s', path, e)
            return False
        raise
    return flags_contain_bad_flags(flags)


def do_dir(root):
    logging.info('Directory: %s', root)
    for entry in traverse_tree(root):
        if skip_leaf(entry):
            continue
        #logging.debug('Path: %s', entry.path)
        if path_has_bad_flags(entry.path):
            logging.warning('Bad flags: %s', entry.path)


def parse_args(argv=None):
    if argv is None:
        argv = sys.argv[1:]
    parser = argparse.ArgumentParser()
    parser.add_argument('dir', metavar='DIR',
                        help='set root directory')
    return parser.parse_args()


def main(argv=None):
    with setup_logging():
        args = parse_args()
        do_dir(args.dir)


if __name__ == '__main__':
    main()