madaramaster 5.0.0__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.
@@ -0,0 +1,3 @@
1
+ """MadaraMaster — secure file sanitization (NIST SP 800-88 / DoD 5220.22-M)."""
2
+
3
+ __version__ = "5.0.0"
@@ -0,0 +1,5 @@
1
+ """Allow ``python -m madaramaster``."""
2
+
3
+ from .cli import main
4
+
5
+ main()
madaramaster/audit.py ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ # Log de auditoría forense — formato JSON Lines
3
+ # jaimefg1888
4
+
5
+ import getpass
6
+ import json
7
+ import logging
8
+ import os
9
+ import socket
10
+ import sys
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+ from typing import Any, Generator, Optional
14
+
15
+
16
+ def default_log_path() -> Path:
17
+ """Return the per-user default location of the audit log.
18
+
19
+ The log records file names, so it is kept out of the working directory
20
+ (which may be the very directory being wiped) and in a private
21
+ per-user location instead:
22
+
23
+ * Linux/BSD: ``$XDG_STATE_HOME/madaramaster/audit.jsonl``
24
+ (default ``~/.local/state/madaramaster/audit.jsonl``)
25
+ * macOS: ``~/Library/Logs/MadaraMaster/audit.jsonl``
26
+ * Windows: ``%LOCALAPPDATA%\\MadaraMaster\\audit.jsonl``
27
+ """
28
+ home = Path.home()
29
+ if sys.platform == "win32":
30
+ base = Path(os.environ.get("LOCALAPPDATA") or home / "AppData" / "Local")
31
+ return base / "MadaraMaster" / "audit.jsonl"
32
+ if sys.platform == "darwin":
33
+ return home / "Library" / "Logs" / "MadaraMaster" / "audit.jsonl"
34
+ base = Path(os.environ.get("XDG_STATE_HOME") or home / ".local" / "state")
35
+ return base / "madaramaster" / "audit.jsonl"
36
+
37
+
38
+ def _safe_username() -> str:
39
+ """Return the current username without raising in daemon / Docker environments.
40
+
41
+ ``os.getlogin()`` raises ``OSError`` when there is no controlling
42
+ terminal (typical in Docker, CI pipelines, or systemd services).
43
+ The function falls back to ``getpass.getuser()`` and finally to
44
+ well-known environment variables.
45
+ """
46
+ try:
47
+ return os.getlogin()
48
+ except OSError:
49
+ pass
50
+ try:
51
+ return getpass.getuser()
52
+ except Exception:
53
+ pass
54
+ return os.environ.get("USER", os.environ.get("USERNAME", "unknown"))
55
+
56
+
57
+ class AuditLogger:
58
+ """Append-only forensic audit logger that writes JSON Lines records.
59
+
60
+ Each call to :meth:`log_wipe_operation` appends a single JSON object
61
+ terminated by a newline to the configured log file. Reading is done
62
+ lazily via :meth:`get_logs` to avoid loading the entire file into RAM.
63
+
64
+ The file and its directory are created on first write with owner-only
65
+ permissions (``0600`` / ``0700`` on POSIX).
66
+
67
+ Args:
68
+ log_path: Destination file for the audit log. Defaults to
69
+ :func:`default_log_path`.
70
+ """
71
+
72
+ def __init__(self, log_path: Optional[Path] = None) -> None:
73
+ self.log_path = Path(log_path) if log_path is not None else default_log_path()
74
+
75
+ def _append(self, line: str) -> None:
76
+ self.log_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
77
+ fd = os.open(self.log_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
78
+ with os.fdopen(fd, "a", encoding="utf-8") as f:
79
+ f.write(line)
80
+
81
+ def log_wipe_operation(
82
+ self,
83
+ file_path: Path,
84
+ file_size: int,
85
+ sha256_before: Optional[str],
86
+ standard_used: str,
87
+ result: dict[str, Any],
88
+ ) -> None:
89
+ """Append one JSON record describing a completed wipe operation.
90
+
91
+ Args:
92
+ file_path: Absolute path of the file that was wiped.
93
+ file_size: Size of the file in bytes, captured before wiping.
94
+ sha256_before: Hex-encoded SHA-256 digest of the file contents
95
+ before the wipe started, or ``None`` when hashing was not
96
+ requested (``--hash``). The digest lets anyone who has the
97
+ log confirm what the file contained, so it is opt-in.
98
+ standard_used: Sanitization standard identifier (e.g. ``"clear"``).
99
+ result: Dictionary returned by the wipe engine. Expected keys:
100
+ ``passes_completed``, ``verified``, ``duration``,
101
+ ``success``, ``error``, ``strategy``.
102
+ """
103
+ try:
104
+ record: dict[str, Any] = {
105
+ "timestamp": datetime.now(timezone.utc).isoformat(),
106
+ "file": str(file_path.absolute()),
107
+ "size_bytes": file_size,
108
+ "sha256_before": sha256_before,
109
+ "standard": standard_used,
110
+ "passes": result.get("passes_completed", 0),
111
+ "verified": result.get("verified", False),
112
+ "duration_sec": result.get("duration", 0.0),
113
+ "user": _safe_username(),
114
+ "hostname": socket.gethostname(),
115
+ "success": result.get("success", False),
116
+ "error": result.get("error"),
117
+ "strategy": result.get("strategy", "Unknown"),
118
+ "final_path": result.get("final_path"),
119
+ }
120
+ self._append(json.dumps(record) + "\n")
121
+ except Exception as exc:
122
+ logging.error("Could not write to the audit log: %s", exc)
123
+
124
+ def get_logs(self) -> Generator[dict[str, Any], None, None]:
125
+ """Yield parsed log records one at a time.
126
+
127
+ Reads the log file lazily so arbitrarily large audit logs can be
128
+ processed without exhausting available memory.
129
+
130
+ Yields:
131
+ One dictionary per valid JSON line in the log file.
132
+ """
133
+ if not self.log_path.exists():
134
+ return
135
+ with open(self.log_path, "r", encoding="utf-8") as f:
136
+ for line in f:
137
+ try:
138
+ yield json.loads(line)
139
+ except json.JSONDecodeError:
140
+ continue
141
+
142
+
143
+ class NullAuditLogger(AuditLogger):
144
+ """Audit logger that records nothing (``--no-log``)."""
145
+
146
+ def __init__(self) -> None:
147
+ self.log_path = None # type: ignore[assignment]
148
+
149
+ def log_wipe_operation(self, *args: Any, **kwargs: Any) -> None:
150
+ return None
151
+
152
+ def get_logs(self) -> Generator[dict[str, Any], None, None]:
153
+ return
154
+ yield
madaramaster/cli.py ADDED
@@ -0,0 +1,118 @@
1
+ """MadaraMaster command-line interface.
2
+
3
+ Secure file sanitization with a live dashboard. Bilingual (EN/ES).
4
+
5
+ Usage::
6
+
7
+ madara # interactive session
8
+ madara wipe <PATH> # wipe a file or directory
9
+ madara wipe <PATH> --dry-run # preview without touching anything
10
+ madara wipe-free-space <DIR> # overwrite the free space of a filesystem
11
+ madara --lang es wipe <PATH> # Spanish interface (or MADARA_LANG=es)
12
+
13
+ ``python -m madaramaster`` and ``python madara.py`` are equivalent.
14
+
15
+ The implementation is split by responsibility; this module is the stable
16
+ entry point (``madaramaster.cli:main``) and re-exports the names it used
17
+ to define:
18
+
19
+ * :mod:`madaramaster.i18n` — translations and the active language
20
+ * :mod:`madaramaster.ui` — console, banner, prompts, summary, dashboard
21
+ * :mod:`madaramaster.runner` — shared wipe flow used by every front end
22
+ * :mod:`madaramaster.free_space` — free-space fill engine
23
+ * :mod:`madaramaster.interactive` — interactive session
24
+ * :mod:`madaramaster.commands` — Typer application and commands
25
+ """
26
+
27
+ from .audit import default_log_path
28
+ from .commands import (
29
+ Language,
30
+ Standard,
31
+ _context_menu_command,
32
+ _ensure_utf8_output,
33
+ _global_options,
34
+ app,
35
+ install_context_menu,
36
+ main,
37
+ version,
38
+ wipe,
39
+ wipe_free_space_cmd,
40
+ )
41
+ from .free_space import (
42
+ _FILL_CHUNK_LARGE,
43
+ _FILL_CHUNK_SMALL,
44
+ _ZEROS_LARGE,
45
+ _ZEROS_SMALL,
46
+ _async_wipe_free_space,
47
+ )
48
+ from .i18n import CONFIRM_YES, EXIT_KEYWORDS, LANG, T
49
+ from .interactive import (
50
+ _confirm_interactive,
51
+ _parse_input_line,
52
+ _print_file_preview,
53
+ _print_session_hints,
54
+ _strip_quotes,
55
+ _target_size,
56
+ interactive_session,
57
+ )
58
+ from .runner import _FLASH_TYPES, _expand_targets, _remove_empty_dirs, async_wipe_logic
59
+ from .trim import send_trim
60
+ from .ui import (
61
+ BANNER,
62
+ SpeedTracker,
63
+ _build_dashboard,
64
+ _lsize,
65
+ _print_residue_warning,
66
+ confirm_action,
67
+ print_banner,
68
+ print_summary,
69
+ select_language,
70
+ )
71
+
72
+ __all__ = [
73
+ "BANNER",
74
+ "CONFIRM_YES",
75
+ "EXIT_KEYWORDS",
76
+ "LANG",
77
+ "Language",
78
+ "SpeedTracker",
79
+ "Standard",
80
+ "T",
81
+ "_FILL_CHUNK_LARGE",
82
+ "_FILL_CHUNK_SMALL",
83
+ "_FLASH_TYPES",
84
+ "_ZEROS_LARGE",
85
+ "_ZEROS_SMALL",
86
+ "_async_wipe_free_space",
87
+ "_build_dashboard",
88
+ "_confirm_interactive",
89
+ "_context_menu_command",
90
+ "_ensure_utf8_output",
91
+ "_expand_targets",
92
+ "_global_options",
93
+ "_lsize",
94
+ "_parse_input_line",
95
+ "_print_file_preview",
96
+ "_print_residue_warning",
97
+ "_print_session_hints",
98
+ "_remove_empty_dirs",
99
+ "_strip_quotes",
100
+ "_target_size",
101
+ "app",
102
+ "async_wipe_logic",
103
+ "confirm_action",
104
+ "default_log_path",
105
+ "install_context_menu",
106
+ "interactive_session",
107
+ "main",
108
+ "print_banner",
109
+ "print_summary",
110
+ "select_language",
111
+ "send_trim",
112
+ "version",
113
+ "wipe",
114
+ "wipe_free_space_cmd",
115
+ ]
116
+
117
+ if __name__ == "__main__":
118
+ main()