dar-backup 0.8.1__py3-none-any.whl → 0.8.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dar_backup/Changelog.md CHANGED
@@ -1,6 +1,24 @@
1
1
  <!-- markdownlint-disable MD024 -->
2
2
  # dar-backup Changelog
3
3
 
4
+ ## v2-beta-0.8.2 - 2025-07-17
5
+
6
+ Github link: [v2-beta-0.8.2](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.8.2/v2)
7
+
8
+ ### Added
9
+
10
+ - Security hardening: CommandRunner now performs strict command-line sanitization
11
+ - Disallows potentially dangerous characters (e.g. ;, &, |) in command arguments
12
+ - Prevents injection-style misuse when restoring specific files or invoking custom commands
13
+
14
+ - Documentation:
15
+ - New [README section](https://github.com/per2jensen/dar-backup?tab=readme-ov-file#limitations-on-file-names-with-special-characters) explains filename restrictions and safe workarounds (e.g. restoring directly with dar, if needed)
16
+ - Includes a Markdown table listing all disallowed characters
17
+
18
+ - Test suite:
19
+ - Existing test cases updated to comply with the new sanitization rules
20
+ - Additional tests ensure CommandRunner handles large binary output and edge cases cleanly
21
+
4
22
  ## v2-beta-0.8.1 - 2025-07-16
5
23
 
6
24
  Github link: [v2-beta-0.8.1](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.8.1/v2)
dar_backup/__about__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.8.1"
1
+ __version__ = "0.8.2"
2
2
 
3
3
  __license__ = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
4
4
  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
@@ -5,33 +5,76 @@ import logging
5
5
  import traceback
6
6
  import threading
7
7
  import os
8
+ import re
9
+ import shlex
8
10
  import sys
9
11
  import tempfile
10
12
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
11
- from typing import List, Optional
13
+ from typing import List, Optional, Union
12
14
  from dar_backup.util import get_logger
13
15
 
14
16
 
17
+ def is_safe_arg(arg: str) -> bool:
18
+ """
19
+ Check if the argument is safe by rejecting dangerous shell characters.
20
+ """
21
+ return not re.search(r'[;&|><`$\n]', arg)
22
+
23
+
24
+ def sanitize_cmd(cmd: List[str]) -> List[str]:
25
+ """
26
+ Validate and sanitize a list of command-line arguments.
27
+ Ensures all elements are strings and do not contain dangerous shell characters.
28
+ Raises ValueError if any argument is unsafe.
29
+ """
30
+
31
+ if not isinstance(cmd, list):
32
+ raise ValueError("Command must be a list of strings")
33
+ for arg in cmd:
34
+ if not isinstance(arg, str):
35
+ raise ValueError(f"Invalid argument type: {arg} (must be string)")
36
+ if not is_safe_arg(arg):
37
+ raise ValueError(f"Unsafe argument detected: {arg}")
38
+ return cmd
39
+
40
+ def _safe_str(s):
41
+ if isinstance(s, bytes):
42
+ return f"<{len(s)} bytes of binary data>"
43
+ return s
44
+
45
+
15
46
  class CommandResult:
16
- def __init__(self, returncode: int, stdout: str, stderr: str, stack: str = None):
47
+ def __init__(
48
+ self,
49
+ returncode: int,
50
+ stdout: Union[str, bytes],
51
+ stderr: Union[str, bytes],
52
+ stack: Optional[str] = None,
53
+ note: Optional[str] = None
54
+ ):
17
55
  self.returncode = returncode
18
56
  self.stdout = stdout
19
57
  self.stderr = stderr
20
58
  self.stack = stack
59
+ self.note = note
60
+
61
+
21
62
 
22
63
  def __repr__(self):
23
64
  return f"<CommandResult returncode={self.returncode}\nstdout={self.stdout}\nstderr={self.stderr}\nstack={self.stack}>"
24
-
65
+
25
66
 
26
67
  def __str__(self):
27
68
  return (
28
- f"CommandResult:\n"
69
+ "CommandResult:\n"
29
70
  f" Return code: {self.returncode}\n"
30
- f" STDOUT:\n{self.stdout}\n"
31
- f" STDERR:\n{self.stderr}\n"
32
- f" Stacktrace:\n{self.stack if self.stack else '<none>'}"
71
+ f" Note: {self.note if self.note else '<none>'}\n"
72
+ f" STDOUT: {_safe_str(self.stdout)}\n"
73
+ f" STDERR: {_safe_str(self.stderr)}\n"
74
+ f" Stacktrace: {self.stack if self.stack else '<none>'}"
33
75
  )
34
76
 
77
+
35
78
  class CommandRunner:
36
79
  def __init__(
37
80
  self,
@@ -46,6 +89,7 @@ class CommandRunner:
46
89
  if not self.logger or not self.command_logger:
47
90
  self.logger_fallback()
48
91
 
92
+
49
93
  def logger_fallback(self):
50
94
  """
51
95
  Setup temporary log files
@@ -83,9 +127,31 @@ class CommandRunner:
83
127
  capture_output: bool = True,
84
128
  text: bool = True
85
129
  ) -> CommandResult:
130
+ self._text_mode = text
86
131
  timeout = timeout or self.default_timeout
87
132
 
88
- command = f"Executing command: {' '.join(cmd)} (timeout={timeout}s)"
133
+ cmd_sanitized = None
134
+
135
+ try:
136
+ cmd_sanitized = sanitize_cmd(cmd)
137
+ except ValueError as e:
138
+ stack = traceback.format_exc()
139
+ self.logger.error(f"Command sanitation failed: {e}")
140
+ return CommandResult(
141
+ returncode=-1,
142
+ note=f"Sanitizing failed: command: {' '.join(cmd)}",
143
+ stdout='',
144
+ stderr=str(e),
145
+ stack=stack,
146
+
147
+ )
148
+ finally:
149
+ cmd = cmd_sanitized
150
+
151
+ #command = f"Executing command: {' '.join(cmd)} (timeout={timeout}s)"
152
+ command = f"Executing command: {' '.join(shlex.quote(arg) for arg in cmd)} (timeout={timeout}s)"
153
+
154
+
89
155
  self.command_logger.info(command)
90
156
  self.logger.debug(command)
91
157
 
@@ -115,9 +181,13 @@ class CommandRunner:
115
181
  chunk = stream.read(1024)
116
182
  if not chunk:
117
183
  break
118
- decoded = chunk.decode('utf-8', errors='replace')
119
- lines.append(decoded)
120
- self.command_logger.log(level, decoded.strip())
184
+ if self._text_mode:
185
+ decoded = chunk.decode('utf-8', errors='replace')
186
+ lines.append(decoded)
187
+ self.command_logger.log(level, decoded.strip())
188
+ else:
189
+ lines.append(chunk)
190
+ # Avoid logging raw binary data to prevent garbled logs
121
191
  except Exception as e:
122
192
  self.logger.warning(f"stream_output decode error: {e}")
123
193
  finally:
@@ -150,18 +220,27 @@ class CommandRunner:
150
220
  t.join()
151
221
 
152
222
 
223
+
224
+ if self._text_mode:
225
+ stdout_combined = ''.join(stdout_lines)
226
+ stderr_combined = ''.join(stderr_lines)
227
+ else:
228
+ stdout_combined = b''.join(stdout_lines)
229
+ stderr_combined = b''.join(stderr_lines)
230
+
231
+
153
232
  if check and process.returncode != 0:
154
233
  self.logger.error(f"Command failed with exit code {process.returncode}")
155
234
  return CommandResult(
156
235
  process.returncode,
157
- ''.join(stdout_lines),
158
- ''.join(stderr_lines),
236
+ stdout_combined,
237
+ stderr_combined,
159
238
  stack=traceback.format_stack()
160
239
  )
161
240
 
162
241
  return CommandResult(
163
242
  process.returncode,
164
- ''.join(stdout_lines),
165
- ''.join(stderr_lines),
243
+ stdout_combined,
244
+ stderr_combined
166
245
  )
167
246
 
dar_backup/dar_backup.py CHANGED
@@ -248,7 +248,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
248
248
  PermissionError: If a permission error occurs while comparing files.
249
249
  """
250
250
  result = True
251
- command = ['dar', '-t', backup_file, '-Q']
251
+ command = ['dar', '-t', backup_file, '-N', '-Q']
252
252
 
253
253
 
254
254
  log_basename = os.path. dirname(config_settings.logfile_location)
@@ -315,7 +315,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
315
315
  for restored_file_path in random_files:
316
316
  try:
317
317
  args.verbose and logger.info(f"Restoring file: '{restored_file_path}' from backup to: '{config_settings.test_restore_dir}' for file comparing")
318
- command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '-Q', '-B', args.darrc, 'restore-options']
318
+ command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '--noconf', '-Q', '-B', args.darrc, 'restore-options']
319
319
  args.verbose and logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
320
320
  process = runner.run(command, timeout = config_settings.command_timeout_secs)
321
321
  if process.returncode != 0:
@@ -347,7 +347,7 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
347
347
  results: List[tuple] = []
348
348
  try:
349
349
  backup_file = os.path.join(config_settings.backup_dir, backup_name)
350
- command = ['dar', '-x', backup_file, '-Q', '-D']
350
+ command = ['dar', '-x', backup_file, '--noconf', '-Q', '-D']
351
351
  if restore_dir:
352
352
  if not os.path.exists(restore_dir):
353
353
  os.makedirs(restore_dir)
@@ -390,7 +390,7 @@ def get_backed_up_files(backup_name: str, backup_dir: str):
390
390
  logger.debug(f"Getting backed up files in xml from DAR archive: '{backup_name}'")
391
391
  backup_path = os.path.join(backup_dir, backup_name)
392
392
  try:
393
- command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
393
+ command = ['dar', '-l', backup_path, '--noconf', '-am', '-as', "-Txml" , '-Q']
394
394
  logger.debug(f"Running command: {' '.join(map(shlex.quote, command))}")
395
395
  command_result = runner.run(command)
396
396
  # Parse the XML data
@@ -418,7 +418,7 @@ def list_contents(backup_name, backup_dir, selection=None):
418
418
  backup_path = os.path.join(backup_dir, backup_name)
