• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rpm-software-management / rpmlint / 27743531775

18 Jun 2026 07:23AM UTC coverage: 88.143% (+0.2%) from 87.933%
27743531775

push

github

6096 of 6916 relevant lines covered (88.14%)

3.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.54
/rpmlint/checks/FilesCheck.py
1
#############################################################################
2
# File          : FilesCheck.py
3
# Package       : rpmlint
4
# Author        : Frederic Lepied
5
# Created on    : Mon Oct  4 19:32:49 1999
6
# Purpose       : test various aspects on files: locations, owner, groups,
7
#                 permission, setuid, setgid...
8
#############################################################################
9

10
from datetime import datetime
4✔
11
import os
4✔
12
from pathlib import Path
4✔
13
import re
4✔
14
import stat
4✔
15

16
import rpm
4✔
17
from rpmlint.checks.AbstractCheck import AbstractCheck
4✔
18
from rpmlint.helpers import byte_to_string
4✔
19
from rpmlint.pkg import is_utf8, is_utf8_bytestr
4✔
20

21
# must be kept in sync with the filesystem package
22
STANDARD_DIRS = (
4✔
23
    '/',
24
    '/bin',
25
    '/boot',
26
    '/etc',
27
    '/etc/X11',
28
    '/etc/opt',
29
    '/etc/profile.d',
30
    '/etc/skel',
31
    '/etc/xinetd.d',
32
    '/home',
33
    '/lib',
34
    '/lib/modules',
35
    '/lib64',
36
    '/media',
37
    '/mnt',
38
    '/mnt/cdrom',
39
    '/mnt/disk',
40
    '/mnt/floppy',
41
    '/opt',
42
    '/proc',
43
    '/root',
44
    '/run',
45
    '/sbin',
46
    '/selinux',
47
    '/srv',
48
    '/sys',
49
    '/tmp',
50
    '/usr',
51
    '/usr/X11R6',
52
    '/usr/X11R6/bin',
53
    '/usr/X11R6/doc',
54
    '/usr/X11R6/include',
55
    '/usr/X11R6/lib',
56
    '/usr/X11R6/lib64',
57
    '/usr/X11R6/man',
58
    '/usr/X11R6/man/man1',
59
    '/usr/X11R6/man/man2',
60
    '/usr/X11R6/man/man3',
61
    '/usr/X11R6/man/man4',
62
    '/usr/X11R6/man/man5',
63
    '/usr/X11R6/man/man6',
64
    '/usr/X11R6/man/man7',
65
    '/usr/X11R6/man/man8',
66
    '/usr/X11R6/man/man9',
67
    '/usr/X11R6/man/mann',
68
    '/usr/bin',
69
    '/usr/bin/X11',
70
    '/usr/etc',
71
    '/usr/games',
72
    '/usr/include',
73
    '/usr/lib',
74
    '/usr/lib/X11',
75
    '/usr/lib/games',
76
    '/usr/lib/gcc-lib',
77
    '/usr/lib/menu',
78
    '/usr/lib64',
79
    '/usr/lib64/gcc-lib',
80
    '/usr/local',
81
    '/usr/local/bin',
82
    '/usr/local/doc',
83
    '/usr/local/etc',
84
    '/usr/local/games',
85
    '/usr/local/info',
86
    '/usr/local/lib',
87
    '/usr/local/lib64',
88
    '/usr/local/man',
89
    '/usr/local/man/man1',
90
    '/usr/local/man/man2',
91
    '/usr/local/man/man3',
92
    '/usr/local/man/man4',
93
    '/usr/local/man/man5',
94
    '/usr/local/man/man6',
95
    '/usr/local/man/man7',
96
    '/usr/local/man/man8',
97
    '/usr/local/man/man9',
98
    '/usr/local/man/mann',
99
    '/usr/local/sbin',
100
    '/usr/local/share',
101
    '/usr/local/share/man',
102
    '/usr/local/share/man/man1',
103
    '/usr/local/share/man/man2',
104
    '/usr/local/share/man/man3',
105
    '/usr/local/share/man/man4',
106
    '/usr/local/share/man/man5',
107
    '/usr/local/share/man/man6',
108
    '/usr/local/share/man/man7',
109
    '/usr/local/share/man/man8',
110
    '/usr/local/share/man/man9',
111
    '/usr/local/share/man/mann',
112
    '/usr/local/src',
113
    '/usr/sbin',
114
    '/usr/share',
115
    '/usr/share/dict',
116
    '/usr/share/doc',
117
    '/usr/share/icons',
118
    '/usr/share/info',
119
    '/usr/share/man',
120
    '/usr/share/man/man1',
121
    '/usr/share/man/man2',
122
    '/usr/share/man/man3',
123
    '/usr/share/man/man4',
124
    '/usr/share/man/man5',
125
    '/usr/share/man/man6',
126
    '/usr/share/man/man7',
127
    '/usr/share/man/man8',
128
    '/usr/share/man/man9',
129
    '/usr/share/man/mann',
130
    '/usr/share/misc',
131
    '/usr/src',
132
    '/usr/tmp',
133
    '/var',
134
    '/var/cache',
135
    '/var/db',
136
    '/var/lib',
137
    '/var/lib/games',
138
    '/var/lib/misc',
139
    '/var/lib/rpm',
140
    '/var/local',
141
    '/var/log',
142
    '/var/mail',
143
    '/var/nis',
144
    '/var/opt',
145
    '/var/preserve',
146
    '/var/spool',
147
    '/var/spool/mail',
148
    '/var/tmp',
149
)
150

4✔
151
compressions = r'\.(gz|z|Z|zip|bz2|lzma|xz|zst)'
4✔
152
sub_bin_regex = re.compile(r'^(/usr)?/s?bin/\S+/')
4✔
153
backup_regex = re.compile(r'(~|\#[^/]+\#|((\.orig|\.rej)(' + compressions + ')?))$')
4✔
154
compr_regex = re.compile(compressions + r'$')
4✔
155
absolute_regex = re.compile(r'^/([^/]+)')
4✔
156
absolute2_regex = re.compile(r'^/?([^/]+)')
4✔
157
points_regex = re.compile(r'^\.\./(.*)')
4✔
158
doc_regex = re.compile(r'^/usr(/share|/X11R6)?/(doc|man|info)/|^/usr/share/gnome/help')
4✔
159
bin_regex = re.compile(r'^/(?:usr/(?:s?bin|games)|s?bin)/(.*)')
4✔
160
includefile_regex = re.compile(r'\.(c|h)(pp|xx)?$', re.IGNORECASE)
4✔
161
develfile_regex = re.compile(r'\.(a|cmxa?|mli?|gir)$')
4✔
162
buildconfigfile_regex = re.compile(r'(\.pc|/bin/.+-config)$')
163
# room for improvement with catching more -R, but also for false positives...
4✔
164
buildconfig_rpath_regex = re.compile(r'(?:-rpath|Wl,-R)\b')
4✔
165
sofile_regex = re.compile(r'/lib(64)?/(.+/)?lib[^/]+\.so$')
4✔
166
devel_regex = re.compile(r'(.*)-(debug(info|source)?|devel|headers|source|static|prof)$')
4✔
167
debuginfo_package_regex = re.compile(r'-debug(info)?$')
4✔
168
debugsource_package_regex = re.compile(r'-debugsource$')
4✔
169
lib_regex = re.compile(r'/lib(?:64)?/lib[A-Za-z0-9](?:(?:|[\w\-\.]*[A-Za-z0-9])\.so\.[\w+\.]+|\w*-\d(?:|[\w\-\.]*[A-Za-z0-9])\.so)$')  # see commit log for detail
4✔
170
ldconfig_regex = re.compile(r'^[^#]*ldconfig', re.MULTILINE)
4✔
171
depmod_regex = re.compile(r'^[^#]*depmod', re.MULTILINE)
4✔
172
install_info_regex = re.compile(r'^[^#]*install-info', re.MULTILINE)
4✔
173
perl_temp_file_regex = re.compile(r'.*perl.*/(\.packlist|perllocal\.pod)$')
4✔
174
scm_regex = re.compile(
175
    r'/(?:RCS|CVS)/[^/]+$|/\.(?:bzr|cvs|git|hg|svn)ignore$|'
176
    r',v$|/\.hgtags$|/\.(?:bzr|git|hg|svn)/|/(?:\.arch-ids|{arch})/')
4✔
177
games_path_regex = re.compile(r'^/usr(/lib(64)?)?/games/')
4✔
178
logrotate_regex = re.compile(r'^/etc/logrotate\.d/(.*)')
4✔
179
kernel_modules_regex = re.compile(r'^(?:/usr)/lib/modules/([0-9]+\.[0-9]+\.[0-9]+[^/]*?)/')
4✔
180
kernel_package_regex = re.compile(r'^kernel(22)?(-)?(smp|enterprise|bigmem|secure|BOOT|i686-up-4GB|p3-smp-64GB)?')
4✔
181
normal_zero_length_regex = re.compile(r'^/etc/security/console\.apps/'
182
                                      r'|/\.nosearch$'
183
                                      r'|/__init__\.py$'
184
                                      r'|/py\.typed$'  # https://www.python.org/dev/peps/pep-0561/#packaging-type-information
185
                                      r'|\.dist-info/REQUESTED$'  # https://www.python.org/dev/peps/pep-0376/#requested
186
                                      r'|/gem\.build_complete$')
