aboutsummaryrefslogblamecommitdiffstatshomepage
path: root/src/bad-attrs.py
blob: 4683540b8a1907bd7df20323d21dcb1747c7b30a (plain) (tree)



































































































                                                                

                                                                   
































                                                        
#!/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()