Skip to content
API Documentation
BastionGuard™
Quarantine.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 *
17 * BastionGuard™ is a trademark of Calogero Scarnà.
18 * The BastionGuard™ name and branding are not licensed under the GPL.
19 */
20
21#include "Resource.hpp"
22#include "Quarantine.hpp"
23#include "Backend.hpp"
24
25#include <filesystem>
26#include <fstream>
27#include <iostream>
28#include <cstdlib>
29#include <system_error>
30#include <unistd.h>
31#include <glib/gi18n.h>
32
33namespace fs = std::filesystem;
34
35namespace {
36
38 std::error_code& ec)
39{
40 ec.clear();
41 const fs::file_status status = fs::symlink_status(path, ec);
42 return !ec && fs::exists(status) && fs::is_regular_file(status);
43}
44
45fs::path normalize_path(const fs::path& path, std::error_code& ec)
46{
47 ec.clear();
48 fs::path normalized = fs::weakly_canonical(path, ec);
49 if (!ec)
50 return normalized.lexically_normal();
51
52 ec.clear();
53 normalized = fs::absolute(path, ec);
54 if (!ec)
55 return normalized.lexically_normal();
56
57 return path.lexically_normal();
58}
59
60bool is_path_inside(const fs::path& candidate, const fs::path& directory)
61{
62 std::error_code candidate_ec;
63 std::error_code directory_ec;
64
65 const fs::path normalized_candidate = normalize_path(candidate, candidate_ec);
66 const fs::path normalized_directory = normalize_path(directory, directory_ec);
67
68 if (candidate_ec || directory_ec || normalized_directory.empty())
69 return false;
70
71 auto candidate_it = normalized_candidate.begin();
72 for (auto directory_it = normalized_directory.begin();
73 directory_it != normalized_directory.end();
74 ++directory_it, ++candidate_it) {
75 if (candidate_it == normalized_candidate.end() ||
76 *candidate_it != *directory_it) {
77 return false;
78 }
79 }
80
81 return true;
82}
83
84void remove_without_throwing(const fs::path& path)
85{
86 std::error_code ec;
87 fs::remove(path, ec);
88}
89
90bool copy_then_remove(const fs::path& source, const fs::path& destination)
91{
92 std::error_code copy_ec;
93 fs::copy_file(source, destination, fs::copy_options::none, copy_ec);
94 if (copy_ec) {
95 std::cerr << _("❌ Errore copia in quarantena: ")
96 << copy_ec.message() << std::endl;
97 return false;
98 }
99
100 std::error_code remove_ec;
101 fs::remove(source, remove_ec);
102 if (remove_ec) {
103 std::cerr << _("❌ Impossibile rimuovere il file originale dopo la copia: ")
104 << remove_ec.message() << std::endl;
105
106 // La quarantena non è riuscita: non lasciare una copia che possa
107 // essere interpretata come isolamento completato.
108 remove_without_throwing(destination);
109 return false;
110 }
111
112 return true;
113}
114
115} // namespace
116
118{
119 std::string path = Backend::getQuarantinePath();
120 if (path.empty()) {
121 const char* home = std::getenv("HOME");
122 path = std::string(home ? home : "/tmp") +
123 "/.local/share/BastionGuard/quarantine";
124 }
125
126 try {
127 const fs::path quarantine_path(path);
128
129 if (quarantine_path.empty() ||
130 quarantine_path == quarantine_path.root_path()) {
131 std::cerr << _("❌ Percorso di quarantena non valido: ")
132 << quarantine_path << std::endl;
133 return {};
134 }
135
136 std::error_code ec;
137 fs::create_directories(quarantine_path, ec);
138 if (ec) {
139 std::cerr << _("❌ Errore creazione cartella quarantena: ")
140 << ec.message() << std::endl;
141 return {};
142 }
143
144 ec.clear();
145 if (!fs::is_directory(quarantine_path, ec) || ec) {
146 std::cerr << _("❌ Il percorso di quarantena non è una directory valida: ")
147 << quarantine_path << std::endl;
148 return {};
149 }
150 } catch (const std::exception& e) {
151 std::cerr << _("❌ Errore creazione cartella quarantena: ")
152 << e.what() << std::endl;
153 return {};
154 }
155
156 return path;
157}
158
159bool Quarantine::move(const std::string& filepath)
160{
161 try {
162 if (filepath.empty()) {
163 std::cerr << _("❌ Percorso file vuoto: quarantena annullata")
164 << std::endl;
165 return false;
166 }
167
168 std::error_code ec;
169 fs::path src = fs::absolute(fs::path(filepath), ec);
170 if (ec) {
171 std::cerr << _("❌ Impossibile risolvere il percorso del file: ")
172 << filepath << " | " << ec.message() << std::endl;
173 return false;
174 }
175 src = src.lexically_normal();
176
177 // Protezione fondamentale: non seguire symlink e non accettare
178 // directory, socket, FIFO o device. Solo file regolari.
179 if (!is_regular_file_without_following_symlinks(src, ec)) {
180 std::cerr << _("❌ Quarantena rifiutata: il percorso non è un file regolare: ")
181 << src;
182 if (ec)
183 std::cerr << " | " << ec.message();
184 std::cerr << std::endl;
185 return false;
186 }
187
188 const std::string quarantine_dir_string = getQuarantinePath();
189 if (quarantine_dir_string.empty())
190 return false;
191
192 const fs::path quarantine_dir(quarantine_dir_string);
193
194 // Evita loop e tentativi di mettere nuovamente in quarantena un file
195 // che si trova già nell'area protetta.
196 if (is_path_inside(src, quarantine_dir)) {
197 std::cerr << _("❌ Il file è già contenuto nella quarantena: ")
198 << src << std::endl;
199 return false;
200 }
201
202 fs::path dest = quarantine_dir / src.filename();
203
204 int counter = 1;
205 while (fs::exists(dest) || fs::exists(dest.string() + ".meta")) {
206 dest = quarantine_dir /
207 (src.stem().string() + "_" + std::to_string(counter) +
208 src.extension().string());
209 ++counter;
210 }
211
212 const fs::path meta_path(dest.string() + ".meta");
213 std::ofstream meta(meta_path, std::ios::out | std::ios::trunc);
214 if (!meta.is_open()) {
215 std::cerr << _("❌ Impossibile creare i metadata di quarantena: ")
216 << meta_path << std::endl;
217 return false;
218 }
219
220 meta << src.string() << '\n';
221 meta.flush();
222 if (!meta.good()) {
223 meta.close();
224 remove_without_throwing(meta_path);
225 std::cerr << _("❌ Scrittura metadata di quarantena fallita: ")
226 << meta_path << std::endl;
227 return false;
228 }
229 meta.close();
230
231 ec.clear();
232 fs::rename(src, dest, ec);
233
234 if (ec) {
235 if (ec == std::make_error_code(std::errc::cross_device_link)) {
236 std::cerr << _("DEBUG move(): filesystem differente, uso copia e rimozione\n");
237
238 if (!copy_then_remove(src, dest)) {
239 remove_without_throwing(meta_path);
240 return false;
241 }
242 } else {
243 std::cerr << _("❌ Errore spostamento quarantena: ")
244 << ec.message() << std::endl;
245 remove_without_throwing(meta_path);
246 return false;
247 }
248 }
249
250 std::cout << _("✔ File spostato in quarantena: ")
251 << dest << std::endl;
252 return true;
253
254 } catch (const std::exception& e) {
255 std::cerr << _("❌ Errore spostamento quarantena: ")
256 << e.what() << std::endl;
257 return false;
258 }
259}
260
261bool Quarantine::restore(const std::string& quarantinedFile)
262{
263 try {
264 std::error_code ec;
265 fs::path qf = fs::absolute(fs::path(quarantinedFile), ec);
266 if (ec) {
267 std::cerr << _("❌ Percorso quarantena non valido: ")
268 << quarantinedFile << " | " << ec.message() << std::endl;
269 return false;
270 }
271 qf = qf.lexically_normal();
272
273 const std::string quarantine_dir_string = getQuarantinePath();
274 if (quarantine_dir_string.empty())
275 return false;
276
277 const fs::path quarantine_dir(quarantine_dir_string);
278 if (!is_path_inside(qf, quarantine_dir)) {
279 std::cerr << _("❌ Ripristino rifiutato: file esterno alla quarantena: ")
280 << qf << std::endl;
281 return false;
282 }
283
284 if (!is_regular_file_without_following_symlinks(qf, ec)) {
285 std::cerr << _("❌ Ripristino rifiutato: elemento non regolare: ")
286 << qf << std::endl;
287 return false;
288 }
289
290 const fs::path meta(qf.string() + ".meta");
291 if (!is_regular_file_without_following_symlinks(meta, ec)) {
292 std::cerr << _("❌ Ripristino impossibile: metadata mancante o non valido per ")
293 << qf << std::endl;
294 return false;
295 }
296
297 std::ifstream in(meta);
298 std::string original_path;
299 std::getline(in, original_path);
300 in.close();
301
302 if (original_path.empty()) {
303 std::cerr << _("❌ Ripristino impossibile: metadata vuoto o corrotto in ")
304 << meta << std::endl;
305 return false;
306 }
307
308 fs::path original = fs::absolute(fs::path(original_path), ec);
309 if (ec) {
310 std::cerr << _("❌ Percorso originale non valido nei metadata: ")
311 << original_path << " | " << ec.message() << std::endl;
312 return false;
313 }
314 original = original.lexically_normal();
315
316 if (is_path_inside(original, quarantine_dir)) {
317 std::cerr << _("❌ Ripristino rifiutato: destinazione interna alla quarantena: ")
318 << original << std::endl;
319 return false;
320 }
321
322 fs::create_directories(original.parent_path(), ec);
323 if (ec) {
324 std::cerr << _("❌ Impossibile ricreare la directory originale: ")
325 << original.parent_path() << " | " << ec.message() << std::endl;
326 return false;
327 }
328
329 fs::path final_dest = original;
330 int counter = 1;
331 while (fs::exists(final_dest)) {
332 final_dest = original.parent_path() /
333 (original.stem().string() + "_restored" +
334 std::to_string(counter) + original.extension().string());
335 ++counter;
336 }
337
338 ec.clear();
339 fs::rename(qf, final_dest, ec);
340
341 if (ec) {
342 if (ec == std::make_error_code(std::errc::cross_device_link)) {
343 std::cerr << _("DEBUG restore(): filesystem differente, uso copia e rimozione\n");
344
345 if (!copy_then_remove(qf, final_dest))
346 return false;
347 } else {
348 std::cerr << _("❌ Errore ripristino: ")
349 << ec.message() << std::endl;
350 return false;
351 }
352 }
353
354 remove_without_throwing(meta);
355
356 std::cout << _("✔ File ripristinato in: ")
357 << final_dest << std::endl;
358 return true;
359
360 } catch (const std::exception& e) {
361 std::cerr << _("❌ Errore ripristino: ")
362 << e.what() << std::endl;
363 return false;
364 }
365}
366
367bool Quarantine::remove(const std::string& quarantinedFile)
368{
369 try {
370 std::error_code ec;
371 fs::path qf = fs::absolute(fs::path(quarantinedFile), ec);
372 if (ec) {
373 std::cerr << _("❌ Percorso quarantena non valido: ")
374 << quarantinedFile << " | " << ec.message() << std::endl;
375 return false;
376 }
377 qf = qf.lexically_normal();
378
379 const std::string quarantine_dir_string = getQuarantinePath();
380 if (quarantine_dir_string.empty())
381 return false;
382
383 if (!is_path_inside(qf, fs::path(quarantine_dir_string))) {
384 std::cerr << _("❌ Eliminazione rifiutata: file esterno alla quarantena: ")
385 << qf << std::endl;
386 return false;
387 }
388
389 if (!is_regular_file_without_following_symlinks(qf, ec)) {
390 std::cerr << _("❌ Eliminazione rifiutata: elemento non regolare: ")
391 << qf << std::endl;
392 return false;
393 }
394
395 const fs::path meta(qf.string() + ".meta");
396
397 ec.clear();
398 fs::remove(qf, ec);
399 if (ec) {
400 std::cerr << _("❌ Errore eliminazione file in quarantena: ")
401 << ec.message() << std::endl;
402 return false;
403 }
404
405 remove_without_throwing(meta);
406
407 std::cout << _("✔ File eliminato dalla quarantena: ")
408 << qf << std::endl;
409 return true;
410
411 } catch (const std::exception& e) {
412 std::cerr << _("❌ Errore eliminazione: ")
413 << e.what() << std::endl;
414 return false;
415 }
416}
#define _(s)
static std::string getQuarantinePath()
Definition Backend.cpp:496
static bool restore(const std::string &quarantinedFile)
static bool remove(const std::string &quarantinedFile)
static std::string getQuarantinePath()
static bool move(const std::string &file)
bool is_regular_file_without_following_symlinks(const fs::path &path, std::error_code &ec)
bool is_path_inside(const fs::path &candidate, const fs::path &directory)
void remove_without_throwing(const fs::path &path)
fs::path normalize_path(const fs::path &path, std::error_code &ec)
bool copy_then_remove(const fs::path &source, const fs::path &destination)