4✔
187
perl_regex = re.compile(r'^/usr/lib/perl5/(?:vendor_perl/)?([0-9]+\.[0-9]+)\.([0-9]+)/')
4✔
188
python_regex = re.compile(r'^/usr/lib(?:64)?/python([.0-9]+)/')
4✔
189
python_bytecode_regex_pep3147 = re.compile(r'^(.*)/__pycache__/(.*?)\.([^.]+)(\.opt-[12])?\.py[oc]$')
4✔
190
python_bytecode_regex = re.compile(r'^(.*)(\.py[oc])$')
4✔
191
log_regex = re.compile(r'^/var/log/[^/]+$')
4✔
192
lib_path_regex = re.compile(r'^(/usr(/X11R6)?)?/lib(64)?')
4✔
193
lib_package_regex = re.compile(r'^(lib|.+-libs)')
4✔
194
hidden_file_regex = re.compile(r'/\.[^/]*$')
4✔
195
manifest_perl_regex = re.compile(r'^/usr/share/doc/perl-.*/MANIFEST(\.SKIP)?$')
4✔
196
shebang_regex = re.compile(br'^#!\s*(\S+)(.*?)$', re.M)
4✔
197
interpreter_regex = re.compile(r'^/(?:usr/)?(?:s?bin|games|libexec(?:/.+)?|(?:lib(?:64)?|share)/.+)/([^/]+)$')
4✔
198
script_regex = re.compile(r'^/((usr/)?s?bin|etc/(rc\.d/init\.d|X11/xinit\.d|cron\.(hourly|daily|monthly|weekly)))/')
4✔
199
sourced_script_regex = re.compile(r'^/etc/(bash_completion\.d|profile\.d)/')
4✔
200
filesys_packages = ['filesystem']  # TODO: make configurable?
4✔
201
quotes_regex = re.compile(r'[\'"]+')
4✔
202
start_certificate_regex = re.compile(r'^-----BEGIN CERTIFICATE-----$')
4✔
203
start_private_key_regex = re.compile(r'^----BEGIN PRIVATE KEY-----$')
4✔
204
non_readable_regexs = (re.compile(r'^/var/log/'),
205
                       re.compile(r'^/etc/(g?shadow-?|securetty)$'))
206

4✔
207
man_base_regex = re.compile(r'^/usr(?:/share)?/man(?:/overrides)?/man(?P<category>[^/]+)/(?P<filename>((?P<binary>.+)\.\d.*))')
208

4✔
209
fsf_license_regex = re.compile(br'(GNU((\s+(Library|Lesser|Affero))?(\s+General)?\s+Public|\s+Free\s+Documentation)\s+Licen[cs]e|(GP|FD)L)', re.IGNORECASE)
4✔
210
fsf_wrong_address_regex = re.compile(br'(675\s+Mass\s+Ave|59\s+Temple\s+Place|02139|51\s+Franklin\s+St)', re.IGNORECASE)
211

4✔
212
scalable_icon_regex = re.compile(r'^/usr(?:/local)?/share/icons/.*/scalable/')
4✔
213
tcl_regex = re.compile(r'^/usr/lib(64)?/([^/]+/)?pkgIndex\.tcl')
214

4✔
215
printable_extended_ascii = b'\n\r\t\f\b'
4✔
216
printable_extended_ascii += bytes(range(32, 256))
217

218

219
# See Python sources for a full list of the values here.
220
# https://github.com/python/cpython/blob/master/Lib/importlib/_bootstrap_external.py
221
# https://github.com/python/cpython/blob/2.7/Python/import.c
222
# https://github.com/python/cpython/commit/93602e3af70d3b9f98ae2da654b16b3382b68d50
4✔
223
_python_magic_values = {
224
    '2.2': [60717],
225
    '2.3': [62011],
226
    '2.4': [62061],
227
    '2.5': [62131],
228
    '2.6': [62161],
229
    '2.7': [62211],
230
    '3.0': [3130],
231
    '3.1': [3150],
232
    '3.2': [3180],
233
    '3.3': [3230],
234
    '3.4': [3310],
235
    '3.5': [3350, 3351],  # 3350 for < 3.5.2
236
    '3.6': [3379],
237
    '3.7': [3390, 3391, 3392, 3393, 3394],
238
}
239

240

4✔
241
def get_expected_pyc_magic(path, python_default_version):
242
    """
243
    .pyc/.pyo files embed a 4-byte magic value identifying which version of
244
    the python bytecode ABI they are for. Given a path to a .pyc/.pyo file,
245
    return a (magic ABI values, python version) tuple.  For example,
246
    '/usr/lib/python3.1/foo.pyc' should return (3151, '3.1').
247
    The first value will be None if the python version was not resolved
248
    from the given pathname and the PythonDefaultVersion configuration
249
    variable is not set, or if we don't know the magic ABI values for the
250
    python version (no matter from which source the version came from).
251
    The second value will be None if a python version could not be resolved
252
    from the given pathname.
253
    """
254

4✔
255
    ver_from_path = None
4✔
256
    m = python_regex.search(path)
4✔
257
    if m:
4✔
258
        ver_from_path = m.group(1)
259

4✔
260
    expected_version = ver_from_path or python_default_version
4✔
261
    expected_magic_values = _python_magic_values.get(expected_version)
262

4✔
263
    if not expected_magic_values:
×
264
        return (None, ver_from_path)
265

266
    # In Python 2, if Py_UnicodeFlag is set, Python's import code uses a value
267
    # one higher, but this is off by default. In Python 3.0 and 3.1 (but no
268
    # longer in 3.2), it always uses the value one higher:
4✔
269
    if expected_version[:3] in ('3.0', '3.1'):
×
270
        expected_magic_values = [x + 1 for x in expected_magic_values]
271

4✔
272
    return (expected_magic_values, ver_from_path)
273

274

4✔
275
def py_demarshal_long(b):
276
    """
277
    Counterpart to Python's PyMarshal_ReadLongFromFile, operating on the
278
    bytes in a string.
279
    """
4✔
280
    if isinstance(b, str):
×
281
        b = map(ord, b)
4✔
282
    return (b[0] + (b[1] << 8) + (b[2] << 16) + (b[3] << 24))
283

284

4✔
285
def pyc_magic_from_chunk(chunk):
286
    """From given chunk (beginning of the file), return Python magic number"""
4✔
287
    return py_demarshal_long(chunk[:4]) & 0xffff
288

289

4✔
290
def pyc_mtime_from_chunk(chunk):
291
    """From given chunk (beginning of the file), return mtime or None
292

293
    From Python 3.7, mtime is not always present.
294

295
    See https://www.python.org/dev/peps/pep-0552/#specification
296
    """
4✔
297
    magic = pyc_magic_from_chunk(chunk)
4✔
298
    second = py_demarshal_long(chunk[4:8])
4✔
299
    if magic >= _python_magic_values['3.7'][0]:
4✔
300
        if second == 0:
4✔
301
            return py_demarshal_long(chunk[8:12])
4✔
302
        return None  # No mtime saved, TODO check hashes instead
4✔
303
    return second
304

305

4✔
306
def python_bytecode_to_script(path):
307
    """
308
    Given a python bytecode path, give the path of the .py file
309
    (or None if not python bytecode).
310
    """
311

4✔
312
    res = python_bytecode_regex_pep3147.search(path)
4✔
313
    if res:
4✔
314
        return res.group(1) + '/' + res.group(2) + '.py'
315

4✔
316
    res = python_bytecode_regex.search(path)
4✔
317
    if res:
4✔
318
        return res.group(1) + '.py'
319

4✔
320
    return None
321

322

4✔
323
def script_interpreter(chunk):
4✔
324
    res = shebang_regex.search(chunk) if chunk else None
4✔
325
    return (byte_to_string(res.group(1)), byte_to_string(res.group(2)).strip()) \
326
        if res and res.start() == 0 else (None, '')
327

328

4✔
329
def find_perm_in_tmpfiles(pkg, fname):
330
    """
331
    Look for file path in all tmpdfiles.d file declared in this pkg and return
332
    the permission column.
333
    """
334

4✔
335
    tmpd = []
4✔
336
    perms = '0644'
4✔
337
    user = 'root'
4✔
338
    group = 'root'
4✔
339
    fname = os.path.realpath(fname)
340

4✔
341
    for k, v in pkg.files.items():
4✔
342
        if 'tmpfiles.d' not in k or not k.endswith('.conf'):
4✔
343
            continue
4✔
344
        if not os.path.exists(v.path) or os.path.isdir(v.path):
×
345
            continue
346

4✔
347
        with open(v.path) as f:
4✔
348
            try:
4✔
349
                tmpd += f.readlines()
×
350
            except ValueError:
351
                # Can't read this file, so we are not trying to read definition
352
                # from there
×
353
                pass
354

4✔
355
    for line in tmpd:
4✔
356
        if f' {fname} ' not in line:
4✔
357
            continue
4✔
358
        try:
4✔
359
            _t, _p, perms, user, group, *_rest = line.split()
×
360
        except IndexError:
×
361
            continue
362

4✔
363
    return perms, user, group
364

365

4✔
366
class FilesCheck(AbstractCheck):
4✔
367
    man_regex = re.compile(r'/man(?:\d[px]?|n)/')
4✔
368
    info_regex = re.compile(r'(/usr/share|/usr)/info/')
369

4✔
370
    def __init__(self, config, output):
4✔
371
        super().__init__(config, output)
4✔
372
        self.use_debugsource = self.config.configuration['UseDebugSource']
