Skip to content

Instantly share code, notes, and snippets.

@mentha
Last active June 4, 2022 06:53
Show Gist options
  • Save mentha/f02780b4889a2d03fbd9380ff33428d9 to your computer and use it in GitHub Desktop.
Save mentha/f02780b4889a2d03fbd9380ff33428d9 to your computer and use it in GitHub Desktop.
dnf plugin to remove setuid bits
%global descr DNF plugin to remove suid from packages
Name: dnf-plugin-remove-suid
Version: {{{ git_dir_version }}}
Release: 1%{?dist}
Summary: %{descr}
URL: https://gist.github.com/mentha/f02780b4889a2d03fbd9380ff33428d9
License: MIT
VCS: {{{ git_dir_vcs }}}
Source: {{{ git_dir_pack }}}
BuildArch: noarch
BuildRequires: python3-devel
BuildRequires: python3-dnf
%description
Source package for %{descr}.
%package -n python3-%{name}
Summary: %{descr}
Requires: python3-dnf
%description -n python3-%{name}
%{descr}.
Adds a subcommand 'remove-suid' to dnf.
%prep
{{{ git_dir_setup_macro }}}
%build
%install
install -Dpm644 remove_suid.py %{buildroot}%{python3_sitelib}/dnf-plugins/remove_suid.py
%files -n python3-%{name}
%{python3_sitelib}/dnf-plugins/remove_suid.py
%{python3_sitelib}/dnf-plugins/__pycache__/remove_suid.*
%changelog
{{{ git_dir_changelog }}}
from configparser import ConfigParser
from functools import total_ordering
import dnf
import dnf.cli
import dnf.sack
import logging
import os
import stat
logger = logging.getLogger(__name__)
@total_ordering
class Package:
def __init__(self, pkg):
self.pkg = pkg
def __getattr__(self, k):
return getattr(self.pkg, k)
def __hash__(self):
return hash((self.name, self.evr, self.a))
def __repr__(self):
return f'{self.name}-{self.evr}.{self.a}'
def __eq__(self, o):
return repr(self) == repr(o)
def __lt__(self, o):
return repr(self) < repr(o)
def packageSet(l):
return {Package(x) for x in l}
class RemoveSuid:
def __init__(self, base):
self.base = base
self.dry = False
def find_packages(self, q):
if not self.base.sack:
self.base.fill_sack(load_system_repo=True, load_available_repos=False)
return packageSet(self.base.sack.query().installed().filter(name__glob=q))
def resolve_queries(self, qs):
r = set()
for q in qs:
r.update(self.find_packages(q))
return r
def do_file(self, fp, pn):
if os.path.islink(fp) or not os.path.isfile(fp):
return False
ret = False
with open(fp, 'rb') as f:
m = False
mode = os.fstat(f.fileno()).st_mode
if mode & stat.S_ISUID:
m = True
mode &= ~stat.S_ISUID
logger.info('%s %s: suid removed', pn, fp)
if mode & stat.S_ISGID:
m = True
mode &= ~stat.S_ISGID
logger.info('%s %s: sgid removed', pn, fp)
if m:
if not self.dry:
os.fchmod(f.fileno(), mode)
ret = True
if 'security.capability' in os.listxattr(f.fileno()):
if not self.dry:
os.removexattr(f.fileno(), 'security.capability')
logger.info('%s %s: capabilities removed', pn, fp)
ret = True
return ret
def do_pkg(self, p):
pn = p.name + '-' + p.evr + '.' + p.a
modified = False
for fp in p.files:
if self.do_file(fp, pn):
modified = True
if not modified:
logger.info('%s: no files have suid or capabilities', pn)
def do_queries(self, qs):
for p in self.resolve_queries(qs):
self.do_pkg(p)
PLUGIN_NAME = 'remove_suid'
class Config:
def __init__(self, name, base):
self.name = name
self.base = base
self.packages = set()
def find_config(self):
for cp in self.base.conf.pluginconfpath:
p = os.path.join(cp, self.name + '.conf')
if os.path.isfile(p):
return p
return None
def load(self):
c = ConfigParser()
p = self.find_config()
if p:
try:
c.read(p)
except Exception as e:
raise dnf.exceptions.ConfigError(f'{p}: {e}')
self.packages = set(c['main'].get('packages', '').split())
def find_config_write(self):
return self.find_config() or os.path.join(self.base.conf.pluginconfpath[0], self.name + '.conf')
def store(self):
with open(self.find_config_write(), 'w') as f:
print('[main]', file=f)
print(f'packages = {" ".join(self.packages)}', file=f)
class RemoveSuidPlugin(dnf.Plugin):
name = PLUGIN_NAME
def __init__(self, base, cli):
super().__init__(base, cli)
self.base = base
self.conf = Config(PLUGIN_NAME, base)
self.rs = RemoveSuid(base)
if cli is not None:
cli.register_command(RemoveSuidCommand)
def transaction(self):
self.conf.load()
pkgs = self.rs.resolve_queries(self.conf.packages)
for p in sorted(packageSet(self.base.transaction.install_set)):
if p in pkgs:
self.rs.do_pkg(p)
class RemoveSuidCommand(dnf.cli.Command):
aliases = ('remove-suid',)
summary = 'Remove SUID and capabilities from packages'
def __init__(self, *a):
super().__init__(*a)
self.conf = Config(PLUGIN_NAME, self.base)
self.rs = RemoveSuid(self.base)
@staticmethod
def set_argparser(parser):
parser.add_argument('subcommand', nargs=1, choices=['apply', 'list', 'add', 'remove'])
parser.add_argument('packages', metavar='PACKAGE', nargs='*', help='Packages to remove suid from')
parser.add_argument('--dryrun', '-n', action='store_true', help='Show actions to be taken')
def configure(self):
if self.cli.command.opts.command not in self.aliases:
return
self.conf.load()
def run(self):
self.rs.dry = self.opts.dryrun
getattr(self, 'cmd_' + self.opts.subcommand[0])(*self.opts.packages)
def cmd_apply(self, *pkgs):
self.rs.do_queries(pkgs)
def cmd_list(self):
for q in sorted(self.conf.packages):
print(q)
pkgs = self.rs.find_packages(q)
if pkgs:
for p in sorted(pkgs):
print('\t' + p.name)
def cmd_add(self, *pkgs):
self.conf.packages.update(pkgs)
self.conf.store()
def cmd_remove(self, *pkgs):
self.conf.packages.difference_update(pkgs)
self.conf.store()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment