Skip to content

Commit a321b7c

Browse files
committed
Simplify and fix code related to mode-like options
- Simplify parsing for all mode-like options (including `os.open()`) - Fix reading of options from configuration file - Fix pylint messages shown to users - Update CHANGELOG Also somewhat unrelated: - Fix constructor arguments
1 parent ae267df commit a321b7c

File tree

6 files changed

+88
-136
lines changed

6 files changed

+88
-136
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717

1818
- Refactor configuration option parsing for mode-like options
1919

20+
### Fixed
21+
22+
- Critical typo for `msgs` attribute of the plugin class. This effectively rendered any previous version useless as
23+
pylint would not recognize the warning/error messages
24+
2025
### Repository
2126

2227
- Restrict running some GitHub actions when a pull request is merged

pylint_secure_coding_standard.py

Lines changed: 55 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -153,42 +153,21 @@ def _is_builtin_open_for_writing(node):
153153
return False
154154

155155

156-
def _is_allowed_mode(node, allowed_modes):
156+
def _get_mode_arg(node, args_idx):
157157
mode = None
158-
if len(node.args) > 1 and isinstance(node.args[1], astroid.Const):
159-
mode = node.args[1].value
158+
if len(node.args) > args_idx and isinstance(node.args[args_idx], astroid.Const):
159+
mode = node.args[args_idx].value
160160
elif node.keywords:
161161
for keyword in node.keywords:
162162
if keyword.arg == 'mode' and isinstance(keyword.value, astroid.Const):
163163
mode = keyword.value.value
164164
break
165-
if mode is not None:
166-
return mode in allowed_modes
167-
168-
# NB: default to True in all other cases
169-
return True
165+
return mode
170166

171167

172-
def _is_os_open_allowed_mode(node, allowed_modes):
173-
mode = None
174-
flags = None # pylint: disable=unused-variable
175-
if len(node.args) > 1 and isinstance(node.args[1], (astroid.Attribute, astroid.BinOp)):
176-
# Cover:
177-
# * os.open(xxx, os.O_WRONLY)
178-
# * os.open(xxx, os.O_WRONLY | os.O_CREATE)
179-
# * os.open(xxx, os.O_WRONLY | os.O_CREATE | os.O_FSYNC)
180-
flags = node.args[1]
181-
if len(node.args) > 2 and isinstance(node.args[2], astroid.Const):
182-
mode = node.args[2].value
183-
elif node.keywords:
184-
for keyword in node.keywords:
185-
if keyword.arg == 'flags' and isinstance(keyword.value, (astroid.Attribute, astroid.BinOp)):
186-
flags = keyword.value # pylint: disable=unused-variable # noqa: F841
187-
if keyword.arg == 'mode' and isinstance(keyword.value, astroid.Const):
188-
mode = keyword.value.value
189-
break
168+
def _is_allowed_mode(node, allowed_modes, args_idx):
169+
mode = _get_mode_arg(node, args_idx=args_idx)
190170
if mode is not None:
191-
# TODO: condition check on the flags value if present (ie. ignore if read-only)
192171
return mode in allowed_modes
193172

194173
# NB: default to True in all other cases
@@ -303,14 +282,10 @@ def _is_yaml_unsafe_call(node):
303282
# ==============================================================================
304283

305284

306-
class SecureCodingStandardChecker(BaseChecker):
285+
class SecureCodingStandardChecker(BaseChecker): # pylint: disable=too-many-instance-attributes
307286
"""Plugin class."""
308287

309288
DEFAULT_MAX_MODE = 0o755
310-
W8012_DISPLAY_MSG = 'Avoid using `os.open` with unsafe permissions'
311-
W8016_DISPLAY_MSG = 'Avoid using `os.mkdir` and `os.makedirs` with unsafe permissions'
312-
W8017_DISPLAY_MSG = 'Avoid using `os.mkfifo` with unsafe permissions'
313-
W8018_DISPLAY_MSG = 'Avoid using `os.mknod` with unsafe permissions'
314289

315290
__implements__ = (IAstroidChecker,)
316291

@@ -420,7 +395,7 @@ class SecureCodingStandardChecker(BaseChecker):
420395
'Use of `shlex.quote()` should be avoided on non-POSIX platforms (such as Windows)',
421396
),
422397
'W8012': (
423-
W8012_DISPLAY_MSG,
398+
'Avoid using `os.open` with unsafe permissions (should be %s)',
424399
'os-open-unsafe-permissions',
425400
'Avoid using `os.open` with unsafe file permissions (by default 0 <= mode <= 0o755)',
426401
),
@@ -440,29 +415,32 @@ class SecureCodingStandardChecker(BaseChecker):
440415
'Use of `shelve.open()` should be avoided in favour of safer file formats',
441416
),
442417
'W8016': (
443-
W8016_DISPLAY_MSG,
418+
'Avoid using `os.mkdir` and `os.makedirs` with unsafe permissions (should be %s)',
444419
'os-mkdir-unsafe-permissions',
445420
'Avoid using `os.mkdir` and `os.makedirs` with unsafe file permissions (by default 0 <= mode <= 0o755)',
446421
),
447422
'W8017': (
448-
W8017_DISPLAY_MSG,
423+
'Avoid using `os.mkfifo` with unsafe permissions (should be %s)',
449424
'os-mkfifo-unsafe-permissions',
450425
'Avoid using `os.mkfifo` with unsafe file permissions (by default 0 <= mode <= 0o755)',
451426
),
452427
'W8018': (
453-
W8018_DISPLAY_MSG,
428+
'Avoid using `os.mknod` with unsafe permissions (should be %s)',
454429
'os-mknod-unsafe-permissions',
455430
'Avoid using `os.mknod` with unsafe file permissions (by default 0 <= mode <= 0o755)',
456431
),
457432
}
458433

459-
def __init__(self, *args, **kwargs):
434+
def __init__(self, linter):
460435
"""Initialize a SecureCodingStandardChecker object."""
461-
super().__init__(*args, **kwargs)
462-
self._prefer_os_open = False
436+
super().__init__(linter)
437+
self._os_open_msg_arg = ''
463438
self._os_open_modes_allowed = []
439+
self._os_mkdir_msg_arg = ''
464440
self._os_mkdir_modes_allowed = []
441+
self._os_mkfifo_msg_arg = ''
465442
self._os_mkfifo_modes_allowed = []
443+
self._os_mknod_msg_arg = ''
466444
self._os_mknod_modes_allowed = []
467445

468446
def visit_call(self, node): # pylint: disable=too-many-branches
@@ -483,18 +461,18 @@ def visit_call(self, node): # pylint: disable=too-many-branches
483461
self.add_message('avoid-shell-true', node=node)
484462
elif _is_function_call(node, module='os', function='popen'):
485463
self.add_message('avoid-os-popen', node=node)
486-
elif _is_builtin_open_for_writing(node) and self._prefer_os_open:
464+
elif _is_builtin_open_for_writing(node) and self._os_open_modes_allowed:
487465
self.add_message('replace-builtin-open', node=node)
488466
elif isinstance(node.func, astroid.Name) and (node.func.name in ('eval', 'exec')):
489467
self.add_message('avoid-eval-exec', node=node)
490468
elif not _is_posix() and _is_function_call(node, module='shlex', function='quote'):
491469
self.add_message('avoid-shlex-quote-on-non-posix', node=node)
492470
elif (
493471
_is_function_call(node, module='os', function='open')
494-
and self._prefer_os_open
495-
and not _is_os_open_allowed_mode(node, self._os_open_modes_allowed)
472+
and self._os_open_modes_allowed
473+
and not _is_allowed_mode(node, self._os_open_modes_allowed, args_idx=2)
496474
):
497-
self.add_message('os-open-unsafe-permissions', node=node)
475+
self.add_message('os-open-unsafe-permissions', node=node, args=(self._os_open_msg_arg,))
498476
elif _is_function_call(node, module='pickle', function=('load', 'loads')):
499477
self.add_message('avoid-pickle-load', node=node)
500478
elif _is_function_call(node, module='marshal', function=('load', 'loads')):
@@ -505,21 +483,21 @@ def visit_call(self, node): # pylint: disable=too-many-branches
505483
if (
506484
_is_function_call(node, module='os', function=('mkdir', 'makedirs'))
507485
and self._os_mkdir_modes_allowed
508-
and not _is_allowed_mode(node, self._os_mkdir_modes_allowed)
486+
and not _is_allowed_mode(node, self._os_mkdir_modes_allowed, args_idx=1)
509487
):
510-
self.add_message('os-mkdir-unsafe-permissions', node=node)
488+
self.add_message('os-mkdir-unsafe-permissions', node=node, args=(self._os_mkdir_msg_arg,))
511489
elif (
512490
_is_function_call(node, module='os', function='mkfifo')
513491
and self._os_mkfifo_modes_allowed
514-
and not _is_allowed_mode(node, self._os_mkfifo_modes_allowed)
492+
and not _is_allowed_mode(node, self._os_mkfifo_modes_allowed, args_idx=1)
515493
):
516-
self.add_message('os-mkfifo-unsafe-permissions', node=node)
494+
self.add_message('os-mkfifo-unsafe-permissions', node=node, args=(self._os_mkfifo_msg_arg,))
517495
elif (
518496
_is_function_call(node, module='os', function='mknod')
519497
and self._os_mknod_modes_allowed
520-
and not _is_allowed_mode(node, self._os_mknod_modes_allowed)
498+
and not _is_allowed_mode(node, self._os_mknod_modes_allowed, args_idx=1)
521499
):
522-
self.add_message('os-mknod-unsafe-permissions', node=node)
500+
self.add_message('os-mknod-unsafe-permissions', node=node, args=(self._os_mknod_msg_arg,))
523501