4✔
373
        self.games_group_regex = re.compile(self.config.configuration['RpmGamesGroup'])
4✔
374
        self.dangling_exceptions = self.config.configuration['DanglingSymlinkExceptions']
4✔
375
        for item in self.dangling_exceptions.values():
2✔
376
            item['path'] = re.compile(item['path'])
4✔
377
        self.module_rpms_ok = self.config.configuration['KernelModuleRPMsOK']
4✔
378
        self.python_default_version = self.config.configuration['PythonDefaultVersion']
4✔
379
        self.perl_version_trick = self.config.configuration['PerlVersionTrick']
4✔
380
        self.skipdocs_regex = re.compile(self.config.configuration['SkipDocsRegexp'], re.IGNORECASE)
4✔
381
        self.meta_package_regex = re.compile(self.config.configuration['MetaPackageRegexp'])
4✔
382
        self.use_relative_symlinks = self.config.configuration['UseRelativeSymlinks']
4✔
383
        self.standard_groups = self.config.configuration['StandardGroups']
4✔
384
        self.standard_users = self.config.configuration['StandardUsers']
4✔
385
        self.disallowed_dirs = self.config.configuration['DisallowedDirs']
4✔
386
        self.compress_ext = self.config.configuration['CompressExtension']
4✔
387
        self.output.error_details.update({
388
            'non-standard-uid':
389
            """A file in this package is owned by a non standard user.
390
            Standard users are:
391
            %s.""" % ', '.join(self.standard_users),
392

393
            'non-standard-gid':
394
            """A file in this package is owned by a non standard group.
395
            Standard groups are:
396
            %s.""" % ', '.join(self.standard_groups),
397

398
            'manpage-not-compressed':
399
                """This manual page is not compressed with the %s compression method
400
                (does not have the %s extension). If the compression does not happen
401
                automatically when the package is rebuilt, make sure that you have the
402
                appropriate rpm helper and/or config packages for your target distribution
403
                installed and try rebuilding again; if it still does not happen automatically,
404
                you can compress this file in the %%install section of the spec file."""
405
                % (self.compress_ext, self.compress_ext),
406

407
            'infopage-not-compressed':
408
                """This info page is not compressed with the %s compression method
409
                (does not have the %s extension). If the compression does not happen
410
                automatically when the package is rebuilt, make sure that you have the
411
                appropriate rpm helper and/or config packages for your target distribution
412
                installed and try rebuilding again; if it still does not happen automatically,
413
                you can compress this file in the %%install section of the spec file."""
414
                % (self.compress_ext, self.compress_ext),
415
        })
4✔
416
        for i in self.disallowed_dirs:
4✔
417
            self.output.error_details.update({'dir-or-file-in-%s' % '-'.join(i.split('/')[1:]):
418
                                             """A file in the package is located in %s. It's not permitted
419
        for packages to install files in this directory.""" % i})
420

4✔
421
    def peek(self, filename, pkg, length=2048):
422
        """
423
        Peek into a file, return a chunk from its beginning and a flag if it
424
        seems to be a text file.
425
        """
4✔
426
        chunk = None
4✔
427
        try:
4✔
428
            with open(filename, 'rb') as fobj:
4✔
429
                chunk = fobj.read(length)
2✔
430
        except OSError as e:  # eg. https://bugzilla.redhat.com/209876
2✔
431
            self.output.add_info('W', pkg, 'read-error', e)
2✔
432
            return (chunk, False)
433

4✔
434
        if b'\0' in chunk:
4✔
435
            return (chunk, False)
436

4✔
437
        if not chunk:  # Empty files are considered text
4✔
438
            return (chunk, True)
439

4✔
440
        fl = filename.lower()
441

442
        # PDF's are binary but often detected as text by the algorithm below
4✔
443
        if fl.endswith('.pdf') and chunk.startswith(b'%PDF-'):
×
444
            return (chunk, False)
445
        # Ditto RDoc RI files
4✔
446
        if fl.endswith('.ri') and '/ri/' in fl:
×
447
            return (chunk, False)
448
        # And Sphinx inventory files
4✔
449
        if fl.endswith('.inv') and chunk.startswith(b'# Sphinx inventory'):
4✔
450
            return (chunk, False)
451

452
        # Binary if control chars are > 30% of the string
4✔
453
        control_chars = chunk.translate(None, printable_extended_ascii)
4✔
454
        nontext_ratio = float(len(control_chars)) / float(len(chunk))
4✔
455
        istext = nontext_ratio <= 0.30
456

4✔
457
        return (chunk, istext)
458

4✔
459
    def check(self, pkg):
4✔
460
        self._check_utf8(pkg)
461

462
        # Rest of the checks are for binary packages only
4✔
463
        if pkg.is_source:
×
464
            return
465

4✔
466
        self.devel_pkg = False
4✔
467
        self.deps = pkg.requires + pkg.prereq
4✔
468
        self.config_files = pkg.config_files
4✔
469
        self.ghost_files = pkg.ghost_files
4✔
470
        self.req_names = pkg.req_names
4✔
471
        self.lib_package = lib_package_regex.search(pkg.name)
4✔
472
        self.is_kernel_package = kernel_package_regex.search(pkg.name)
4✔
473
        self.debuginfo_package = debuginfo_package_regex.search(pkg.name)
4✔
474
        self.debugsource_package = debugsource_package_regex.search(pkg.name)
475

476
        # report these errors only once
4✔
477
        self.perl_dep_error = False
4✔
478
        self.python_dep_error = False
4✔
479
        self.lib_file = False
4✔
480
        self.non_lib_file = None
4✔
481
        self.log_files = []
4✔
482
        self.logrotate_file = False
4✔
483
        self.debuginfo_srcs = False
4✔
484
        self.debuginfo_debugs = False
485

486
        # Prefetch scriptlets, strip quotes from them (#169)
4✔
487
        self.postin = pkg[rpm.RPMTAG_POSTIN] or \
488
            pkg.scriptprog(rpm.RPMTAG_POSTINPROG)
4✔
489
        if self.postin:
2✔
490
            self.postin = quotes_regex.sub('', self.postin)
4✔
491
        self.postun = pkg[rpm.RPMTAG_POSTUN] or \
492
            pkg.scriptprog(rpm.RPMTAG_POSTUNPROG)
4✔
493
        if self.postun:
2✔
494
            self.postun = quotes_regex.sub('', self.postun)
495

496
        # Unique (rdev, inode) combinations
4✔
497
        self.hardlinks = {}
498

499
        # All executable files from standard bin dirs (basename => [paths])
500
        # Hack: basenames with empty paths links are symlinks (not subject
501
        # to duplicate binary check, but yes for man page existence check)
4✔
502
        self.bindir_exes = {}
503

504
        # All man page 'base' names (without section etc extensions)
4✔
505
        self.man_basenames = set()
506

4✔
507
        self._check_devel(pkg)
4✔
508
        self._check_nodoc(pkg)
4✔
509
        self._check_meta_package(pkg)
4✔
510
        self._check_empty_debuginfo(pkg)
511

512
        # Iterate over all pkg.files
4✔
513
        self._check_files(pkg)
514

4✔
515
        self._check_log_files_without_logrotate(pkg)
4✔
516
        self._check_outside_libdir_files(pkg)
4✔
517
        self._check_debuginfo_without_sources(pkg)
4✔
518
        self._check_bindir_exes(pkg)
519

4✔
520
    def _check_utf8(self, pkg):
4✔
521
        for filename in pkg.header[rpm.RPMTAG_FILENAMES] or ():
4✔
522
            if not is_utf8_bytestr(filename):
×
523
                self.output.add_info('E', pkg, 'filename-not-utf8', byte_to_string(filename))
524

4✔
525
    def _check_devel(self, pkg):
526
        # Check if the package is a development package
4✔
527
        self.devel_pkg = devel_regex.search(pkg.name)
528

4✔
529
        if not self.devel_pkg:
4✔
530
            for p in pkg.provides:
2✔
531
                if devel_regex.search(p[0]):
×
532
                    self.devel_pkg = True
×
533
                    break
534

4✔
535
    def _check_nodoc(self, pkg):
4✔
536
        if not self.lib_package and not pkg.doc_files:
4✔
537
            self.output.add_info('W', pkg, 'no-documentation')
538

4✔
539
    def _check_meta_package(self, pkg):
4✔
540
        if pkg.files and self.meta_package_regex.search(pkg.name):
×
541
            self.output.add_info('W', pkg, 'file-in-meta-package')
542

4✔
543
    def _check_empty_debuginfo(self, pkg):
4✔
544
        debuginfo = self.debuginfo_package or self.debugsource_package
4✔
545
        if not pkg.files and debuginfo:
×
546
            self.output.add_info('E', pkg, 'empty-debuginfo-package')
547

4✔
548
    def _check_log_files_without_logrotate(self, pkg):
4✔
549
        if len(self.log_files) and not self.logrotate_file:
×
550
            self.output.add_info('W', pkg, 'log-files-without-logrotate', sorted(self.log_files))
551

4✔
552
    def _check_outside_libdir_files(self, pkg):
4✔
553
        if self.lib_package and self.lib_file and self.non_lib_file:
×
554
            self.output.add_info('E', pkg, 'outside-libdir-files', self.non_lib_file)
555

4✔
556
    def _check_debuginfo_without_sources(self, pkg):
4✔
557
        if not self.use_debugsource and self.debuginfo_package and self.debuginfo_debugs and not self.debuginfo_srcs:
×
558
            self.output.add_info('E', pkg, 'debuginfo-without-sources')
