aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src/bad-attrs.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/bad-attrs.py')
-rwxr-xr-xsrc/bad-attrs.py135
1 files changed, 135 insertions, 0 deletions
diff --git a/src/bad-attrs.py b/src/bad-attrs.py
new file mode 100755
index 0000000..4683540
--- /dev/null
+++ b/src/bad-attrs.py
@@ -0,0 +1,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()