524502
def visit_import(self, node):
525503
"""Visitor method called for astroid.Import nodes."""
@@ -560,11 +538,11 @@ def visit_with(self, node):
560538
"""Visitor method called for astroid.With nodes."""
561539
for item in node.items:
562540
if item and isinstance(item[0], astroid.Call):
563-
if self._prefer_os_open:
541+
if self._os_open_modes_allowed:
564542
if _is_builtin_open_for_writing(item[0]):
565543
self.add_message('replace-builtin-open', node=node)
566-
elif _is_function_call(item[0], module='os', function='open') and not _is_os_open_allowed_mode(
567-
item[0], self._os_open_modes_allowed
544+
elif _is_function_call(item[0], module='os', function='open') and not _is_allowed_mode(
545+
item[0], self._os_open_modes_allowed, args_idx=2
568546
):
569547
self.add_message('os-open-unsafe-permissions', node=node)
570548
elif _is_function_call(item[0], module='shelve', function='open'):
@@ -574,55 +552,30 @@ def visit_assert(self, node):
574552
"""Visitor method called for astroid.Assert nodes."""
575553
self.add_message('avoid-assert', node=node)
576554

577-
def set_os_open_mode(self, value):
578-
"""
579-
Control whether we prefer `os.open` over the builtin `open`.
580-
581-
Args:
582-
value (str): Option value
583-
"""
555+
def _set_mode_option(self, config_name, name, value):
556+
modes = _read_octal_mode_option(config_name, value, self.DEFAULT_MAX_MODE)
584557

585-
def _update_display_msg(suffix=''):
586-
self.msg['W8012'] = (self.W8012_DISPLAY_MSG + suffix, self.msg['W8012'][1], self.msg['W8012'][2])
587-
588-
modes = _read_octal_mode_option('os_open_mode', value, list(range(0, self.DEFAULT_MAX_MODE + 1)))
589-
590-
self._os_open_modes_allowed.clear()
591-
592-
if isinstance(modes, int):
593-
self._prefer_os_open = modes > 0
594-
if self._prefer_os_open:
595-
self._os_open_modes_allowed = list(range(0, modes + 1))
596-
_update_display_msg(suffix=f' (mode <= {value})')
597-
elif not modes:
598-
self._prefer_os_open = False
599-
else:
600-
self._prefer_os_open = True
601-
self._os_open_modes_allowed = modes
602-
_update_display_msg(suffix=f' (mode in {modes})')
603-
604-
def _set_mode_option(self, config_name, name, value, warning_id):
605-
def _update_display_msg(suffix=''):
606-
self.msg[warning_id] = (
607-
getattr(self, f'{warning_id}_DISPLAY_MSG') + suffix,
608-
self.msg[warning_id][1],
609-
self.msg[warning_id][2],
610-
)
611-
612-
modes = _read_octal_mode_option(config_name, value, list(range(0, self.DEFAULT_MAX_MODE + 1)))
613-
614-
if isinstance(modes, int):
615-
if modes > 0:
616-
setattr(self, f'_os_{name}_modes_allowed', list(range(0, modes + 1)))
617-
_update_display_msg(suffix=f' (mode <= {value})')
618-
else:
619-
getattr(self, f'_os_{name}_modes_allowed').clear()
558+
if isinstance(modes, int) and modes > 0:
559+
setattr(self, f'_os_{name}_modes_allowed', list(range(0, modes + 1)))
560+
setattr(self, f'_os_{name}_msg_arg', f'0 < mode < {oct(modes)}')
620561
elif modes:
621562
setattr(self, f'_os_{name}_modes_allowed', modes)
622-
_update_display_msg(suffix=f' (mode in {modes})')
563+
setattr(self, f'_os_{name}_msg_arg', f'mode in {[oct(mode) for mode in modes]}')
623564
else:
624565
getattr(self, f'_os_{name}_modes_allowed').clear()
625566

567+
def set_os_open_allowed_modes(self, value):
568+
"""
569+
Set the allowed modes for `os.open`.
570+
571+
Note:
572+
This option has no effect on non-POSIX platforms.
573+
574+
Args:
575+
value (str): Option value
576+
"""
577+
self._set_mode_option('os_open_mode', 'open', value)
578+
626579
def set_os_mkdir_allowed_modes(self, value):
627580
"""
628581
Set the allowed modes for `os.mkdir` and `os.makedirs`.
@@ -633,7 +586,7 @@ def set_os_mkdir_allowed_modes(self, value):
633586
Args:
634587
value (str): Option value
635588
"""
636-
self._set_mode_option('os_mkdir_mode', 'mkdir', value, 'W8016')
589+
self._set_mode_option('os_mkdir_mode', 'mkdir', value)
637590

638591
def set_os_mkfifo_allowed_modes(self, value):
639592
"""
@@ -645,7 +598,7 @@ def set_os_mkfifo_allowed_modes(self, value):
645598
Args:
646599
value (str): Option value
647600
"""
648-
self._set_mode_option('os_mkfifo_mode', 'mkfifo', value, 'W8017')
601+
self._set_mode_option('os_mkfifo_mode', 'mkfifo', value)
649602

650603
def set_os_mknod_allowed_modes(self, value):
651604
"""
@@ -657,7 +610,7 @@ def set_os_mknod_allowed_modes(self, value):
657610
Args:
658611
value (str): Option value
659612
"""
660-
self._set_mode_option('os_mknod_mode', 'mknod', value, 'W8018')
613+
self._set_mode_option('os_mknod_mode', 'mknod', value)
661614

662615

663616
def register(linter): # pragma: no cover
@@ -669,8 +622,7 @@ def load_configuration(linter): # pragma: no cover
669622
"""Load data from the configuration file."""
670623
for checker in linter.get_checkers():
671624
if isinstance(checker, SecureCodingStandardChecker):
672-
checker.set_os_open_mode(checker.config.os_open_mode)
673-
checker.set_os_open_mode(checker.config.os_mkdir_mode)
674-
checker.set_os_open_mode(checker.config.os_mkfifo_mode)
675-
checker.set_os_open_mode(checker.config.os_mknod_mode)
676-
break
625+
checker.set_os_open_allowed_modes(checker.config.os_open_mode)
626+
checker.set_os_mkdir_allowed_modes(checker.config.os_mkdir_mode)
627+
checker.set_os_mkfifo_allowed_modes(checker.config.os_mkfifo_mode)
628+
checker.set_os_mknod_allowed_modes(checker.config.os_mknod_mode)

tests/builtin_open_test.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ def test_builtin_open_ok(self):
4141
with_nodes = nodes[5:]
4242

4343
with self.assertNoMessages():
44-
self.checker.set_os_open_mode('True')
45-
assert self.checker._prefer_os_open
44+
self.checker.set_os_open_allowed_modes('True')
4645
for idx, node in enumerate(call_nodes):
4746
self.checker.visit_call(node)
4847
for idx, node in enumerate(with_nodes):
@@ -75,8 +74,7 @@ def test_builtin_open_ok(self):
7574
@pytest.mark.parametrize('os_open_mode', (False, True))
7675
def test_builtin_open_call(self, s, os_open_mode):
7776
node = astroid.extract_node(s + ' #@')
78-
self.checker.set_os_open_mode(str(os_open_mode))
79-
assert self.checker._prefer_os_open == os_open_mode
77+
self.checker.set_os_open_allowed_modes(str(os_open_mode))
8078
if os_open_mode:
8179
with self.assertAddsMessages(pylint.testutils.Message(msg_id='replace-builtin-open', node=node)):
8280
self.checker.visit_call(node)
@@ -88,8 +86,7 @@ def test_builtin_open_call(self, s, os_open_mode):
8886
@pytest.mark.parametrize('os_open_mode', (False, True))
8987
def test_builtin_open_with(self, s, os_open_mode):
9088
node = astroid.extract_node(s + ' #@')
91-
self.checker.set_os_open_mode(str(os_open_mode))
92-
assert self.checker._prefer_os_open == os_open_mode
89+
self.checker.set_os_open_allowed_modes(str(os_open_mode))
9390
if os_open_mode:
9491
with self.assertAddsMessages(pylint.testutils.Message(msg_id='replace-builtin-open', node=node)):
9592
self.checker.visit_with(node)

tests/os_mkdir_mkfifo_mknod_test.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,15 @@ def test_os_function_call(self, mocker, platform, enabled_platform, function, op
113113
mocker.patch('platform.system', lambda: platform)
114114
getattr(self.checker, f'set_os_{function}_allowed_modes')(str(option))
115115

116+
print(s + ' #@')
116117
node = astroid.extract_node(s + ' #@')
117118
if enabled_platform and option:
118119
with self.assertAddsMessages(
119-
pylint.testutils.Message(msg_id=f'os-{function}-unsafe-permissions', node=node)
120+
pylint.testutils.Message(
121+
msg_id=f'os-{function}-unsafe-permissions',
122+
node=node,
123+
args=(getattr(self.checker, f'_os_{function}_msg_arg'),),
124+
)
120125
):
121126
self.checker.visit_call(node)
122127
else:

0 commit comments

Comments
 (0)