559

4✔
560
    def _check_bindir_exes(self, pkg):
4✔
561
        for exe, paths in self.bindir_exes.items():
4✔
562
            if len(paths) > 1:
×
563
                self.output.add_info('W', pkg, 'duplicate-executable', exe, paths)
4✔
564
            if exe not in self.man_basenames:
4✔
565
                self.output.add_info('W', pkg, 'no-manual-page-for-binary', exe)
566

4✔
567
    def _check_files(self, pkg):
4✔
568
        for f, pkgfile in pkg.files.items():
4✔
569
            self._check_file(pkg, f, pkgfile)
570

4✔
571
    def _check_file(self, pkg, fname, pkgfile):
4✔
572
        if log_regex.search(fname):
×
573
            self.log_files.append(fname)
574

4✔
575
        self._check_file_manpage(pkg, fname)
4✔
576
        self._check_file_infopage_compressed(pkg, fname)
4✔
577
        self._check_file_unexpandaed_macro(pkg, fname)
4✔
578
        self._check_file_non_standard_uid(pkg, fname, pkgfile)
4✔
579
        self._check_file_non_standard_gid(pkg, fname, pkgfile)
4✔
580
        self._check_file_kernel_modules(pkg, fname)
4✔
581
        self._check_file_dir_or_file(pkg, fname)
4✔
582
        self._check_file_non_ghost_in_run(pkg, fname)
4✔
583
        self._check_file_systemd_unit_in_etc(pkg, fname)
4✔
584
        self._check_file_udev_rule_in_etc(pkg, fname)
4✔
585
        self._check_file_tmpfiles_conf_in_etc(pkg, fname)
4✔
586
        self._check_file_subdir_in_bin(pkg, fname)
4✔
587
        self._check_file_siteperl_in_perl_module(pkg, fname)
4✔
588
        self._check_file_backup_file_in_package(pkg, fname)
4✔
589
        self._check_file_version_control_internal_file(pkg, fname)
4✔
590
        self._check_file_htaccess_file(pkg, fname)
4✔
591
        self._check_file_hidden_file_or_dir(pkg, fname)
4✔
592
        self._check_file_manifest_in_perl_module(pkg, fname)
4✔
593
        self._check_file_info_dir_file(pkg, fname)
4✔
594
        self._check_file_makefile_junk(pkg, fname)
4✔
595
        self._check_file_logrotate(pkg, fname)
4✔
596
        self._check_file_crontab(pkg, fname)
4✔
597
        self._check_file_xinetd(pkg, fname)
4✔
598
        self._check_file_compressed_symlink(pkg, fname, pkgfile)
4✔
599
        self._check_file_hardlink(pkg, fname, pkgfile)
600

601
        # normal file check
4✔
602
        self._check_file_normal_file(pkg, fname, pkgfile)
603
        # normal dir check
4✔
604
        self._check_file_dir(pkg, fname, pkgfile)
605
        # symbolic link check
4✔
606
        self._check_file_link(pkg, fname, pkgfile)
607

4✔
608
        self._check_file_crond(pkg, fname, pkgfile)
609

610
        # files with all permissions bits as zero
611
        # https://github.com/rpm-software-management/rpmlint/issues/878
4✔
612
        self._check_file_zero_perms(pkg, fname, pkgfile)
613

4✔
614
    def _check_file_manpage(self, pkg, fname):
615
        """
616
        Check if the the manual page is compressed with the compression method
617
        stated in the rpmlint configuration (CompressExtension option).
618
        Check also for a correct manual page location and if not included in a subfolder.
619

620
        Print a warning if it's not compressed.
621
        """
4✔
622
        if stat.S_ISDIR(pkg.files[fname].mode):
4✔
623
            return
624

4✔
625
        res = man_base_regex.fullmatch(fname)
4✔
626
        if not res:
4✔
627
            return
628

4✔
629
        category = res.group('category')
4✔
630
        filename = Path(res.group('filename'))
4✔
631
        suffixes = filename.suffixes
632

4✔
633
        if self.compress_ext:
4✔
634
            if self.compress_ext != suffixes[-1][1:]:
4✔
635
                self.output.add_info('W', pkg, 'manpage-not-compressed',
636
                                     self.compress_ext, fname)
4✔
637
            suffixes = suffixes[:-1]
638

4✔
639
        if suffixes:
4✔
640
            file_category = suffixes[-1][1:]
4✔
641
            if not file_category.startswith(category):
4✔
642
                self.output.add_info('E', pkg, 'bad-manual-page-folder', fname, f'expected folder: man{file_category}')
4✔
643
            if str(filename.parent) != '.':
4✔
644
                self.output.add_info('E', pkg, 'manual-page-in-subfolder', fname)
645

4✔
646
    def _check_file_infopage_compressed(self, pkg, fname):
647
        """
648
        Check if the the info page is compressed with the compression method
649
        stated in the rpmlint configuration (CompressExtension option).
650

651
        Print a warning if it's not compressed.
652
        """
4✔
653
        if self.compress_ext and self.info_regex.search(fname) and \
654
                not fname.endswith('/info/dir') and not fname.endswith(self.compress_ext):
×
655
            self.output.add_info('W', pkg, 'infopage-not-compressed',
656
                                 self.compress_ext, fname)
657

4✔
658
    def _check_file_crond(self, pkg, fname, pkgfile):
4✔
659
        if not fname.startswith('/etc/cron.d/'):
4✔
660
            return
661

×
662
        mode = pkgfile.mode
×
663
        mode_is_exec = mode & 0o111
×
664
        if stat.S_ISLNK(mode):
×
665
            self.output.add_info('E', pkg, 'symlink-crontab-file', fname)
666

×
667
        if mode_is_exec:
×
668
            self.output.add_info('E', pkg, 'executable-crontab-file', fname)
669

×
670
        if stat.S_IWGRP & mode or stat.S_IWOTH & mode:
×
671
            self.output.add_info('E', pkg, 'non-owner-writeable-only-crontab-file', fname)
672

4✔
673
    def _check_file_zero_perms(self, pkg, fname, pkgfile):
4✔
674
        mode = pkgfile.mode
4✔
675
        perm = mode & 0o7777
4✔
676
        if not perm:
4✔
677
            if pkgfile.is_ghost:
4✔
678
                perms, user, group = find_perm_in_tmpfiles(pkg, pkgfile.name)
4✔
679
                suggestion = f'Suggestion: "%ghost %attr({perms},{user},{group}) {pkgfile.name}"'
680
                # Suggest the attrs if there's a tmpfiles.d
4✔
681
                self.output.add_info('W', pkg, 'zero-perms-ghost', suggestion)
682
            else:
4✔
683
                self.output.add_info('W', pkg, 'zero-perms', fname, '%o' % perm)
684

4✔
685
    def _check_file_unexpandaed_macro(self, pkg, fname):
4✔
686
        for match in self.macro_regex.findall(fname):
4✔
687
            self.output.add_info('W', pkg, 'unexpanded-macro', fname, match)
688

4✔
689
    def _check_file_non_standard_uid(self, pkg, fname, pkgfile):
4✔
690
        if pkgfile.user not in self.standard_users:
4✔
691
            self.output.add_info('W', pkg, 'non-standard-uid', fname, pkgfile.user)
692

4✔
693
    def _check_file_non_standard_gid(self, pkg, fname, pkgfile):
4✔
694
        if pkgfile.group not in self.standard_groups:
4✔
695
            self.output.add_info('W', pkg, 'non-standard-gid', fname, pkgfile.group)
696

4✔
697
    def _check_file_kernel_modules(self, pkg, fname):
4✔
698
        if not self.module_rpms_ok and kernel_modules_regex.search(fname) and not \
699
                self.is_kernel_package:
×
700
            self.output.add_info('E', pkg, 'kernel-modules-not-in-kernel-packages', fname)
701

4✔
702
    def _check_file_dir_or_file(self, pkg, fname):
4✔
703
        for i in self.disallowed_dirs:
4✔
704
            if fname.startswith(i):
2✔
705
                self.output.add_info('E', pkg, 'dir-or-file-in-%s' %
706
                                     '-'.join(i.split('/')[1:]), fname)
707

4✔
708
    def _check_file_non_ghost_in_run(self, pkg, fname):
4✔
709
        if fname.startswith('/run/'):
4✔
710
            if fname not in self.ghost_files:
4✔
711
                self.output.add_info('W', pkg, 'non-ghost-in-run', fname)
712

4✔
713
    def _check_file_systemd_unit_in_etc(self, pkg, fname):
4✔
714
        if fname.startswith('/etc/systemd/system/'):
4✔
715
            self.output.add_info('W', pkg, 'systemd-unit-in-etc', fname)
716

4✔
717
    def _check_file_udev_rule_in_etc(self, pkg, fname):
4✔
718
        if fname.startswith('/etc/udev/rules.d/'):
4✔
719
            self.output.add_info('W', pkg, 'udev-rule-in-etc', fname)
720

4✔
721
    def _check_file_tmpfiles_conf_in_etc(self, pkg, fname):
4✔
722
        if fname.startswith('/etc/tmpfiles.d/'):
4✔
723
            self.output.add_info('W', pkg, 'tmpfiles-conf-in-etc', fname)
724

4✔
725
    def _check_file_subdir_in_bin(self, pkg, fname):
4✔
726
        if sub_bin_regex.search(fname):
4✔
727
            self.output.add_info('E', pkg, 'subdir-in-bin', fname)
