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.
- madaramaster/__init__.py +3 -0
- madaramaster/__main__.py +5 -0
- madaramaster/audit.py +154 -0
- madaramaster/cli.py +118 -0
- madaramaster/commands.py +458 -0
- madaramaster/engine.py +957 -0
- madaramaster/free_space.py +114 -0
- madaramaster/i18n.py +323 -0
- madaramaster/interactive.py +208 -0
- madaramaster/models.py +58 -0
- madaramaster/residue.py +242 -0
- madaramaster/runner.py +206 -0
- madaramaster/safety.py +188 -0
- madaramaster/storage.py +503 -0
- madaramaster/trim.py +140 -0
- madaramaster/ui.py +330 -0
- madaramaster/utils.py +30 -0
- madaramaster-5.0.0.dist-info/METADATA +409 -0
- madaramaster-5.0.0.dist-info/RECORD +23 -0
- madaramaster-5.0.0.dist-info/WHEEL +5 -0
- madaramaster-5.0.0.dist-info/entry_points.txt +2 -0
- madaramaster-5.0.0.dist-info/licenses/LICENSE +22 -0
- madaramaster-5.0.0.dist-info/top_level.txt +1 -0
madaramaster/__init__.py
ADDED
madaramaster/__main__.py
ADDED
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()
|