419
419
 
420
420
  try:
421
- command = ['dar', '-l', backup_path, '-am', '-as', '-Q']
421
+ command = ['dar', '-l', backup_path, '--noconf', '-am', '-as', '-Q']
422
422
  if selection:
423
423
  selection_criteria = shlex.split(selection)
424
424
  command.extend(selection_criteria)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dar-backup
3
- Version: 0.8.1
3
+ Version: 0.8.2
4
4
  Summary: A script to do full, differential and incremental backups using dar. Some files are restored from the backups during verification, after which par2 redundancy files are created. The script also has a cleanup feature to remove old backups and par2 files.
5
5
  Project-URL: GPG Public Key, https://keys.openpgp.org/search?q=dar-backup@pm.me
6
6
  Project-URL: Homepage, https://github.com/per2jensen/dar-backup/tree/main/v2
@@ -1,15 +1,15 @@
1
1
  dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
2
- dar_backup/Changelog.md,sha256=T6w4NGFjBg2rgyTmgScKPFwdJx_M3XkEnU7aG3NQMyo,12094
2
+ dar_backup/Changelog.md,sha256=kJHH12ETI46nzZURvhKZvZR4RndnY8KP22ORfN5u3iA,12988
3
3
  dar_backup/README.md,sha256=S8wpgaqa2LzXXZiQEzoOV1qjrpf9m1baYmvzGYtgEcE,59990