728

4✔
729
    def _check_file_siteperl_in_perl_module(self, pkg, fname):
4✔
730
        if '/site_perl/' in fname:
4✔
731
            self.output.add_info('W', pkg, 'siteperl-in-perl-module', fname)
732

4✔
733
    def _check_file_backup_file_in_package(self, pkg, fname):
4✔
734
        if backup_regex.search(fname):
4✔
735
            self.output.add_info('E', pkg, 'backup-file-in-package', fname)
736

4✔
737
    def _check_file_version_control_internal_file(self, pkg, fname):
4✔
738
        if scm_regex.search(fname):
4✔
739
            self.output.add_info('E', pkg, 'version-control-internal-file', fname)
740

4✔
741
    def _check_file_htaccess_file(self, pkg, fname):
4✔
742
        if fname.endswith('/.htaccess'):
4✔
743
            self.output.add_info('E', pkg, 'htaccess-file', fname)
744

4✔
745
    def _check_file_hidden_file_or_dir(self, pkg, fname):
4✔
746
        if (hidden_file_regex.search(fname) and
747
                not fname.startswith('/etc/skel/') and
748
                not fname.endswith('/.build-id') and
749
                not fname.endswith('/.cargo-checksum.json')):
4✔
750
            self.output.add_info('W', pkg, 'hidden-file-or-dir', fname)
751

4✔
752
    def _check_file_manifest_in_perl_module(self, pkg, fname):
4✔
753
        if manifest_perl_regex.search(fname):
4✔
754
            self.output.add_info('W', pkg, 'manifest-in-perl-module', fname)
755

4✔
756
    def _check_file_info_dir_file(self, pkg, fname):
4✔
757
        if fname == '/usr/info/dir' or fname == '/usr/share/info/dir':
4✔
758
            self.output.add_info('E', pkg, 'info-dir-file', fname)
759

4✔
760
    def _check_file_makefile_junk(self, pkg, fname):
4✔
761
        fpath = Path(fname)
4✔
762
        is_doc = fname in pkg.doc_files
4✔
763
        if fpath.name == 'Makefile.am' and str(fpath.with_suffix('.in')) in pkg.files and is_doc:
4✔
764
            self.output.add_info('W', pkg, 'makefile-junk', fname)
765

4✔
766
    def _check_file_logrotate(self, pkg, fname):
4✔
767
        res = logrotate_regex.search(fname)
4✔
768
        if res:
×
769
            self.logrotate_file = True
×
770
            if res.group(1) != pkg.name:
×
771
                self.output.add_info('E', pkg, 'incoherent-logrotate-file', fname)
772

4✔
773
        deps = [x[0] for x in pkg.requires + pkg.recommends + pkg.suggests]
4✔
774
        if res and 'logrotate' not in deps and pkg.name != 'logrotate':
×
775
            self.output.add_info('E', pkg, 'missing-dependency-to-logrotate', 'for logrotate script', fname)
776

4✔
777
    def _check_file_crontab(self, pkg, fname):
4✔
778
        deps = [x[0] for x in pkg.requires + pkg.recommends + pkg.suggests]
4✔
779
        if fname.startswith('/etc/cron.') and 'crontabs' not in deps and pkg.name != 'crontabs':
×
780
            self.output.add_info('E', pkg, 'missing-dependency-to-crontabs', 'for cron script', fname)
781

4✔
782
    def _check_file_xinetd(self, pkg, fname):
4✔
783
        deps = [x[0] for x in pkg.requires + pkg.recommends + pkg.suggests]
4✔
784
        if fname.startswith('/etc/xinet.d/') and 'xinetd' not in deps and pkg.name != 'xinetd':
×
785
            self.output.add_info('E', pkg, 'missing-dependency-to-xinetd', 'for xinet.d script', fname)
786

4✔
787
    def _check_file_compressed_symlink(self, pkg, fname, pkgfile):
4✔
788
        link = pkgfile.linkto
4✔
789
        if link != '':
4✔
790
            ext = compr_regex.search(link)
4✔
791
            if ext and not re.compile(r'\.%s$' % ext.group(1)).search(fname):
×
792
                self.output.add_info('E', pkg, 'compressed-symlink-with-wrong-ext',
793
                                     fname, link)
794

4✔
795
    def _check_file_hardlink(self, pkg, fname, pkgfile):
4✔
796
        rdev = pkgfile.rdev
4✔
797
        inode = pkgfile.inode
798

799
        # Hardlink check
4✔
800
        for hardlink in self.hardlinks.get((rdev, inode), ()):
2✔
801
            if Path(hardlink).parent != Path(fname).parent:
1✔
802
                self.output.add_info('W', pkg, 'cross-directory-hard-link', fname, hardlink)
4✔
803
        self.hardlinks.setdefault((rdev, inode), []).append(fname)
804

4✔
805
    def _check_file_link_devel(self, pkg, fname, pkgfile):
4✔
806
        is_so = sofile_regex.search(fname)
4✔
807
        if not self.devel_pkg and is_so and not pkgfile.linkto.endswith('.so'):
4✔
808
            self.output.add_info('W', pkg, 'devel-file-in-non-devel-package', fname)
809

4✔
810
    def _check_file_link_man(self, pkg, fname):
4✔
811
        res = man_base_regex.fullmatch(fname)
4✔
812
        if res:
1✔
813
            self.man_basenames.add(res.group('binary'))
814

4✔
815
    def _check_file_link_bindir_exes(self, pkg, fname):
4✔
816
        res = bin_regex.search(fname)
4✔
817
        if res:
4✔
818
            exe = res.group(1)
4✔
819
            if '/' not in exe:
4✔
820
                self.bindir_exes.setdefault(exe, [])
821

4✔
822
    def _check_file_link_absolute(self, pkg, fname, pkgfile):
4✔
823
        link = pkgfile.linkto
824

825
        # absolute link
4✔
826
        r = absolute_regex.search(link)
4✔
827
        if not r:
4✔
828
            return
829

×
830
        is_so = sofile_regex.search(fname)
×
831
        if not is_so and link not in pkg.files and \
832
                link not in self.req_names:
×
833
            is_exception = False
×
834
            for e in self.dangling_exceptions.values():
×
835
                if e['path'].search(link):
×
836
                    is_exception = e['name']
×
837
                    break
×
838
            if is_exception:
×
839
                if is_exception not in self.req_names:
×
840
                    self.output.add_info('W', pkg, 'no-dependency-on',
841
                                         is_exception)
842
            else:
×
843
                self.output.add_info('W', pkg, 'dangling-symlink', fname, link)
×
844
        linktop = r.group(1)
845

×
846
        r = absolute_regex.search(fname)
×
847
        if r:
×
848
            filetop = r.group(1)
×
849
            if filetop == linktop or self.use_relative_symlinks:
×
850
                self.output.add_info('W', pkg, 'symlink-should-be-relative',
851
                                     fname, link)
852

4✔
853
    def _check_file_link_relative(self, pkg, fname, pkgfile):
4✔
854
        link = pkgfile.linkto
855

856
        # relative link
4✔
857
        r = absolute_regex.search(link)
4✔
858
        if r:
×
859
            return
860

4✔
861
        is_so = sofile_regex.search(fname)
4✔
862
        if not is_so:
4✔
863
            abslink = f'{Path(fname).parent}/{link}'
4✔
864
            abslink = os.path.normpath(abslink)
4✔
865
            if abslink not in pkg.files and abslink not in self.req_names:
×
866
                is_exception = False
×
867
                for e in self.dangling_exceptions.values():
×
868
                    if e['path'].search(link):
×
869
                        is_exception = e['name']
×
870
                        break
×
871
                if is_exception:
×
872
                    if is_exception not in self.req_names:
×
873
                        self.output.add_info('W', pkg, 'no-dependency-on',
874
                                             is_exception)
875
                else:
×
876
                    self.output.add_info('W', pkg, 'dangling-relative-symlink',
877
                                         fname, link)
4✔
878
        pathcomponents = fname.split('/')[1:]
4✔
879
        r = points_regex.search(link)
4✔
880
        lastpop = None
4✔
881
        mylink = None
882

4✔
883
        while r:
4✔
884
            mylink = r.group(1)
4✔
885
            if len(pathcomponents) == 0:
×
886
                self.output.add_info('E', pkg, 'symlink-has-too-many-up-segments',
887
                                     fname, link)
×
888
                break
889
            else:
4✔
890
                lastpop = pathcomponents[0]
4✔
891
                pathcomponents = pathcomponents[1:]
4✔
892
                r = points_regex.search(mylink)
893

4✔
894
        if mylink and lastpop:
4✔
895
            r = absolute2_regex.search(mylink)
4✔
896
            linktop = r.group(1)
897

898
            # does the link go up and then down into the same
899
            # directory?
900
            # if linktop == lastpop:
901
            #    self.output.add_info('W', pkg, 'lengthy-symlink', f, link)
902

903
            # have we reached the root directory?
4✔
904
            if len(pathcomponents) == 0 and linktop != lastpop \
905
                    and not self.use_relative_symlinks:
906
                # relative link into other toplevel directory
×
907
                self.output.add_info('W', pkg, 'symlink-should-be-absolute', fname,
908
                                     link)
909
            # check additional segments for mistakes like
910
            # `foo/../bar/'
4✔
911
            for linksegment in mylink.split('/'):
4✔
912
                if linksegment == '..':
×
913
                    self.output.add_info('E',
914
                                         pkg,
915
                                         'symlink-contains-up-and-down-segments',
916
                                         fname, link)
