Skip to content
API Documentation
BastionGuard™
archive_worker.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 <algorithm>
22#include <cctype>
23#include <cstdint>
24#include <filesystem>
25#include <fstream>
26#include <iostream>
27#include <string>
28#include <vector>
29
30namespace fs = std::filesystem;
31
32namespace {
33
35 std::string name;
36 uint16_t method = 0;
37 uint32_t compressed_size = 0;
38 uint32_t uncompressed_size = 0;
39 uint32_t local_header_offset = 0;
40 std::uint64_t data_offset = 0;
41 std::uint64_t data_end = 0;
42};
43
44constexpr uint32_t SIG_LOCAL_FILE_HEADER = 0x04034b50;
45constexpr uint32_t SIG_CENTRAL_DIR_HEADER = 0x02014b50;
46constexpr uint32_t SIG_END_OF_CENTRAL_DIR = 0x06054b50;
47
48uint16_t read_u16_le(std::ifstream& f) {
49 unsigned char b[2] = {0, 0};
50 f.read(reinterpret_cast<char*>(b), 2);
51 return static_cast<uint16_t>(b[0] | (b[1] << 8));
52}
53
54uint32_t read_u32_le(std::ifstream& f) {
55 unsigned char b[4] = {0, 0, 0, 0};
56 f.read(reinterpret_cast<char*>(b), 4);
57 return static_cast<uint32_t>(
58 static_cast<uint32_t>(b[0]) |
59 (static_cast<uint32_t>(b[1]) << 8) |
60 (static_cast<uint32_t>(b[2]) << 16) |
61 (static_cast<uint32_t>(b[3]) << 24)
62 );
63}
64
65bool read_exact(std::ifstream& f, char* buf, std::size_t len) {
66 f.read(buf, static_cast<std::streamsize>(len));
67 return static_cast<std::size_t>(f.gcount()) == len;
68}
69
70std::string read_string(std::ifstream& f, std::size_t len) {
71 std::string s(len, '\0');
72 if (!len) {
73 return s;
74 }
75 f.read(&s[0], static_cast<std::streamsize>(len));
76 if (static_cast<std::size_t>(f.gcount()) != len) {
77 return {};
78 }
79 return s;
80}
81
82bool contains_dotdot_path(const std::string& name) {
83 if (name.find("..") == std::string::npos) {
84 return false;
85 }
86 return name.find("../") != std::string::npos ||
87 name.find("..\\") != std::string::npos ||
88 name == "..";
89}
90
91bool is_absolute_or_weird_path(const std::string& name) {
92 if (name.empty()) {
93 return false;
94 }
95 if (name[0] == '/' || name[0] == '\\') {
96 return true;
97 }
98 if (name.size() >= 2 && std::isalpha(static_cast<unsigned char>(name[0])) && name[1] == ':') {
99 return true;
100 }
101 return false;
102}
103
104bool looks_like_nested_archive_name(const std::string& name) {
105 std::string lower = name;
106 std::transform(lower.begin(), lower.end(), lower.begin(),
107 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
108
109 return lower.ends_with(".zip") ||
110 lower.ends_with(".7z") ||
111 lower.ends_with(".rar") ||
112 lower.ends_with(".tar") ||
113 lower.ends_with(".gz") ||
114 lower.ends_with(".tgz") ||
115 lower.ends_with(".bz2") ||
116 lower.ends_with(".xz");
117}
118
119void print_json_bool(const char* key, bool value, bool comma = true) {
120 std::cout << "\"" << key << "\":" << (value ? "true" : "false");
121 if (comma) {
122 std::cout << ",";
123 }
124}
125
126void print_json_int(const char* key, int value, bool comma = true) {
127 std::cout << "\"" << key << "\":" << value;
128 if (comma) {
129 std::cout << ",";
130 }
131}
132
133void print_json_str(const char* key, const std::string& value, bool comma = true) {
134 std::cout << "\"" << key << "\":\"";
135 for (const char c : value) {
136 if (c == '"' || c == '\\') {
137 std::cout << '\\';
138 }
139 std::cout << c;
140 }
141 std::cout << "\"";
142 if (comma) {
143 std::cout << ",";
144 }
145}
146
147bool find_eocd(std::ifstream& f, std::uint64_t file_size, std::uint64_t& eocd_offset) {
148 if (file_size < 22) {
149 return false;
150 }
151
152 const std::uint64_t max_comment = 65535;
153 const std::uint64_t window = std::min<std::uint64_t>(file_size, 22 + max_comment);
154 const std::uint64_t start = file_size - window;
155
156 f.seekg(static_cast<std::streamoff>(start), std::ios::beg);
157 std::vector<char> buf(static_cast<std::size_t>(window));
158 if (!buf.empty()) {
159 f.read(buf.data(), static_cast<std::streamsize>(buf.size()));
160 if (static_cast<std::size_t>(f.gcount()) != buf.size()) {
161 return false;
162 }
163 }
164
165 for (std::int64_t i = static_cast<std::int64_t>(buf.size()) - 4; i >= 0; --i) {
166 const unsigned char* p =
167 reinterpret_cast<const unsigned char*>(buf.data() + i);
168 const uint32_t sig =
169 static_cast<uint32_t>(p[0]) |
170 (static_cast<uint32_t>(p[1]) << 8) |
171 (static_cast<uint32_t>(p[2]) << 16) |
172 (static_cast<uint32_t>(p[3]) << 24);
173
174 if (sig == SIG_END_OF_CENTRAL_DIR) {
175 eocd_offset = start + static_cast<std::uint64_t>(i);
176 return true;
177 }
178 }
179
180 return false;
181}
182
183// Cerca un extra field ZIP64 (header ID 0x0001) e legge comp/uncomp size a 64 bit
184// se i campi "legacy" valgono 0xFFFFFFFF. Ritorna true se ha trovato il blocco ZIP64.
185// I valori a 64 bit aggiornano *out_comp / *out_uncomp soltanto se erano sentinella.
187 const std::vector<unsigned char>& extra,
188 bool uncomp_is_sentinel,
189 bool comp_is_sentinel,
190 bool offset_is_sentinel,
191 std::uint64_t* out_uncomp,
192 std::uint64_t* out_comp,
193 std::uint64_t* out_offset)
194{
195 std::size_t i = 0;
196 while (i + 4 <= extra.size()) {
197 const uint16_t hid = static_cast<uint16_t>(extra[i] | (extra[i + 1] << 8));
198 const uint16_t hlen = static_cast<uint16_t>(extra[i + 2] | (extra[i + 3] << 8));
199 i += 4;
200 if (i + hlen > extra.size()) {
201 return false;
202 }
203 if (hid == 0x0001) {
204 std::size_t j = i;
205 auto read_u64 = [&](std::uint64_t& dst) {
206 if (j + 8 > i + hlen) return false;
207 std::uint64_t v = 0;
208 for (int k = 0; k < 8; ++k) {
209 v |= static_cast<std::uint64_t>(extra[j + k]) << (8 * k);
210 }
211 dst = v;
212 j += 8;
213 return true;
214 };
215 // Ordine: Uncompressed, Compressed, LocalHeaderOffset, DiskStart.
216 // Solo i campi "sentinella" sono presenti.
217 if (uncomp_is_sentinel && out_uncomp) {
218 if (!read_u64(*out_uncomp)) return true;
219 }
220 if (comp_is_sentinel && out_comp) {
221 if (!read_u64(*out_comp)) return true;
222 }
223 if (offset_is_sentinel && out_offset) {
224 if (!read_u64(*out_offset)) return true;
225 }
226 return true;
227 }
228 i += hlen;
229 }
230 return false;
231}
232
234 std::ifstream& f,
235 std::uint64_t file_size,
236 std::uint64_t local_offset,
237 uint16_t& method,
238 uint32_t& compressed_size,
239 uint32_t& uncompressed_size,
240 uint16_t& gp_flags_out,
241 std::string& filename,
242 std::uint64_t& data_offset,
243 std::uint64_t& data_end,
244 std::uint64_t& zip64_comp_size,
245 std::uint64_t& zip64_uncomp_size,
246 bool& has_zip64_extra)
247{
248 zip64_comp_size = 0;
249 zip64_uncomp_size = 0;
250 has_zip64_extra = false;
251
252 if (local_offset + 30 > file_size) {
253 return false;
254 }
255
256 f.seekg(static_cast<std::streamoff>(local_offset), std::ios::beg);
257 const uint32_t sig = read_u32_le(f);
258 if (sig != SIG_LOCAL_FILE_HEADER) {
259 return false;
260 }
261
262 (void)read_u16_le(f); // version needed
263 const uint16_t gp_flags = read_u16_le(f);
264 gp_flags_out = gp_flags;
265 method = read_u16_le(f);
266 (void)read_u16_le(f); // time
267 (void)read_u16_le(f); // date
268 (void)read_u32_le(f); // crc
269 compressed_size = read_u32_le(f);
270 uncompressed_size = read_u32_le(f);
271 const uint16_t name_len = read_u16_le(f);
272 const uint16_t extra_len = read_u16_le(f);
273
274 filename = read_string(f, name_len);
275 if (filename.size() != name_len) {
276 return false;
277 }
278
279 std::vector<unsigned char> extra(extra_len);
280 if (extra_len > 0) {
281 f.read(reinterpret_cast<char*>(extra.data()), extra_len);
282 if (static_cast<std::size_t>(f.gcount()) != extra_len) {
283 return false;
284 }
285 }
286
287 // ZIP64: se comp/uncomp size == 0xFFFFFFFF, i valori veri stanno nell'extra field 0x0001.
288 const bool comp_is_sentinel = (compressed_size == 0xFFFFFFFFu);
289 const bool uncomp_is_sentinel = (uncompressed_size == 0xFFFFFFFFu);
290
291 if (comp_is_sentinel || uncomp_is_sentinel) {
292 has_zip64_extra = parse_zip64_extra(
293 extra,
294 uncomp_is_sentinel,
295 comp_is_sentinel,
296 /*offset_is_sentinel=*/false,
297 &zip64_uncomp_size,
298 &zip64_comp_size,
299 nullptr);
300 }
301
302 data_offset = local_offset + 30ULL + name_len + extra_len;
303
304 // Dimensione affidabile per controllare se i dati escono dal file:
305 // - ZIP64 con extra valido → usa zip64_comp_size
306 // - data descriptor (bit 3) → non nota qui: usa 0 (bypassa il check di overflow)
307 // - altrimenti → compressed_size
308 const bool has_data_descriptor = (gp_flags & 0x08) != 0;
309 std::uint64_t effective_comp = compressed_size;
310 if (comp_is_sentinel && has_zip64_extra) {
311 effective_comp = zip64_comp_size;
312 } else if (has_data_descriptor && compressed_size == 0) {
313 // Dati con data descriptor: la dimensione vera sta dopo i dati, skippiamo il check.
314 effective_comp = 0;
315 }
316
317 data_end = data_offset + effective_comp;
318 if (data_offset > file_size || data_end > file_size) {
319 return false;
320 }
321
322 return true;
323}
324
325} // namespace
326
327int main(int argc, char** argv) {
328 if (argc != 2) {
329 std::cerr << "usage: archive_worker <archive-path>\n";
330 return 2;
331 }
332
333 const fs::path path(argv[1]);
334
335 bool is_zip = false;
336 bool stored_size_mismatch = false;
337 bool header_mismatch = false;
338 bool invalid_offset = false;
339 bool overlapping_entries = false;
340 bool nested_archive = false;
341
342 int risk_score = 0;
343 std::string risk = "CLEAN";
344 std::string reason_code;
345 std::string detail;
346
347 std::ifstream f(path, std::ios::binary);
348 if (!f) {
349 std::cerr << "cannot open file\n";
350 return 3;
351 }
352
353 f.seekg(0, std::ios::end);
354 const std::uint64_t file_size = static_cast<std::uint64_t>(f.tellg());
355 if (file_size < 4) {
356 std::cout << "{";
357 print_json_bool("is_zip", false);
358 print_json_bool("stored_size_mismatch", false);
359 print_json_bool("header_mismatch", false);
360 print_json_bool("invalid_offset", false);
361 print_json_bool("overlapping_entries", false);
362 print_json_bool("nested_archive", false);
363 print_json_int("risk_score", 0);
364 print_json_str("risk", "CLEAN");
365 print_json_str("reason_code", "");
366 print_json_str("detail", "", false);
367 std::cout << "}\n";
368 return 0;
369 }
370
371 f.seekg(0, std::ios::beg);
372 const uint32_t first_sig = read_u32_le(f);
373 if (first_sig != SIG_LOCAL_FILE_HEADER) {
374 std::cout << "{";
375 print_json_bool("is_zip", false);
376 print_json_bool("stored_size_mismatch", false);
377 print_json_bool("header_mismatch", false);
378 print_json_bool("invalid_offset", false);
379 print_json_bool("overlapping_entries", false);
380 print_json_bool("nested_archive", false);
381 print_json_int("risk_score", 0);
382 print_json_str("risk", "CLEAN");
383 print_json_str("reason_code", "");
384 print_json_str("detail", "", false);
385 std::cout << "}\n";
386 return 0;
387 }
388
389 is_zip = true;
390
391 std::uint64_t eocd_offset = 0;
392 if (!find_eocd(f, file_size, eocd_offset)) {
393 risk_score += 30;
394 risk = "MALFORMED";
395 reason_code = "ZIP_EOCD_NOT_FOUND";
396 detail = "end of central directory not found";
397 } else {
398 f.seekg(static_cast<std::streamoff>(eocd_offset), std::ios::beg);
399 const uint32_t eocd_sig = read_u32_le(f);
400 if (eocd_sig != SIG_END_OF_CENTRAL_DIR) {
401 risk_score += 30;
402 risk = "MALFORMED";
403 reason_code = "ZIP_EOCD_INVALID";
404 detail = "invalid end of central directory signature";
405 } else {
406 (void)read_u16_le(f); // disk number
407 (void)read_u16_le(f); // cd start disk
408 const uint16_t entries_this_disk = read_u16_le(f);
409 const uint16_t total_entries = read_u16_le(f);
410 const uint32_t cd_size = read_u32_le(f);
411 const uint32_t cd_offset = read_u32_le(f);
412 (void)read_u16_le(f); // comment len
413
414 if (entries_this_disk != total_entries) {
415 header_mismatch = true;
416 risk_score += 15;
417 }
418
419 if (static_cast<std::uint64_t>(cd_offset) + cd_size > file_size) {
420 invalid_offset = true;
421 risk_score += 45;
422 reason_code = "ZIP_INVALID_OFFSET";
423 detail = "central directory outside file";
424 } else {
425 std::vector<ZipEntryInfo> entries;
426 entries.reserve(total_entries);
427
428 std::uint64_t pos = cd_offset;
429 for (uint16_t i = 0; i < total_entries; ++i) {
430 if (pos + 46 > file_size) {
431 invalid_offset = true;
432 risk_score += 45;
433 if (reason_code.empty()) {
434 reason_code = "ZIP_INVALID_OFFSET";
435 detail = "central directory header outside file";
436 }
437 break;
438 }
439
440 f.seekg(static_cast<std::streamoff>(pos), std::ios::beg);
441 const uint32_t cd_sig = read_u32_le(f);
442 if (cd_sig != SIG_CENTRAL_DIR_HEADER) {
443 header_mismatch = true;
444 risk_score += 35;
445 if (reason_code.empty()) {
446 reason_code = "ZIP_HEADER_MISMATCH";
447 detail = "central directory signature mismatch";
448 }
449 break;
450 }
451
452 (void)read_u16_le(f); // version made by
453 (void)read_u16_le(f); // version needed
454 const uint16_t cd_gp_flags = read_u16_le(f);
455 const uint16_t cd_method = read_u16_le(f);
456 (void)read_u16_le(f); // time
457 (void)read_u16_le(f); // date
458 (void)read_u32_le(f); // crc32
459 const uint32_t cd_comp_size = read_u32_le(f);
460 const uint32_t cd_uncomp_size = read_u32_le(f);
461 const uint16_t name_len = read_u16_le(f);
462 const uint16_t extra_len = read_u16_le(f);
463 const uint16_t comment_len = read_u16_le(f);
464 (void)read_u16_le(f); // disk start
465 (void)read_u16_le(f); // int attrs
466 (void)read_u32_le(f); // ext attrs
467 const uint32_t local_header_offset_32 = read_u32_le(f);
468
469 std::string cd_name = read_string(f, name_len);
470 if (cd_name.size() != name_len) {
471 invalid_offset = true;
472 risk_score += 45;
473 if (reason_code.empty()) {
474 reason_code = "ZIP_INVALID_OFFSET";
475 detail = "cannot read central directory filename";
476 }
477 break;
478 }
479
480 // Leggi extra field della central directory (può contenere ZIP64 ext)
481 std::vector<unsigned char> cd_extra(extra_len);
482 if (extra_len > 0) {
483 f.read(reinterpret_cast<char*>(cd_extra.data()), extra_len);
484 if (static_cast<std::size_t>(f.gcount()) != extra_len) {
485 invalid_offset = true;
486 risk_score += 45;
487 if (reason_code.empty()) {
488 reason_code = "ZIP_INVALID_OFFSET";
489 detail = "cannot read central directory extra field";
490 }
491 break;
492 }
493 }
494
495 // ZIP64: se comp/uncomp/offset valgono 0xFFFFFFFF, il vero valore
496 // sta nell'extra field 0x0001.
497 const bool cd_comp_is_sentinel = (cd_comp_size == 0xFFFFFFFFu);
498 const bool cd_uncomp_is_sentinel = (cd_uncomp_size == 0xFFFFFFFFu);
499 const bool cd_off_is_sentinel = (local_header_offset_32 == 0xFFFFFFFFu);
500
501 std::uint64_t cd_comp_size_64 = cd_comp_size;
502 std::uint64_t cd_uncomp_size_64 = cd_uncomp_size;
503 std::uint64_t local_header_offset = local_header_offset_32;
504 bool cd_has_zip64_extra = false;
505 if (cd_comp_is_sentinel || cd_uncomp_is_sentinel || cd_off_is_sentinel) {
506 cd_has_zip64_extra = parse_zip64_extra(
507 cd_extra,
508 cd_uncomp_is_sentinel,
509 cd_comp_is_sentinel,
510 cd_off_is_sentinel,
511 cd_uncomp_is_sentinel ? &cd_uncomp_size_64 : nullptr,
512 cd_comp_is_sentinel ? &cd_comp_size_64 : nullptr,
513 cd_off_is_sentinel ? &local_header_offset : nullptr);
514
515 // Se il local header offset era sentinel ma non troviamo l'extra
516 // field, il file è davvero malformato.
517 if (cd_off_is_sentinel && !cd_has_zip64_extra) {
518 invalid_offset = true;
519 risk_score += 45;
520 if (reason_code.empty()) {
521 reason_code = "ZIP_INVALID_OFFSET";
522 detail = "missing ZIP64 extra field for local header offset";
523 }
524 }
525 }
526
527 if (contains_dotdot_path(cd_name) || is_absolute_or_weird_path(cd_name)) {
528 // Segnale FORTE: un file legittimo non ha mai "../" o path
529 // assoluti nel nome. Zip-slip classico.
530 header_mismatch = true;
531 risk_score += 40;
532 if (reason_code.empty()) {
533 reason_code = "ZIP_PATH_TRAVERSAL_NAME";
534 detail = "suspicious path inside archive";
535 }
536 }
537
538 if (looks_like_nested_archive_name(cd_name)) {
539 nested_archive = true;
540 }
541
542 if (local_header_offset >= file_size) {
543 invalid_offset = true;
544 risk_score += 45;
545 if (reason_code.empty()) {
546 reason_code = "ZIP_INVALID_OFFSET";
547 detail = "local header offset outside file";
548 }
549 } else {
550 uint16_t local_method = 0;
551 uint32_t local_comp_size = 0;
552 uint32_t local_uncomp_size = 0;
553 uint16_t local_gp_flags = 0;
554 std::string local_name;
555 std::uint64_t data_offset = 0;
556 std::uint64_t data_end = 0;
557 std::uint64_t local_zip64_comp = 0;
558 std::uint64_t local_zip64_uncomp = 0;
559 bool local_has_zip64_extra = false;
560
561 if (!read_local_entry(
562 f,
563 file_size,
564 local_header_offset,
565 local_method,
566 local_comp_size,
567 local_uncomp_size,
568 local_gp_flags,
569 local_name,
570 data_offset,
571 data_end,
572 local_zip64_comp,
573 local_zip64_uncomp,
574 local_has_zip64_extra))
575 {
576 invalid_offset = true;
577 risk_score += 45;
578 if (reason_code.empty()) {
579 reason_code = "ZIP_INVALID_OFFSET";
580 detail = "invalid local header";
581 }
582 } else {
583 // Data descriptor (bit 3): per lo standard, i size nel
584 // local header possono essere 0 e i valori reali sono nel
585 // data descriptor dopo i dati. Non è un indizio di tampering.
586 const bool local_has_dd = (local_gp_flags & 0x08) != 0;
587 const bool cd_has_dd = (cd_gp_flags & 0x08) != 0;
588
589 // Calcoliamo size effettive (ZIP64-aware) per il confronto.
590 std::uint64_t eff_local_comp = local_comp_size;
591 std::uint64_t eff_local_uncomp = local_uncomp_size;
592 if (local_comp_size == 0xFFFFFFFFu && local_has_zip64_extra) {
593 eff_local_comp = local_zip64_comp;
594 }
595 if (local_uncomp_size == 0xFFFFFFFFu && local_has_zip64_extra) {
596 eff_local_uncomp = local_zip64_uncomp;
597 }
598
599 // Le size possono essere legittimamente 0 nel local header
600 // quando c'è data descriptor: saltiamo il confronto.
601 const bool skip_size_check = local_has_dd &&
602 local_comp_size == 0 &&
603 local_uncomp_size == 0;
604
605 bool name_mismatch = (local_name != cd_name);
606 bool method_mismatch = (local_method != cd_method);
607 bool size_mismatch = !skip_size_check &&
608 (eff_local_comp != cd_comp_size_64 ||
609 eff_local_uncomp != cd_uncomp_size_64);
610 // Anche il flag data-descriptor dovrebbe combaciare; se no
611 // è un segnale debole, ma non "vero" mismatch semantico.
612 bool dd_flag_mismatch = (local_has_dd != cd_has_dd);
613
614 if (name_mismatch || method_mismatch || size_mismatch) {
615 header_mismatch = true;
616 risk_score += 35;
617 if (reason_code.empty()) {
618 reason_code = "ZIP_HEADER_MISMATCH";
619 detail = "local header and central directory differ";
620 }
621 } else if (dd_flag_mismatch) {
622 // Segnale debole: flag non combacia ma dati sono coerenti.
623 risk_score += 5;
624 }
625
626 // STORED-size-mismatch: un file non compresso DEVE avere
627 // compressed_size == uncompressed_size. Skip se sentinel o dd.
628 if (cd_method == 0 &&
629 !cd_comp_is_sentinel &&
630 !cd_uncomp_is_sentinel &&
631 !cd_has_dd &&
632 cd_comp_size != cd_uncomp_size)
633 {
634 stored_size_mismatch = true;
635 risk_score += 40;
636 reason_code = "ZIP_STORED_SIZE_MISMATCH";
637 detail = "STORED entry but compressed and uncompressed sizes differ";
638 }
639
640 ZipEntryInfo entry;
641 entry.name = cd_name;
642 entry.method = cd_method;
643 entry.compressed_size = cd_comp_size;
644 entry.uncompressed_size = cd_uncomp_size;
645 entry.local_header_offset =
646 static_cast<uint32_t>(local_header_offset & 0xFFFFFFFFu);
647 entry.data_offset = data_offset;
648 // Usa size ZIP64-corretta; con data descriptor non
649 // conosciamo la dimensione esatta lato local header,
650 // quindi usiamo la central directory (affidabile).
651 entry.data_end = data_offset + cd_comp_size_64;
652 entries.push_back(std::move(entry));
653 }
654 }
655
656 pos += 46ULL + name_len + extra_len + comment_len;
657 if (pos > file_size) {
658 invalid_offset = true;
659 risk_score += 45;
660 if (reason_code.empty()) {
661 reason_code = "ZIP_INVALID_OFFSET";
662 detail = "central directory entry overflow";
663 }
664 break;
665 }
666 }
667
668 if (!entries.empty()) {
669 std::sort(entries.begin(), entries.end(),
670 [](const ZipEntryInfo& a, const ZipEntryInfo& b) {
671 return a.data_offset < b.data_offset;
672 });
673
674 for (std::size_t i = 1; i < entries.size(); ++i) {
675 if (entries[i].data_offset < entries[i - 1].data_end) {
676 overlapping_entries = true;
677 risk_score += 50;
678 if (reason_code.empty()) {
679 reason_code = "ZIP_OVERLAPPING_ENTRIES";
680 detail = "overlapping archive entries detected";
681 }
682 break;
683 }
684 }
685 }
686
687 if (nested_archive) {
688 // Segnale molto debole: JAR, AAB, installer, archivi di backup,
689 // bundle di distribuzione spesso contengono archivi annidati.
690 // Da solo non deve scattare SUSPICIOUS.
691 risk_score += 5;
692 if (reason_code.empty()) {
693 reason_code = "ZIP_NESTED_ARCHIVE";
694 detail = "nested archive detected";
695 }
696 }
697 }
698 }
699 }
700
701 // Soglie:
702 // - risk >= 60 → EVASIVE (più segnali forti, o uno forte + correlati)
703 // - risk >= 30 → SUSPICIOUS (un segnale forte tipo header_mismatch o stored_size_mismatch)
704 // - altrimenti → CLEAN (anche con segnali molto deboli come un nested archive isolato)
705 // Se nel processo è già stato impostato "MALFORMED" (EOCD non trovato), preserviamolo.
706 if (risk == "MALFORMED") {
707 // no-op: preserva MALFORMED
708 } else if (risk_score >= 60) {
709 risk = "EVASIVE";
710 } else if (risk_score >= 30) {
711 risk = "SUSPICIOUS";
712 } else {
713 risk = "CLEAN";
714 }
715
716 std::cout << "{";
717 print_json_bool("is_zip", is_zip);
718 print_json_bool("stored_size_mismatch", stored_size_mismatch);
719 print_json_bool("header_mismatch", header_mismatch);
720 print_json_bool("invalid_offset", invalid_offset);
721 print_json_bool("overlapping_entries", overlapping_entries);
722 print_json_bool("nested_archive", nested_archive);
723 print_json_int("risk_score", risk_score);
724 print_json_str("risk", risk);
725 print_json_str("reason_code", reason_code);
726 print_json_str("detail", detail, false);
727 std::cout << "}\n";
728
729 return 0;
730}
int main(int argc, char *argv[])
bool read_local_entry(std::ifstream &f, std::uint64_t file_size, std::uint64_t local_offset, uint16_t &method, uint32_t &compressed_size, uint32_t &uncompressed_size, uint16_t &gp_flags_out, std::string &filename, std::uint64_t &data_offset, std::uint64_t &data_end, std::uint64_t &zip64_comp_size, std::uint64_t &zip64_uncomp_size, bool &has_zip64_extra)
void print_json_str(const char *key, const std::string &value, bool comma=true)
void print_json_int(const char *key, int value, bool comma=true)
bool parse_zip64_extra(const std::vector< unsigned char > &extra, bool uncomp_is_sentinel, bool comp_is_sentinel, bool offset_is_sentinel, std::uint64_t *out_uncomp, std::uint64_t *out_comp, std::uint64_t *out_offset)
bool read_exact(std::ifstream &f, char *buf, std::size_t len)
bool contains_dotdot_path(const std::string &name)
bool looks_like_nested_archive_name(const std::string &name)
std::string read_string(std::ifstream &f, std::size_t len)
void print_json_bool(const char *key, bool value, bool comma=true)
uint16_t read_u16_le(std::ifstream &f)
bool is_absolute_or_weird_path(const std::string &name)
bool find_eocd(std::ifstream &f, std::uint64_t file_size, std::uint64_t &eocd_offset)
uint32_t read_u32_le(std::ifstream &f)