4
- dar_backup/__about__.py,sha256=SSWyynCpdSF2lIcROjGdr29Z-m523S9BbEc-ytO_L30,344
4
+ dar_backup/__about__.py,sha256=HtwR6RuPdVHxYDraJvSQ0J9gRO3qkMMealFqBS1CfGc,344
5
5
  dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  dar_backup/clean_log.py,sha256=pmmyPmLWbm3_3sHwJt9V_xBwUF8v015iS17ypJAGAZ4,6023
7
7
  dar_backup/cleanup.py,sha256=_ggDcpMCB1MhXStvYussp_PdGfhIFtEutT5BNrNkMSY,13297
8
- dar_backup/command_runner.py,sha256=_aaLk8j44tFNYBv9FJTpUaIFE_iIKEO1W1xDaiO5HJg,5599
8
+ dar_backup/command_runner.py,sha256=cwthuNU4N1vzAjri0uh2x32vmCLu1B2S3OxGExpCGRE,7840
9
9
  dar_backup/config_settings.py,sha256=2UAHvatrVO4ark6lCn2Q7qBvZN6DUMK2ftlNrKpzlWc,5792
10
10
  dar_backup/dar-backup.conf,sha256=46V2zdvjj_aThFY11wWlffjmoiChYmatdf5DXCsmmao,1208
11
11
  dar_backup/dar-backup.conf.j2,sha256=z3epGo6nB_Jh3liTOp93wJO_HKUsf7qghe9cdtFH7cY,2021
12
- dar_backup/dar_backup.py,sha256=pgUQ1wZ5_yocRJJDRa4AkVyI7I8Z5WBY5vD5a4ASkmA,43001
12
+ dar_backup/dar_backup.py,sha256=JW1k0LuQhf_y2f1K1pIu1-PmKw4bGSrCB_MBoPvFWuM,43057
13
13
  dar_backup/dar_backup_systemd.py,sha256=PwAc2H2J3hQLWpnC6Ib95NZYtB2G2NDgkSblfLj1n10,3875
14
14
  dar_backup/demo.py,sha256=bxEq_nJwHuQydERPppkvhotg1fdwBX_CE33m5fX_kxw,7945
15
15
  dar_backup/demo_backup_def.j2,sha256=hQW2Glp0QGV3Kt8cwjS0mpOCdyzjVlpgbgL6LpXTKJA,1793
@@ -18,8 +18,8 @@ dar_backup/installer.py,sha256=xSXh77qquIZbUTSY3AbhERQbS7bnrPE__M_yqpszdhM,6883
18
18
  dar_backup/manager.py,sha256=d1zliFpSuWc8JhjKqLMC-xOhp5kSIcfeGkMZOVuZcM0,27143
19
19
  dar_backup/rich_progress.py,sha256=SfwFxebBl6jnDQMUQr4McknkW1yQWaJVo1Ju1OD3okA,3221
20
20
  dar_backup/util.py,sha256=iTOGsZyIdkvh9tIu7hD_IXi-9HO6GhVgqact5GGInEY,26063
21
- dar_backup-0.8.1.dist-info/METADATA,sha256=UPCRBCwXlLLjGVGZC-BVqCZXOCYa82lccd8W16I2sCg,102681
22
- dar_backup-0.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- dar_backup-0.8.1.dist-info/entry_points.txt,sha256=pOK9M8cHeAcGIatrYzkm_1O89kPk0enyYONALYjFBx4,286
24
- dar_backup-0.8.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
- dar_backup-0.8.1.dist-info/RECORD,,
21
+ dar_backup-0.8.2.dist-info/METADATA,sha256=YgHdGcT3ZcaCUYxtRwaArlHj4xelOH0DVumw2bgGwTk,102681
22
+ dar_backup-0.8.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ dar_backup-0.8.2.dist-info/entry_points.txt,sha256=pOK9M8cHeAcGIatrYzkm_1O89kPk0enyYONALYjFBx4,286
24
+ dar_backup-0.8.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
+ dar_backup-0.8.2.dist-info/RECORD,,