917

4✔
918
    def _check_file_link_bindir_shebang(self, pkg, fname, pkgfile):
4✔
919
        basedir = Path(fname).parent
4✔
920
        linkto = str((basedir / Path(pkgfile.linkto)).resolve())
921
        # Link to a file not in the package, so ignore
4✔
922
        if linkto not in pkg.files:
×
923
            return
924

4✔
925
        realbin = pkg.files[linkto]
926
        # Link to something in bindir is okay
4✔
927
        if bin_regex.search(realbin.name):
2✔
928
            return
4✔
929
        if not stat.S_ISREG(realbin.mode):
×
930
            return
931

4✔
932
        file_chunk, file_istext = self.peek(realbin.path, pkg)
4✔
933
        file_interpreter, _file_interpreter_args = script_interpreter(file_chunk)
934
        # Not a script with shebang, so ignore
4✔
935
        if not file_interpreter:
4✔
936
            return
937

938
        # If the shebang interpreter is a dependency, it's okay
4✔
939
        deps = [x[0] for x in pkg.requires]
4✔
940
        if file_interpreter in deps:
4✔
941
            return
942

4✔
943
        self.output.add_info('W', pkg, 'symlink-to-binary-with-shebang', fname,
944
                             f'is a link to a script ({realbin.name}) but missing'
945
                             f' requires for {file_interpreter}')
946

4✔
947
    def _check_file_link(self, pkg, fname, pkgfile):
4✔
948
        if not stat.S_ISLNK(pkgfile.mode):
4✔
949
            return
950

4✔
951
        self._check_file_link_devel(pkg, fname, pkgfile)
4✔
952
        self._check_file_link_man(pkg, fname)
4✔
953
        self._check_file_link_bindir_exes(pkg, fname)
4✔
954
        self._check_file_link_absolute(pkg, fname, pkgfile)
4✔
955
        self._check_file_link_relative(pkg, fname, pkgfile)
4✔
956
        self._check_file_link_bindir_shebang(pkg, fname, pkgfile)
957

4✔
958
    def _check_file_dir(self, pkg, fname, pkgfile):
4✔
959
        if not stat.S_ISDIR(pkgfile.mode):
4✔
960
            return
961

4✔
962
        mode = pkgfile.mode
4✔
963
        perm = mode & 0o7777
4✔
964
        if mode & 0o1002 == 2:  # world writable w/o sticky bit
×
965
            self.output.add_info('E', pkg, 'world-writable', fname, '%o' % perm)
4✔
966
        if perm != 0o755:
4✔
967
            self.output.add_info('E', pkg, 'non-standard-dir-perm', fname, '%o' % perm)
4✔
968
        if pkg.name not in filesys_packages and fname in STANDARD_DIRS:
×
969
            self.output.add_info('E', pkg, 'standard-dir-owned-by-package', fname)
4✔
970
        if hidden_file_regex.search(fname) and not fname.endswith('/.build-id'):
×
971
            self.output.add_info('W', pkg, 'hidden-file-or-dir', fname)
972

4✔
973
    def _check_file_normal_file(self, pkg, fname, pkgfile):
4✔
974
        if not stat.S_ISREG(pkgfile.mode):
4✔
975
            return
976

4✔
977
        self._file_nonexec_file = False
4✔
978
        self._file_chunk = None
4✔
979
        self._file_istext = False
4✔
980
        self._file_interpreter = None
4✔
981
        self._file_interpreter_args = None
4✔
982
        self._file_is_buildconfig = False
983

984
        # set[ug]id bit check
4✔
985
        self._check_file_normal_file_setuid_bit(pkg, fname, pkgfile)
4✔
986
        self._check_file_normal_file_libfile(pkg, fname)
4✔
987
        self._check_file_normal_file_logfile(pkg, fname, pkgfile)
988
        # Fill class attributes, chunk, istext, interpreter, is_buildconfig
4✔
989
        self._check_file_normal_file_getdata(pkg, fname, pkgfile)
4✔
990
        self._check_file_normal_file_doc(pkg, fname)
4✔
991
        self._check_file_normal_file_non_devel(pkg, fname)
4✔
992
        self._check_file_normal_file_lib(pkg, fname, pkgfile)
4✔
993
        self._check_file_normal_file_depmod_call(pkg, fname)
4✔
994
        self._check_file_normal_file_install_info(pkg, fname)
4✔
995
        self._check_file_normal_file_perl_temp(pkg, fname)
4✔
996
        self._check_file_normal_file_rpaths_in_buildconfig(pkg, fname)
997
        # look for man pages
4✔
998
        self._check_file_normal_file_look_for_man(pkg, fname)
4✔
999
        self._check_file_normal_file_bin(pkg, fname, pkgfile)
4✔
1000
        self._check_file_normal_file_devel(pkg, fname, pkgfile)
4✔
1001
        self._check_file_normal_file_non_readable(pkg, fname, pkgfile)
4✔
1002
        self._check_file_normal_file_zero_length(pkg, fname, pkgfile)
4✔
1003
        self._check_file_normal_file_world_w(pkg, fname, pkgfile)
4✔
1004
        self._check_file_normal_file_perl_dep(pkg, fname)
4✔
1005
        self._check_file_normal_file_python_dep(pkg, fname)
4✔
1006
        self._check_file_normal_file_python_source(pkg, fname)
1007
        # normal executable check
4✔
1008
        self._check_file_normal_file_exec(pkg, fname, pkgfile)
4✔
1009
        self._check_file_normal_file_non_conf_in_etc(pkg, fname)
4✔
1010
        self._check_file_normal_file_python_noarch(pkg, fname)
4✔
1011
        self._check_file_normal_file_debuginfo(pkg, fname)
4✔
1012
        self._check_file_normal_file_gzipped_svg(pkg, fname)
4✔
1013
        self._check_file_normal_file_pem(pkg, fname)
4✔
1014
        self._check_file_normal_file_tcl(pkg, fname)
1015
        # text file checks
4✔
1016
        self._check_file_normal_file_text(pkg, fname, pkgfile)
4✔
1017
        self._check_file_normal_file_not_utf8(pkg, fname, pkgfile)
1018

4✔
1019
    def _check_file_normal_file_setuid_bit(self, pkg, fname, pkgfile):
4✔
1020
        user = pkgfile.user
4✔
1021
        group = pkgfile.group
4✔
1022
        mode = pkgfile.mode
4✔
1023
        perm = mode & 0o7777
4✔
1024
        if stat.S_ISGID & mode or stat.S_ISUID & mode:
×
1025
            if stat.S_ISUID & mode:
×
1026
                self.output.add_info('E', pkg, 'setuid-binary', fname, user, '%o' % perm)
×
1027
            if (stat.S_ISGID & mode and
1028
                    not (group == 'games' and
1029
                         (games_path_regex.search(fname) or
1030
                          self.games_group_regex.search(pkg[rpm.RPMTAG_GROUP])))):
×
1031
                self.output.add_info('E', pkg, 'setgid-binary', fname, group,
1032
                                     '%o' % perm)
×
1033
            if mode & 0o777 != 0o755:
×
1034
                self.output.add_info('E', pkg, 'non-standard-executable-perm', fname,
1035
                                     '%o' % perm)
1036

4✔
1037
    def _check_file_normal_file_libfile(self, pkg, fname):
4✔
1038
        is_doc = fname in pkg.doc_files
1039

4✔
1040
        if not self.devel_pkg:
4✔
1041
            if lib_path_regex.search(fname):
4✔
1042
                self.lib_file = True
4✔
1043
            elif not is_doc:
4✔
1044
                self.non_lib_file = fname
1045

4✔
1046
    def _check_file_normal_file_logfile(self, pkg, fname, pkgfile):
4✔
1047
        user = pkgfile.user
4✔
1048
        group = pkgfile.group
1049

4✔
1050
        if log_regex.search(fname):
×
1051
            self._file_nonexec_file = True
×
1052
            if user != 'root':
×
1053
                self.output.add_info('E', pkg, 'non-root-user-log-file', fname, user)
×
1054
            if group != 'root':
×
1055
                self.output.add_info('E', pkg, 'non-root-group-log-file', fname, group)
×
1056
            if fname not in self.ghost_files:
×
1057
                self.output.add_info('E', pkg, 'non-ghost-file', fname)
1058

4✔
1059
    def _check_file_normal_file_getdata(self, pkg, fname, pkgfile):
4✔
1060
        res = None
4✔
1061
        try:
4✔
1062
            res = os.access(pkgfile.path, os.R_OK)
×
1063
        except UnicodeError as e:  # e.g. non-ASCII, C locale, python 3
×
1064
            self.output.add_info('W', pkg, 'inaccessible-filename', fname, e)
1065
        else:
4✔
1066
            if res:
4✔
1067
                (self._file_chunk, self._file_istext) = self.peek(pkgfile.path, pkg)
1068

4✔
1069
        (self._file_interpreter, self._file_interpreter_args) = script_interpreter(self._file_chunk)
4✔
1070
        self._file_is_buildconfig = self._file_istext and buildconfigfile_regex.search(fname)
1071

4✔
1072
    def _check_file_normal_file_doc(self, pkg, fname):
4✔
1073
        is_doc = fname in pkg.doc_files
4✔
1074
        if doc_regex.search(fname):
4✔
1075
            if not self._file_interpreter:
4✔
1076
                self._file_nonexec_file = True
4✔
1077
            if not is_doc:
4✔
1078
                self.output.add_info('E', pkg, 'not-listed-as-documentation', fname)
1079

4✔
1080
    def _check_file_normal_file_non_devel(self, pkg, fname):
4✔
1081
        if self.devel_pkg and fname.endswith('.typelib'):
4✔
1082
            self.output.add_info('E', pkg, 'non-devel-file-in-devel-package', fname)
1083

4✔
1084
    def _check_file_normal_file_lib(self, pkg, fname, pkgfile):
1085
        # check ldconfig call in %post and %postun
4✔
1086
        mode = pkgfile.mode
4✔
1087
        if lib_regex.search(fname):
4✔
1088
            if self.devel_pkg and not (sofile_regex.search(fname) and stat.S_ISLNK(mode)):
4✔
1089
                self.output.add_info('E', pkg, 'non-devel-file-in-devel-package', fname)
4✔
1090
            if not self.postin:
4✔
1091
                self.output.add_info('E', pkg, 'library-without-ldconfig-postin', fname)
1092
            else:
2✔
1093
                if not ldconfig_regex.search(self.postin):
1✔
1094
                    self.output.add_info('E', pkg, 'postin-without-ldconfig', fname)
1095

4✔
1096
            if not self.postun:
4✔
1097
                self.output.add_info('E', pkg, 'library-without-ldconfig-postun', fname)
1098
            else:
1✔
1099
                if not ldconfig_regex.search(self.postun):
×
1100
                    self.output.add_info('E', pkg, 'postun-without-ldconfig', fname)
1101

4✔
1102
    def _check_file_normal_file_depmod_call(self, pkg, fname):
1103
        # check depmod call in %post and %postun
4✔
1104
        res = not self.is_kernel_package and kernel_modules_regex.search(fname)
4✔
1105
        if res:
×
1106
            kernel_version = res.group(1)
×
1107
            kernel_version_regex = re.compile(
1108
                r'\bdepmod\s+-a.*F\s+/boot/System\.map-' +
1109
                re.escape(kernel_version) + r'\b.*\b' +
1110
                re.escape(kernel_version) + r'\b',
1111
                re.MULTILINE | re.DOTALL)
1112

×
1113
            if not self.postin or not depmod_regex.search(self.postin):
×
1114
                self.output.add_info('E', pkg, 'module-without-depmod-postin', fname)
1115
            # check that we run depmod on the right kernel
×
1116
            elif not kernel_version_regex.search(self.postin):
×
1117
                self.output.add_info('E', pkg, 'postin-with-wrong-depmod', fname)
1118

×
1119
            if not self.postun or not depmod_regex.search(self.postun):
×
1120
                self.output.add_info('E', pkg, 'module-without-depmod-postun', fname)
1121
            # check that we run depmod on the right kernel
×
1122
            elif not kernel_version_regex.search(self.postun):
×
1123
                self.output.add_info('E', pkg, 'postun-with-wrong-depmod', fname)
1124

4✔
1125
    def _check_file_normal_file_install_info(self, pkg, fname):
1126
        # check install-info call in %post and %postun
4✔
1127
        if fname.startswith('/usr/share/info/'):
×
1128
            if not self.postin:
×
1129
                self.output.add_info('E', pkg,
1130
                                     'info-files-without-install-info-postin', fname)
×
1131
            elif not install_info_regex.search(self.postin):
×
1132
                self.output.add_info('E', pkg, 'postin-without-install-info', fname)
1133

×
1134
            preun = pkg[rpm.RPMTAG_PREUN] or \
1135
                pkg.scriptprog(rpm.RPMTAG_PREUNPROG)
×
1136
            if not self.postun and not preun:
×
1137
                self.output.add_info('E', pkg,
1138
                                     'info-files-without-install-info-postun', fname)
×
1139
            elif not ((self.postun and install_info_regex.search(self.postun)) or
1140
                      (preun and install_info_regex.search(preun))):
×
1141
                self.output.add_info('E', pkg, 'postin-without-install-info', fname)
1142

4✔
1143
    def _check_file_normal_file_perl_temp(self, pkg, fname):
1144
        # check perl temp file
4✔
1145
        if perl_temp_file_regex.search(fname):
×
1146
            self.output.add_info('W', pkg, 'perl-temp-file', fname)
1147

4✔
1148
    def _check_file_normal_file_rpaths_in_buildconfig(self, pkg, fname):
1149
        # check rpaths in buildconfig files
4✔
1150
        if self._file_is_buildconfig:
×
1151
            ln = pkg.grep(buildconfig_rpath_regex, fname)
×
1152
            if ln:
×
1153
                self.output.add_info('E', pkg, 'rpath-in-buildconfig', fname, 'lines', ln)
1154

4✔
1155
    def _check_file_normal_file_look_for_man(self, pkg, fname):
4✔
1156
        res = man_base_regex.fullmatch(fname)
4✔
1157
        if res:
4✔
1158
            self.man_basenames.add(res.group('binary'))
1159

4✔
1160
    def _check_file_normal_file_bin(self, pkg, fname, pkgfile):
4✔
1161
        mode = pkgfile.mode
4✔
1162
        perm = mode & 0o7777
4✔
1163
        mode_is_exec = mode & 0o111
4✔
1164
        res = bin_regex.search(fname)
4✔
1165
        if res:
4✔
1166
            if not mode_is_exec:
4✔
1167
                self.output.add_info('W', pkg, 'non-executable-in-bin', fname,
1168
                                     '%o' % perm)
1169
            else:
4✔
1170
                exe = res.group(1)
4✔
1171
                if '/' not in exe:
4✔
1172
                    self.bindir_exes.setdefault(exe, []).append(fname)
1173

4✔
1174
    def _check_file_normal_file_devel(self, pkg, fname, pkgfile):
4✔
1175
        is_doc = fname in pkg.doc_files
4✔
1176
        if (not self.devel_pkg and not is_doc and
1177
            (self._file_is_buildconfig or includefile_regex.search(fname) or
1178
             develfile_regex.search(fname))):
×
1179
            self.output.add_info('W', pkg, 'devel-file-in-non-devel-package', fname)
1180

4✔
1181
    def _check_file_normal_file_non_readable(self, pkg, fname, pkgfile):
1182
        # Do not check permissions for ghosts files
1183
        # https://github.com/rpm-software-management/rpmlint/issues/1287
4✔
1184
        if pkgfile.is_ghost:
4✔
1185
            return
1186

4✔
1187
        mode = pkgfile.mode
4✔
1188
        perm = mode & 0o7777
4✔
1189
        if mode & 0o444 != 0o444 and perm & 0o7000 == 0:
×
1190
            ok_nonreadable = False
×
1191
            for regex in non_readable_regexs:
×
1192
                if regex.search(fname):
×
1193
                    ok_nonreadable = True
×
1194
                    break
×
1195
            if not ok_nonreadable:
×
1196
                self.output.add_info('E', pkg, 'non-readable', fname, '%o' % perm)
1197

4✔
1198
    def _check_file_normal_file_zero_length(self, pkg, fname, pkgfile):
4✔
1199
        size = pkgfile.size
4✔
1200
        if size == 0 and not normal_zero_length_regex.search(fname) and \
1201
                fname not in self.ghost_files:
4✔
1202
            self.output.add_info('E', pkg, 'zero-length', fname)
1203

4✔
1204
    def _check_file_normal_file_world_w(self, pkg, fname, pkgfile):
4✔
1205
        mode = pkgfile.mode
4✔
1206
        perm = mode & 0o7777
4✔
1207
        if mode & stat.S_IWOTH:
×
1208
            self.output.add_info('E', pkg, 'world-writable', fname, '%o' % perm)
1209

4✔
1210
    def _check_file_normal_file_perl_dep(self, pkg, fname):
4✔
1211
        if not self.perl_dep_error:
4✔
1212
            res = perl_regex.search(fname)
4✔
1213
            if res:
×
1214
                deps = [x[0] for x in pkg.requires + pkg.recommends + pkg.suggests]
×
1215
                if self.perl_version_trick:
×
1216
                    vers = res.group(1) + '.' + res.group(2)
1217
                else:
×
1218
                    vers = res.group(1) + res.group(2)
×
1219
                if not (pkg.check_versioned_dep('perl-base', vers) or
1220
                        pkg.check_versioned_dep('perl', vers) or
1221
                        f'perl(:MODULE_COMPAT_{vers})' in deps):
×
1222
                    self.output.add_info('E', pkg, 'no-dependency-on',
1223
                                         'perl-base', vers)
×
1224
                    self.perl_dep_error = True
1225

4✔
1226
    def _check_file_normal_file_python_dep(self, pkg, fname):
4✔
1227
        if not self.python_dep_error:
4✔
1228
            res = python_regex.search(fname)
4✔
1229
            if (res and not
1230
                    any(pkg.check_versioned_dep(dep, res.group(1))
1231
                        for dep in (
1232
                            'python', 'python-base', 'python(abi)'))):
×
1233
                self.output.add_info('E', pkg, 'no-dependency-on', 'python-base',
1234
                                     res.group(1))
×
1235
                self.python_dep_error = True
1236

4✔
1237
    def _check_file_normal_file_python_source(self, pkg, fname):
4✔
1238
        source_file = python_bytecode_to_script(fname)
4✔
1239
        if not source_file:
4✔
1240
            return
1241

4✔
1242
        if source_file in pkg.files:
4✔
1243
            if self._file_chunk:
1244
                # Verify that the magic ABI value embedded in the
1245
                # .pyc header is correct
4✔
1246
                found_magic = pyc_magic_from_chunk(self._file_chunk)
4✔
1247
                exp_magic, exp_version = get_expected_pyc_magic(fname, self.python_default_version)
4✔
1248
                if exp_magic and found_magic not in exp_magic:
4✔
1249
                    found_version = 'unknown'
4✔
1250
                    for (pv, pm) in _python_magic_values.items():
4✔
1251
                        if found_magic in pm:
×
1252
                            found_version = pv
×
1253
                            break
1254
                    # If expected version was from the file path,
1255
                    # issue # an error, otherwise a warning.
4✔
1256
                    msg = (pkg,
1257
                           'python-bytecode-wrong-magic-value',
1258
                           fname, 'expected %s (%s), found %d (%s)' %
1259
                           (' or '.join(map(str, exp_magic)),
1260
                            exp_version or self.python_default_version,
1261
                            found_magic, found_version))
4✔
1262
                    if exp_version is not None:
4✔
1263
                        self.output.add_info('E', *msg)
1264
                    else:
×
1265
                        self.output.add_info('W', *msg)
1266

1267
                # Verify that the timestamp embedded in the .pyc
1268
                # header matches the mtime of the .py file:
4✔
1269
                pyc_timestamp = pyc_mtime_from_chunk(self._file_chunk)
1270
                # If it's a symlink, check target file mtime.
4✔
1271
                srcfile = pkg.readlink(pkg.files[source_file])
4✔
1272
                if not srcfile:
×
1273
                    self.output.add_info('W', pkg, 'python-bytecode-without-source', fname)
4✔
1274
                elif (pyc_timestamp is not None and
1275
                      pyc_timestamp != srcfile.mtime):
×
1276
                    cts = datetime.fromtimestamp(
1277
                        pyc_timestamp).isoformat()
×
1278
                    sts = datetime.fromtimestamp(
1279
                        srcfile.mtime).isoformat()
×
1280
                    self.output.add_info('E',
1281
                                         pkg, 'python-bytecode-inconsistent-mtime',
1282
                                         fname, cts, srcfile.name, sts)
1283
        else:
×
1284
            self.output.add_info('W', pkg, 'python-bytecode-without-source', fname)
1285

4✔
1286
    def _check_file_normal_file_exec(self, pkg, fname, pkgfile):
4✔
1287
        mode = pkgfile.mode
4✔
1288
        perm = mode & 0o7777
4✔
1289
        mode_is_exec = mode & 0o111
4✔
1290
        if mode & stat.S_IXUSR and perm != 0o755:
×
1291
            self.output.add_info('E', pkg, 'non-standard-executable-perm',
1292
                                 fname, '%o' % perm)
4✔
1293
        if mode_is_exec:
4✔
1294
            if fname in self.config_files:
×
1295
                self.output.add_info('E', pkg, 'executable-marked-as-config-file', fname)
4✔
1296
            if not self._file_nonexec_file:
1297
                # doc_regex and log_regex checked earlier, no match,
1298
                # check rest of usual cases here.  Sourced scripts have
1299
                # their own check, so disregard them here.
4✔
1300
                self._file_nonexec_file = fname.endswith('.pc') or \
1301
                    compr_regex.search(fname) or \
1302
                    includefile_regex.search(fname) or \
1303
                    develfile_regex.search(fname) or \
1304
                    logrotate_regex.search(fname)
4✔
1305
            if self._file_nonexec_file:
×
1306
                self.output.add_info('W', pkg, 'spurious-executable-perm', fname)
1307

4✔
1308
    def _check_file_normal_file_non_conf_in_etc(self, pkg, fname):
4✔
1309
        if fname.startswith('/etc/') and fname not in self.config_files and \
1310
                fname not in self.ghost_files and not fname.startswith('/etc/ld.so.conf.d/'):
4✔
1311
            self.output.add_info('W', pkg, 'non-conffile-in-etc', fname)
1312

4✔
1313
    def _check_file_normal_file_python_noarch(self, pkg, fname):
4✔
1314
        if pkg.arch == 'noarch' and fname.startswith('/usr/lib64/python'):
×
1315
            self.output.add_info('E', pkg, 'noarch-python-in-64bit-path', fname)
1316

4✔
1317
    def _check_file_normal_file_debuginfo(self, pkg, fname):
4✔
1318
        if self.debuginfo_package:
×
1319
            if fname.endswith('.debug'):
×
1320
                self.debuginfo_debugs = True
1321
            else:
×
1322
                self.debuginfo_srcs = True
1323

4✔
1324
    def _check_file_normal_file_gzipped_svg(self, pkg, fname):
4✔
1325
        if fname.endswith('.svgz') and fname[0:-1] not in pkg.files \
1326
                and scalable_icon_regex.search(fname):
×
1327
            self.output.add_info('W', pkg, 'gzipped-svg-icon', fname)
1328

4✔
1329
    def _check_file_normal_file_pem(self, pkg, fname):
4✔
1330
        if fname.endswith('.pem') and fname not in self.ghost_files:
×
1331
            if pkg.grep(start_certificate_regex, fname):
×
1332
                self.output.add_info('W', pkg, 'pem-certificate', fname)
×
1333
            if pkg.grep(start_private_key_regex, fname):
×
1334
                self.output.add_info('E', pkg, 'pem-private-key', fname)
1335

4✔
1336
    def _check_file_normal_file_tcl(self, pkg, fname):
4✔
1337
        if tcl_regex.search(fname):
4✔
1338
            self.output.add_info('E', pkg, 'tcl-extension-file', fname)
1339

4✔
1340
    def _check_file_normal_file_text(self, pkg, fname, pkgfile):
4✔
1341
        if not self._file_istext:
4✔
1342
            return
1343

4✔
1344
        mode = pkgfile.mode
4✔
1345
        perm = mode & 0o7777
4✔
1346
        mode_is_exec = mode & 0o111
4✔
1347
        is_doc = fname in pkg.doc_files
1348

1349
        # ignore perl module shebang -- TODO: disputed...
4✔
1350
        if fname.endswith('.pm'):
×
1351
            self._file_interpreter = None
1352
        # sourced scripts should not be executable
4✔
1353
        if sourced_script_regex.search(fname):
×
1354
            if self._file_interpreter:
×
1355
                self.output.add_info('E', pkg,
1356
                                     'sourced-script-with-shebang', fname,
1357
                                     self._file_interpreter, self._file_interpreter_args)
×
1358
            if mode_is_exec:
×
1359
                self.output.add_info('E', pkg, 'executable-sourced-script',
1360
                                     fname, '%o' % perm)
1361
        # ...but executed ones should
4✔
1362
        elif self._file_interpreter or mode_is_exec or script_regex.search(fname):
4✔
1363
            if self._file_interpreter:
4✔
1364
                res = interpreter_regex.search(self._file_interpreter)
4✔
1365
                if (mode_is_exec or script_regex.search(fname)):
4✔
1366
                    if res and res.group(1) == 'env':
×
1367
                        self.output.add_info('E', pkg, 'env-script-interpreter',
1368
                                             fname, self._file_interpreter,
1369
                                             self._file_interpreter_args)
4✔
1370
                    elif not res:
4✔
1371
                        self.output.add_info('E', pkg, 'wrong-script-interpreter',
1372
                                             fname, self._file_interpreter,
1373
                                             self._file_interpreter_args)
4✔
1374
            elif not self._file_nonexec_file and not \
1375
                    (lib_path_regex.search(fname) and
1376
                     fname.endswith('.la')):
4✔
1377
                self.output.add_info('E', pkg, 'script-without-shebang', fname)
1378

4✔
1379
            if not mode_is_exec and not is_doc and \
1380
                    self._file_interpreter and self._file_interpreter.startswith('/'):
1✔
1381
                self.output.add_info('E', pkg, 'non-executable-script', fname,
1382
                                     '%o' % perm, self._file_interpreter,
1383
                                     self._file_interpreter_args)
4✔
1384
            if b'\r' in self._file_chunk:
×
1385
                self.output.add_info('E', pkg, 'wrong-script-end-of-line-encoding', fname)
4✔
1386
        elif is_doc and not self.skipdocs_regex.search(fname):
4✔
1387
            if b'\r' in self._file_chunk:
×
1388
                self.output.add_info('W', pkg, 'wrong-file-end-of-line-encoding', fname)
1389
            # We check only doc text files for UTF-8-ness;
1390
            # checking everything may be slow and can generate
1391
            # lots of unwanted noise.
4✔
1392
            if not is_utf8(pkgfile.path):
×
1393
                self.output.add_info('W', pkg, 'file-not-utf8', fname)
4✔
1394
        if fsf_license_regex.search(self._file_chunk) and \
1395
                fsf_wrong_address_regex.search(self._file_chunk):
4✔
1396
            self.output.add_info('E', pkg, 'incorrect-fsf-address', fname)
1397

4✔
1398
    def _check_file_normal_file_not_utf8(self, pkg, fname, pkgfile):
4✔
1399
        is_doc = fname in pkg.doc_files
4✔
1400
        if not self._file_istext and is_doc and self._file_chunk and compr_regex.search(fname):
4✔
1401
            ff = compr_regex.sub('', fname)
1402
            # compressed docs, eg. info and man files etc
4✔
1403
            if not self.skipdocs_regex.search(ff) and not is_utf8(pkgfile.path):
4✔
1404
                self.output.add_info('W', pkg, 'file-not-utf8', fname)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc