fix(security): harden BastionGuard and prepare v2.0.4 release

This commit is contained in:
specialworld83 2026-10-14 16:38:38 +02:00
commit ca1dba5a74
149 changed files with 5243 additions and 1493 deletions

View file

@ -524,6 +524,7 @@ set(BastionGuard_SOURCES
src/password_manager/PasswordManagerPage.cpp
src/backup/BackupPage.cpp
src/Utils.cpp
src/rootguard/RootGuardPage.cpp
)
if(ENABLE_CEF)
@ -542,8 +543,8 @@ target_include_directories(BastionGuard
)
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.3"
BASTIONGUARD_BUILD=20260803
BASTIONGUARD_VERSION="2.0.4"
BASTIONGUARD_BUILD=20261013
)
target_link_libraries(BastionGuard

View file

@ -16,7 +16,7 @@
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>yes</allow_active>
<allow_active>auth_admin_keep</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">
/usr/share/BastionGuard/data/scripts/BastionGuard-net-setup.sh /usr/libexec/BastionGuard-helper /usr/bin/nft /usr/bin/firewall-cmd /usr/sbin/ufw
@ -34,7 +34,7 @@
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>yes</allow_active>
<allow_active>auth_admin_keep</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">
/usr/sbin/nft /usr/bin/nft /usr/bin/cp /bin/systemctl /usr/lib/systemd/systemd
@ -52,11 +52,10 @@
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>yes</allow_active>
<allow_active>auth_admin_keep</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">
/bin/systemctl /usr/bin/systemctl /usr/lib/systemd/systemd
</annotate>
<!-- pkexec exec.path is exactly one absolute executable, not a list. -->
<annotate key="org.freedesktop.policykit.exec.path">/usr/libexec/bastionguard/bastionguard-service</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_user_interaction">true</annotate>
</action>
@ -70,11 +69,10 @@
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>yes</allow_active>
<allow_active>auth_admin_keep</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">
/bin/sh /usr/bin/bash /usr/bin/install /usr/bin/openssl /usr/bin/trust /usr/bin/update-ca-certificates
</annotate>
<!-- No generic exec.path mapping here: shells and general-purpose tools
must never receive passwordless pkexec authorization. -->
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_user_interaction">true</annotate>
</action>

View file

@ -2,7 +2,7 @@
# Maintainer: BastionGuard <info@bastionguard.eu>
pkgname=bastionguard
pkgver=2.0.3
pkgver=2.0.4
pkgrel=0
pkgdesc="Transparent security control plane for Linux desktops"
url="https://bastionguard.eu/"

View file

@ -1851,3 +1851,214 @@ label.suggested-action { color: #7a8190; font-weight: 600; }
font-size: 11px;
margin-right: 4px;
}
/* RootGuardPage optional theme integration. */
.rootguard-rule-list,
.rootguard-incident-list,
.rootguard-terminal {
border-radius: 6px;
}
.rootguard-rule-list row,
.rootguard-incident-row {
padding: 2px;
}
/* ============================================================
* BastionGuard RootGuardPage — Dark theme
* Append this block to BastionGuard-dark.css.
* Palette aligned with the current BastionGuard dark theme.
* ============================================================ */
/* RootGuard header. */
.secure-headerbar .secure-header-title,
.secure-header-title {
color: #ffffff;
font-size: 15px;
font-weight: 600;
}
/* Compact tab bars used by RootGuardPage and Application rules. */
stackswitcher {
background: transparent;
padding: 2px;
}
stackswitcher button {
background-color: #242830;
color: #c9cdd4;
border: 1px solid #3a3f4b;
border-radius: 7px;
padding: 6px 14px;
margin: 0 2px;
min-height: 28px;
font-weight: 500;
}
stackswitcher button label {
color: #c9cdd4;
}
stackswitcher button:hover {
background-color: #2e3541;
border-color: #526073;
}
stackswitcher button:checked {
background-color: #1e4f82;
border-color: #4d8fe0;
color: #ffffff;
}
stackswitcher button:checked label {
color: #ffffff;
font-weight: 600;
}
stackswitcher button:disabled,
stackswitcher button:disabled label {
background-color: #242830;
border-color: #343945;
color: #6f7785;
}
/* Status summary cards. */
.rootguard-status-item {
background-color: #242830;
border: 1px solid #3a3f4b;
border-radius: 10px;
padding: 8px 12px;
}
.rootguard-status-label {
color: #9aa4b2;
font-size: 12px;
font-weight: 500;
}
.rootguard-status-value {
color: #e4e7eb;
font-size: 14px;
font-weight: 600;
}
.dim-label {
color: #9aa4b2;
font-size: 12px;
}
/* Incident and rule lists. */
.rootguard-incident-list,
.rootguard-rule-list {
background-color: #20242b;
border: 1px solid #3a3f4b;
border-radius: 8px;
}
.rootguard-incident-list row,
.rootguard-rule-list row {
background-color: transparent;
border-bottom: 1px solid #303540;
padding: 2px;
}
.rootguard-incident-list row:last-child,
.rootguard-rule-list row:last-child {
border-bottom: none;
}
.rootguard-incident-list row:hover,
.rootguard-rule-list row:hover {
background-color: #2a303a;
}
.rootguard-incident-list row:selected,
.rootguard-rule-list row:selected {
background-color: #243f61;
color: #ffffff;
}
.rootguard-incident-list row:selected *,
.rootguard-rule-list row:selected * {
color: #ffffff;
}
.rootguard-incident-row {
background-color: transparent;
border-radius: 7px;
padding: 8px;
}
.rootguard-blocked {
color: #ff7373;
font-weight: 600;
}
.rootguard-alert {
color: #f0ad4e;
font-weight: 600;
}
/* Event console. */
.rootguard-terminal,
.rootguard-terminal text {
background-color: #0f1117;
color: #c9d1d9;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
}
.rootguard-terminal {
border: 1px solid #3a3f4b;
}
.rootguard-terminal text selection {
background-color: #1e4f82;
color: #ffffff;
}
.rootguard-terminal scrollbar trough,
.rootguard-incident-list scrollbar trough,
.rootguard-rule-list scrollbar trough {
background-color: #20242b;
border-radius: 6px;
}
.rootguard-terminal scrollbar slider,
.rootguard-incident-list scrollbar slider,
.rootguard-rule-list scrollbar slider {
background-color: #4a5060;
border-radius: 6px;
min-width: 6px;
min-height: 24px;
}
.rootguard-terminal scrollbar slider:hover,
.rootguard-incident-list scrollbar slider:hover,
.rootguard-rule-list scrollbar slider:hover {
background-color: #667085;
}
/* RootGuard informational popover and dialogs. */
popover contents {
background-color: #242830;
color: #e4e7eb;
border: 1px solid #3a3f4b;
border-radius: 9px;
}
window.BastionGuard-popup.BastionGuard-dialog {
background-color: #242830;
border: 1px solid #3a3f4b;
border-radius: 12px;
}
window.BastionGuard-popup.BastionGuard-dialog label {
color: #e4e7eb;
}
window.BastionGuard-popup.BastionGuard-dialog .dim-label {
color: #9aa4b2;
}

View file

@ -1857,3 +1857,214 @@ label.suggested-action { color: #868e96; font-weight: 600; }
font-size: 11px;
margin-right: 4px;
}
/* RootGuardPage optional theme integration. */
.rootguard-rule-list,
.rootguard-incident-list,
.rootguard-terminal {
border-radius: 6px;
}
.rootguard-rule-list row,
.rootguard-incident-row {
padding: 2px;
}
/* ============================================================
* BastionGuard RootGuardPage — Light theme
* Append this block to BastionGuard.css.
* Palette aligned with the current BastionGuard light theme.
* ============================================================ */
/* RootGuard header. */
.secure-headerbar .secure-header-title,
.secure-header-title {
color: #ffffff;
font-size: 15px;
font-weight: 600;
}
/* Compact tab bars used by RootGuardPage and Application rules. */
stackswitcher {
background: transparent;
padding: 2px;
}
stackswitcher button {
background-color: #ffffff;
color: #495057;
border: 1px solid #dee2e6;
border-radius: 7px;
padding: 6px 14px;
margin: 0 2px;
min-height: 28px;
font-weight: 500;
}
stackswitcher button label {
color: #495057;
}
stackswitcher button:hover {
background-color: #edf3f9;
border-color: #b8c9dc;
}
stackswitcher button:checked {
background-color: #3a6ea5;
border-color: #3a6ea5;
color: #ffffff;
}
stackswitcher button:checked label {
color: #ffffff;
font-weight: 600;
}
stackswitcher button:disabled,
stackswitcher button:disabled label {
background-color: #f1f3f5;
border-color: #dee2e6;
color: #8a9099;
}
/* Status summary cards. */
.rootguard-status-item {
background-color: #ffffff;
border: 1px solid #dee2e6;
border-radius: 10px;
padding: 8px 12px;
}
.rootguard-status-label {
color: #667085;
font-size: 12px;
font-weight: 500;
}
.rootguard-status-value {
color: #212529;
font-size: 14px;
font-weight: 600;
}
.dim-label {
color: #667085;
font-size: 12px;
}
/* Incident and rule lists. */
.rootguard-incident-list,
.rootguard-rule-list {
background-color: #ffffff;
border: 1px solid #dee2e6;
border-radius: 8px;
}
.rootguard-incident-list row,
.rootguard-rule-list row {
background-color: transparent;
border-bottom: 1px solid #edf0f2;
padding: 2px;
}
.rootguard-incident-list row:last-child,
.rootguard-rule-list row:last-child {
border-bottom: none;
}
.rootguard-incident-list row:hover,
.rootguard-rule-list row:hover {
background-color: #f5f8fb;
}
.rootguard-incident-list row:selected,
.rootguard-rule-list row:selected {
background-color: #dce9f5;
color: #212529;
}
.rootguard-incident-list row:selected *,
.rootguard-rule-list row:selected * {
color: #212529;
}
.rootguard-incident-row {
background-color: transparent;
border-radius: 7px;
padding: 8px;
}
.rootguard-blocked {
color: #c62828;
font-weight: 600;
}
.rootguard-alert {
color: #a45a00;
font-weight: 600;
}
/* Event console. */
.rootguard-terminal,
.rootguard-terminal text {
background-color: #f8f9fa;
color: #303640;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
}
.rootguard-terminal {
border: 1px solid #cfd6dd;
}
.rootguard-terminal text selection {
background-color: #3a6ea5;
color: #ffffff;
}
.rootguard-terminal scrollbar trough,
.rootguard-incident-list scrollbar trough,
.rootguard-rule-list scrollbar trough {
background-color: #eef1f4;
border-radius: 6px;
}
.rootguard-terminal scrollbar slider,
.rootguard-incident-list scrollbar slider,
.rootguard-rule-list scrollbar slider {
background-color: #adb5bd;
border-radius: 6px;
min-width: 6px;
min-height: 24px;
}
.rootguard-terminal scrollbar slider:hover,
.rootguard-incident-list scrollbar slider:hover,
.rootguard-rule-list scrollbar slider:hover {
background-color: #868e96;
}
/* RootGuard informational popover and dialogs. */
popover contents {
background-color: #ffffff;
color: #212529;
border: 1px solid #dee2e6;
border-radius: 9px;
}
window.BastionGuard-popup.BastionGuard-dialog {
background-color: #ffffff;
border: 1px solid #cfd6dd;
border-radius: 12px;
}
window.BastionGuard-popup.BastionGuard-dialog label {
color: #212529;
}
window.BastionGuard-popup.BastionGuard-dialog .dim-label {
color: #667085;
}

View file

View file

46
data/icons/rootguard.svg Normal file
View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
height="800"
width="800"
version="1.1"
id="_x32_"
viewBox="0 0 512 512"
xml:space="preserve"
sodipodi:docname="rootguard.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"><inkscape:page
x="0"
y="0"
width="512"
height="512"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview>
<style
type="text/css"
id="style1">
.st0{fill:#000000;}
</style>
<g
id="g1"
style="fill:#ffffff">
<path
class="st0"
d="m 429.823,409.401 c -11.741,-17.577 -29.574,-27.295 -46.322,-33.618 -8.41,-3.157 -16.624,-5.492 -23.859,-7.474 -7.227,-1.965 -13.508,-3.618 -17.725,-5.213 -7.384,-2.738 -15.186,-6.289 -20.628,-10.112 -2.722,-1.899 -4.81,-3.856 -6.027,-5.517 -1.225,-1.702 -1.562,-2.878 -1.578,-3.906 0,-7.103 0,-15.974 0,-27.666 9.447,-10.508 23.021,-26.794 28.595,-52.618 1.949,-0.88 3.873,-1.875 5.731,-3.166 4.62,-3.19 8.542,-7.819 11.864,-14.116 3.346,-6.323 6.306,-14.405 9.463,-25.463 1.603,-5.607 2.343,-10.474 2.343,-14.815 0.008,-4.999 -1.028,-9.332 -2.911,-12.85 -2.482,-4.678 -6.29,-7.457 -9.701,-9.002 l 7.703,-11.478 V 159.66 c 0.732,-0.995 1.2,-1.908 1.085,-2.631 v -27.567 l 19.6,-39.184 c 4.966,-9.932 1.127,-22.018 -8.666,-27.263 L 265.693,2.43 c -6.05,-3.24 -13.327,-3.24 -19.378,0 L 133.21,63.015 c -9.784,5.245 -13.624,17.331 -8.657,27.263 l 19.6,39.209 v 27.543 c -0.123,0.748 0.288,1.66 1.085,2.696 v 12.662 l 7.704,11.46 c -0.798,0.354 -1.62,0.757 -2.467,1.275 -2.664,1.644 -5.352,4.218 -7.235,7.745 -1.883,3.519 -2.919,7.851 -2.91,12.85 0,4.341 0.74,9.208 2.343,14.815 4.226,14.726 8.041,24.238 12.982,31.218 2.475,3.461 5.287,6.24 8.345,8.361 1.858,1.29 3.783,2.286 5.731,3.166 5.574,25.824 19.148,42.111 28.595,52.618 0,11.692 0,20.562 0,27.666 0,0.872 -0.354,2.13 -1.702,3.93 -1.989,2.672 -6.108,5.912 -11,8.657 -4.876,2.771 -10.483,5.18 -15.391,6.907 -5.764,2.047 -15.054,4.168 -25.455,7.153 -15.629,4.522 -34.111,11.033 -49.149,23.925 -7.506,6.446 -14.092,14.544 -18.729,24.607 -4.636,10.056 -7.301,22.01 -7.292,35.986 0,3.248 0.14,6.602 0.428,10.072 0.222,2.434 1.142,4.407 2.236,6.034 2.064,3.042 4.81,5.295 8.246,7.622 6.019,3.979 14.364,7.876 25.043,11.699 31.958,11.396 84.881,21.829 150.449,21.846 53.268,0 98.232,-6.914 130.329,-15.605 16.057,-4.349 28.875,-9.118 38.141,-13.878 4.646,-2.393 8.394,-4.768 11.37,-7.358 1.488,-1.316 2.795,-2.688 3.889,-4.325 1.094,-1.627 2.015,-3.6 2.228,-6.034 0.288,-3.461 0.428,-6.816 0.428,-10.046 0.024,-18.64 -4.762,-33.661 -12.572,-45.353 z M 231.936,137.544 c 0,-13.294 10.77,-24.065 24.073,-24.065 13.286,0 24.056,10.77 24.056,24.065 v 2.31 c 0,13.287 -10.77,24.065 -24.056,24.065 -13.303,0 -24.073,-10.778 -24.073,-24.065 z m 4.53,323.095 -54.806,-86.517 c 4.991,-2.088 10.104,-4.727 14.84,-7.777 l 31.366,45.104 15.128,-28.455 z m -9.349,-61.802 -25.273,-36.332 c 2.31,-1.891 4.505,-3.897 6.306,-6.256 2.54,-3.33 4.472,-7.49 4.596,-12.218 l 35.386,15.276 z m -14.322,-61.95 c 0,-6.462 0,-14.142 0,-23.761 v -2.77 l -1.858,-2.072 c -9.874,-10.993 -23.234,-25.586 -27.871,-51.516 l -0.732,-4.144 -3.955,-1.414 c -2.524,-0.896 -4.439,-1.817 -6.116,-2.984 -2.475,-1.743 -4.72,-4.127 -7.342,-9.011 -2.59,-4.86 -5.328,-12.119 -8.328,-22.65 -1.324,-4.604 -1.792,-8.181 -1.792,-10.845 0.008,-3.083 0.6,-4.884 1.2,-6.026 0.913,-1.653 2.032,-2.36 3.454,-2.936 0.642,-0.246 1.29,-0.378 1.842,-0.468 l 14.873,22.157 6.363,-37.285 0.699,-2.336 c 19.42,6.159 44.644,11.1 72.779,11.1 28.06,0 53.366,-4.9 72.794,-11.041 l 0.674,2.277 6.364,37.285 14.873,-22.141 c 0.937,0.148 2.195,0.46 3.166,1.102 0.83,0.535 1.504,1.151 2.121,2.286 0.6,1.142 1.192,2.943 1.209,6.026 0,2.664 -0.477,6.24 -1.792,10.845 -3.988,14.059 -7.564,22.231 -10.853,26.77 -1.644,2.293 -3.149,3.724 -4.818,4.892 -1.677,1.168 -3.592,2.089 -6.116,2.984 l -3.963,1.414 -0.724,4.144 c -4.636,25.93 -17.998,40.524 -27.871,51.516 l -1.858,2.072 v 2.77 c 0,9.62 0,17.299 0,23.761 l -43.204,18.655 z m 86.467,7.144 c 0.107,4.572 1.883,8.707 4.342,12.012 1.841,2.483 4.053,4.636 6.47,6.585 l -25.191,36.208 -21.006,-39.53 z m -23.719,116.624 -6.528,-77.653 15.12,28.447 31.308,-45.022 c 1.044,0.667 2.089,1.324 3.182,1.949 3.789,2.154 7.794,3.98 11.798,5.664 z"
id="path1"
style="fill:#ffffff" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

51
data/icons/vpn.svg Normal file
View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
width="728.03668"
height="800"
viewBox="-5.53 0 111.82643 122.88"
version="1.1"
id="Layer_1"
xml:space="preserve"
sodipodi:docname="vpn.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"><inkscape:page
x="0"
y="0"
width="111.82643"
height="122.88"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview>
<style
type="text/css"
id="style1">.st0{fill-rule:evenodd;clip-rule:evenodd;}</style>
<g
id="g1"
style="fill:#ffffff"
transform="translate(-5.5279743)">
<path
class="st0"
d="M 55.72,0 C 76.59,13.2 95.39,19.47 111.57,17.99 114.41,75.1 93.32,108.83 55.94,122.88 19.84,109.72 -1.5,77.42 0.08,17.11 19.07,18.1 37.69,14.01 55.72,0 Z M 20.14,44.81 h 7.11 l 4.95,15.83 4.87,-15.83 h 6.9 L 35.81,66.8 h -7.36 z m 25.53,0 h 11.29 c 2.46,0 4.3,0.58 5.52,1.76 1.22,1.17 1.84,2.84 1.84,4.99 0,2.22 -0.67,3.96 -2,5.21 -1.34,1.25 -3.37,1.87 -6.11,1.87 h -3.72 v 8.16 h -6.82 z m 6.82,9.37 h 1.66 c 1.31,0 2.23,-0.23 2.76,-0.68 0.53,-0.46 0.79,-1.03 0.79,-1.75 0,-0.69 -0.23,-1.28 -0.69,-1.76 -0.46,-0.48 -1.32,-0.72 -2.59,-0.72 h -1.94 v 4.91 z m 14.92,-9.37 h 6.35 l 8.28,12.17 V 44.81 h 6.41 V 66.8 H 82.04 L 73.8,54.72 V 66.8 H 67.41 Z M 55.72,7.04 C 74.19,18.73 90.85,24.26 105.16,22.97 107.67,73.52 88.98,103.38 55.9,115.84 23.97,104.19 5.06,75.62 6.46,22.23 23.27,23.11 39.75,19.47 55.72,7.08 Z"
id="path1"
style="fill:#ffffff" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

0
data/init/common/bastionguard-periodic Executable file → Normal file
View file

0
data/init/common/bastionguard-sanesecurity-update Executable file → Normal file
View file

0
data/init/common/bastionguard-service Executable file → Normal file
View file

0
data/init/common/bastionguard-supervise Executable file → Normal file
View file

0
data/init/dinit/bastionguard-dinit-run Executable file → Normal file
View file

0
data/init/dinit/bastionguard-dinit-user-run Executable file → Normal file
View file

0
data/init/openrc/bastionguard-openrc-service Executable file → Normal file
View file

0
data/init/sysvinit/bastionguard-sysv-service.in Executable file → Normal file
View file

0
data/scripts/BastionGuard-export-env.sh Executable file → Normal file
View file

0
data/scripts/BastionGuard-locale.sh Executable file → Normal file
View file

0
data/scripts/BastionGuard-net-setup.sh Executable file → Normal file
View file

View file

@ -1,63 +1,155 @@
#!/bin/bash
set -e
set -euo pipefail
umask 077
LOG_DIR="/var/log/BastionGuard"
LOG_FILE="$LOG_DIR/phishing_updater.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
PHISH_SYS="/usr/share/BastionGuard/data/phishing"
DNSMASQ_CONF="/etc/dnsmasq.d/BastionGuard-blacklist.conf"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
mkdir -p "$LOG_DIR" "$PHISH_SYS"
if [[ ${EUID} -ne 0 ]]; then
echo "BastionGuard-phishing-updater: must run as root via pkexec" >&2
exit 77
fi
install -d -o root -g root -m 0755 "$LOG_DIR" "$PHISH_SYS"
echo "[$DATE] Avvio aggiornamento blacklist..." >> "$LOG_FILE"
# 1. Installa blacklist da /tmp (preparate dal backend user-space)
if [[ -f "/tmp/bastionguard-blacklist.txt" ]]; then
install -m 0644 /tmp/bastionguard-blacklist.txt \
"$PHISH_SYS/blacklist.txt"
echo "[$DATE] blacklist.txt installata" >> "$LOG_FILE"
# The systemd timer may invoke the helper without staged input. Preserve the
# old no-op behaviour, but never consume predictable files from /tmp.
if [[ $# -eq 0 ]]; then
echo "[$DATE] Nessun staging directory fornita; nessun aggiornamento da installare." >> "$LOG_FILE"
exit 0
fi
if [[ $# -ne 1 ]]; then
echo "Uso: $0 <staging-dir>" >&2
exit 64
fi
if [[ -f "/tmp/bastionguard-blacklist-reduce.txt" ]]; then
install -m 0644 /tmp/bastionguard-blacklist-reduce.txt \
"$PHISH_SYS/blacklist-reduce.txt"
STAGE="$1"
CALLER_UID="${PKEXEC_UID:-${SUDO_UID:-}}"
if [[ -z "$CALLER_UID" || ! "$CALLER_UID" =~ ^[0-9]+$ || "$CALLER_UID" -eq 0 ]]; then
echo "BastionGuard-phishing-updater: caller UID non affidabile" >&2
exit 65
fi
# 2. Merge whitelist utente → copia in sys se esiste
REAL_USER="${SUDO_USER:-$(logname 2>/dev/null || echo '')}"
if [[ -n "$REAL_USER" ]]; then
USER_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
WL_TXT="$USER_HOME/.local/share/BastionGuard/phishing/whitelist.txt"
if [[ -f "$WL_TXT" ]]; then
install -m 0644 "$WL_TXT" "$PHISH_SYS/whitelist.txt"
echo "[$DATE] whitelist utente copiata in sys ($WL_TXT)" >> "$LOG_FILE"
if [[ -L "$STAGE" || ! -d "$STAGE" ]]; then
echo "BastionGuard-phishing-updater: staging directory non valida" >&2
exit 66
fi
STAGE_REAL=$(realpath -e -- "$STAGE")
case "$STAGE_REAL" in
/tmp/bastionguard-phish.*) ;;
*) echo "BastionGuard-phishing-updater: staging path non autorizzato" >&2; exit 67 ;;
esac
if [[ $(stat -c '%u' -- "$STAGE_REAL") != "$CALLER_UID" || \
$(stat -c '%a' -- "$STAGE_REAL") != "700" ]]; then
echo "BastionGuard-phishing-updater: owner/mode staging non validi" >&2
exit 68
fi
validate_stage_file() {
local f="$1"
[[ -f "$f" && ! -L "$f" ]] || return 1
[[ $(stat -c '%u' -- "$f") == "$CALLER_UID" ]] || return 1
[[ $(stat -c '%a' -- "$f") == "600" ]] || return 1
}
BLACKLIST="$STAGE_REAL/blacklist.txt"
BLACKLIST_REDUCE="$STAGE_REAL/blacklist-reduce.txt"
DNSMASQ_STAGE="$STAGE_REAL/dnsmasq-blacklist.conf"
validate_stage_file "$BLACKLIST" || { echo "blacklist staging non valida" >&2; exit 69; }
validate_stage_file "$BLACKLIST_REDUCE" || { echo "blacklist-reduce staging non valida" >&2; exit 69; }
validate_stage_file "$DNSMASQ_STAGE" || { echo "dnsmasq staging non valida" >&2; exit 69; }
# Snapshot the caller-owned inputs into a root-only directory before parsing.
# The final install is always performed from this immutable snapshot, so a
# caller cannot race validation by swapping the original /tmp files.
install -d -o root -g root -m 0700 /var/lib/BastionGuard
ROOT_STAGE=$(mktemp -d /var/lib/BastionGuard/phishing-stage.XXXXXX)
trap 'rm -rf -- "$ROOT_STAGE"' EXIT
install -m 0600 -- "$BLACKLIST" "$ROOT_STAGE/blacklist.txt"
install -m 0600 -- "$BLACKLIST_REDUCE" "$ROOT_STAGE/blacklist-reduce.txt"
install -m 0600 -- "$DNSMASQ_STAGE" "$ROOT_STAGE/dnsmasq-blacklist.conf"
BLACKLIST="$ROOT_STAGE/blacklist.txt"
BLACKLIST_REDUCE="$ROOT_STAGE/blacklist-reduce.txt"
DNSMASQ_STAGE="$ROOT_STAGE/dnsmasq-blacklist.conf"
validate_blacklist() {
local f="$1" line
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" || "$line" == \#* ]] && continue
[[ ${#line} -le 4096 ]] || return 1
# Backend.cpp emits URLs, domains or IPs only. Reject control/whitespace
# and arbitrary file content before it can become world-readable.
if [[ "$line" =~ ^https?://[^[:space:][:cntrl:]]+$ ]]; then
continue
fi
if [[ "$line" =~ ^[A-Za-z0-9._:-]+$ && "$line" == *.* ]]; then
continue
fi
return 1
done < "$f"
}
validate_blacklist "$BLACKLIST" || { echo "blacklist contenuto non valido" >&2; exit 70; }
validate_blacklist "$BLACKLIST_REDUCE" || { echo "blacklist-reduce contenuto non valido" >&2; exit 70; }
# Only the directive generated by Backend.cpp is accepted. This blocks an
# attacker from smuggling dhcp-script= or any other executable dnsmasq option.
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" ]] && continue
if [[ ! "$line" =~ ^address=/[A-Za-z0-9.-]+/127\.0\.0\.2$ ]]; then
echo "BastionGuard-phishing-updater: direttiva dnsmasq rifiutata" >&2
exit 71
fi
done < "$DNSMASQ_STAGE"
install -m 0644 -- "$BLACKLIST" "$PHISH_SYS/blacklist.txt"
install -m 0644 -- "$BLACKLIST_REDUCE" "$PHISH_SYS/blacklist-reduce.txt"
echo "[$DATE] blacklist installate" >> "$LOG_FILE"
# Copy the caller's whitelist only if it is a regular, caller-owned file.
REAL_HOME=$(getent passwd "$CALLER_UID" | cut -d: -f6)
if [[ -n "$REAL_HOME" ]]; then
WL_TXT="$REAL_HOME/.local/share/BastionGuard/phishing/whitelist.txt"
if [[ -f "$WL_TXT" && ! -L "$WL_TXT" && $(stat -c '%u' -- "$WL_TXT") == "$CALLER_UID" ]]; then
install -m 0600 -- "$WL_TXT" "$ROOT_STAGE/whitelist.txt"
WL_OK=1
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" || "$line" == \#* ]] && continue
if [[ ! "$line" =~ ^[A-Za-z0-9.-]+$ ]]; then WL_OK=0; break; fi
done < "$ROOT_STAGE/whitelist.txt"
if [[ "$WL_OK" -eq 1 ]]; then
install -m 0644 -- "$ROOT_STAGE/whitelist.txt" "$PHISH_SYS/whitelist.txt"
echo "[$DATE] whitelist utente copiata in sys" >> "$LOG_FILE"
fi
fi
fi
# 3. Installa config dnsmasq se preparata dal backend
install -d -o root -g root -m 0755 /etc/dnsmasq.d
install -m 0644 -- "$DNSMASQ_STAGE" "$DNSMASQ_CONF"
DNSMASQ_OK=0
if [[ -f "/tmp/BastionGuard-dnsmasq-blacklist.conf" ]]; then
install -m 0644 /tmp/BastionGuard-dnsmasq-blacklist.conf \
"$DNSMASQ_CONF"
echo "[$DATE] dnsmasq conf installata" >> "$LOG_FILE"
if dnsmasq --test > /tmp/BastionGuard-dnsmasq.log 2>&1; then
DNSMASQ_OK=1
else
echo "[$DATE] ⚠ dnsmasq --test fallito, skip reload" >> "$LOG_FILE"
fi
if dnsmasq --test > "$LOG_DIR/dnsmasq-test.log" 2>&1; then
DNSMASQ_OK=1
else
echo "[$DATE] dnsmasq --test fallito, skip reload" >> "$LOG_FILE"
fi
# 4. Riavvia dnsmasq e BastionGuard-phishing-scanner in parallelo
SCANNER_ACTIVE=0
systemctl is-active --quiet BastionGuard-phishing-scanner.service && SCANNER_ACTIVE=1
systemctl is-active --quiet BastionGuard-phishing-scanner.service && SCANNER_ACTIVE=1 || true
if [[ "$DNSMASQ_OK" -eq 1 || "$SCANNER_ACTIVE" -eq 1 ]]; then
[[ "$DNSMASQ_OK" -eq 1 ]] && systemctl restart dnsmasq 2>/dev/null &
[[ "$SCANNER_ACTIVE" -eq 1 ]] && systemctl restart BastionGuard-phishing-scanner.service 2>/dev/null &
wait
[[ "$DNSMASQ_OK" -eq 1 ]] && echo "[$DATE] dnsmasq ricaricato" >> "$LOG_FILE"
[[ "$SCANNER_ACTIVE" -eq 1 ]] && echo "[$DATE] Scanner riavviato" >> "$LOG_FILE"
if [[ "$DNSMASQ_OK" -eq 1 ]]; then
systemctl restart dnsmasq
echo "[$DATE] dnsmasq ricaricato" >> "$LOG_FILE"
fi
if [[ "$SCANNER_ACTIVE" -eq 1 ]]; then
systemctl restart BastionGuard-phishing-scanner.service
echo "[$DATE] Scanner riavviato" >> "$LOG_FILE"
fi
echo "[$DATE] Aggiornamento completato" >> "$LOG_FILE"

0
data/scripts/BastionGuard-restart-user-services.sh Executable file → Normal file
View file

0
data/scripts/BastionGuard-setup-clamav-daemon.sh Executable file → Normal file
View file

0
data/scripts/bank-helper Executable file → Normal file
View file

0
data/scripts/camera-privacy-toggle.sh Executable file → Normal file
View file

0
data/scripts/enable-user-agents.sh Executable file → Normal file
View file

View file

@ -1,2 +1,5 @@
# BastionGuard — aggiornamento phishing blacklist senza password
ALL ALL=(root) NOPASSWD: /usr/share/BastionGuard/data/scripts/BastionGuard-phishing-updater.sh
# BastionGuard phishing updater
#
# Intentionally no NOPASSWD rule. The updater is invoked through pkexec and
# requires an administrator authorization. Keeping this file installed ensures
# upgrades replace the insecure v2.0.3 rule on existing systems.

View file

@ -1,2 +1,2 @@
version=2.0.3
build=20260803
version=2.0.4
build=20261013

View file

@ -0,0 +1,318 @@
/*
GenieLocker ransomware detection rules for BastionGuard / YARA 4.x
Purpose:
- Detect known GenieLocker Windows, Linux, and VMware ESXi samples.
- Detect probable new variants using high-specificity static indicators.
- Detect the GenieLocker VCJOURN auxiliary journal artifact.
Response model:
BastionGuard reports the match to the user. The user remains responsible
for deciding whether to block, quarantine, allow, or investigate the file.
Intelligence sources:
- Kaspersky Securelist, "Toy Ghouls' new toy: the GenieLocker ransomware",
published 2026-07-30.
- Tom's Hardware Italia, "GenieLocker si evolve: Windows, Linux ed ESXi
sono nel mirino", published 2026-07-30.
Notes:
- Exact-hash rules have critical severity and high confidence.
- Heuristic rules should be reviewed in context before quarantine.
- MD5 is used only because the public IOC list was published as MD5.
- This file contains no automatic response or hidden enforcement logic.
*/
import "hash"
/* -------------------------------------------------------------------------
PRIVATE HELPERS: EXACT PUBLISHED SAMPLE HASHES
------------------------------------------------------------------------- */
private rule GenieLocker_Known_Windows_MD5
{
condition:
filesize > 0 and filesize <= 100MB and
(
hash.md5(0, filesize) == "a50eaaf514f4f84e61ca2455a8789753" or
hash.md5(0, filesize) == "f08f476f26b01d142ca73923de65fc0c" or
hash.md5(0, filesize) == "fd46a80c2f45577263328984edf7f4dc" or
hash.md5(0, filesize) == "de3cfbb50f66079bfee20a6f64e59433" or
hash.md5(0, filesize) == "780c8f4c6f077da4da96582987920362" or
hash.md5(0, filesize) == "d87d0b01d95acc936b7dc47b8f41937a" or
hash.md5(0, filesize) == "34a7f28e0bb69b0d49bacc88bdf20ac1" or
hash.md5(0, filesize) == "5d62c1349b8981c396c9a23f4f8f053c" or
hash.md5(0, filesize) == "a8842616c9057d5cf6e1fe1fa8c3c160" or
hash.md5(0, filesize) == "34b8828635f88078735799a3c1ac8e28" or
hash.md5(0, filesize) == "d3e06eb34d8eee7ef92cac3ad0a20ff5" or
hash.md5(0, filesize) == "c68b6862725777651085650db34947fc" or
hash.md5(0, filesize) == "9cd514ff2809ce0b993e3b8649e82a94" or
hash.md5(0, filesize) == "824ca1e906cc073ee5b0f3519df69a8f" or
hash.md5(0, filesize) == "25480dad40152ef3d0c6d38eecc9bd9b" or
hash.md5(0, filesize) == "7dad78584795aa5c160520cc6accf260" or
hash.md5(0, filesize) == "18f61c6d686cffd131c9fd3f3437064b" or
hash.md5(0, filesize) == "9969a8221312dba70dd5cbddf83a146c" or
hash.md5(0, filesize) == "f7b9e36e94163a9a303160945f99267a" or
hash.md5(0, filesize) == "b893eafed0659f70d4ac250f09073723" or
hash.md5(0, filesize) == "d661cf666b9acbab7cfeae1127a261a9" or
hash.md5(0, filesize) == "3a4479b51890373bfc4a011ef41fe376" or
hash.md5(0, filesize) == "58c0dda52b8f069660166d61fd74f911"
)
}
private rule GenieLocker_Known_Linux_ESXi_MD5
{
condition:
filesize > 0 and filesize <= 100MB and
hash.md5(0, filesize) == "9201e35e2993612612919a3c71302cab"
}
/* -------------------------------------------------------------------------
EXACT IOC RULES
------------------------------------------------------------------------- */
rule GenieLocker_Known_Windows_Sample : ransomware genielocker windows exact_ioc
{
meta:
author = "Calogero Scarnà"
created = "2026-08-01"
modified = "2026-08-01"
description = "Detects published GenieLocker Windows samples by exact MD5"
family = "GenieLocker"
threat_actor = "Toy Ghouls"
platform = "Windows"
severity = "critical"
confidence = "high"
response_mode = "user_managed"
recommendation = "Review alert and block or quarantine if the hash match is confirmed"
source = "Kaspersky Securelist, 2026-07-30"
reference = "securelist.com/genielocker-ransomware-for-windows-linux-and-esxi/120843/"
condition:
GenieLocker_Known_Windows_MD5
}
rule GenieLocker_Known_Linux_ESXi_Sample : ransomware genielocker linux esxi exact_ioc
{
meta:
author = "Calogero Scarnà"
created = "2026-08-01"
modified = "2026-08-01"
description = "Detects the published GenieLocker Linux and VMware ESXi sample by exact MD5"
family = "GenieLocker"
threat_actor = "Toy Ghouls"
platform = "Linux/VMware ESXi"
severity = "critical"
confidence = "high"
response_mode = "user_managed"
recommendation = "Review alert and block or quarantine if the hash match is confirmed"
source = "Kaspersky Securelist, 2026-07-30"
reference = "securelist.com/genielocker-ransomware-for-windows-linux-and-esxi/120843/"
condition:
GenieLocker_Known_Linux_ESXi_MD5
}
/* -------------------------------------------------------------------------
WINDOWS PE HEURISTIC
The condition requires a PE file plus combinations of GenieLocker-specific
journal/extension strings, unusually long exclusion/termination lists,
command-line options, and anti-debug/service-control APIs.
------------------------------------------------------------------------- */
rule GenieLocker_Windows_PE_Heuristic : ransomware genielocker windows heuristic
{
meta:
author = "Calogero Scarnà"
created = "2026-08-01"
modified = "2026-08-01"
description = "Detects probable GenieLocker Windows PE variants using high-specificity static indicators"
family = "GenieLocker"
threat_actor = "Toy Ghouls"
platform = "Windows"
severity = "high"
confidence = "medium-high"
response_mode = "user_managed"
recommendation = "Inspect path, signer, parent process, hash, and matched strings before quarantine"
source = "Kaspersky Securelist technical analysis, 2026-07-30"
reference = "securelist.com/genielocker-ransomware-for-windows-linux-and-esxi/120843/"
strings:
/* File-encryption artifacts */
$journal_magic = "VCJOURN" ascii wide
$sample_extension = ".03ffc1c4a3da0f02" ascii wide
/* Highly characteristic exclusion and target lists */
$folder_exclusions_1 = "$recycle.bin;config.msi;$windows.~bt;$windows.~ws;windows;boot;program files;program files (x86);programdata" ascii wide nocase
$folder_exclusions_2 = "system volume information;tor browser;windows.old;intel;msocache;perflogs;x64dbg;public;all users;default;microsoft;appdata" ascii wide nocase
$file_exclusions = "autorun.inf;boot.ini;bootfont.bin;bootsect.bak;desktop.ini;iconcache.db;ntldr;ntuser.dat" ascii wide nocase
$process_targets_1 = "sql;oracle;ocssd;dbsnmp;synctime;agntsvc;isqlplussvc;xfssvccon;mydesktopservice" ascii wide nocase
$process_targets_2 = "excel;infopath;msaccess;mspub;onenote;outlook;powerpnt;steam;thebat;thunderbird;visio;winword" ascii wide nocase
$process_targets_3 = "vmwp;vmms;vmcompute;mssqlserver" ascii wide nocase
$service_targets_1 = "vss;sql;svc$;memtas;mepocs;msexchange;sophos;veeam;backup" ascii wide nocase
$service_targets_2 = "GxVss;GxBlr;GxFWD;GxCVD;GxCIMgr" ascii wide nocase
$service_targets_3 = "agent_ovpnconnect" ascii wide nocase
/* Command-line interface */
$option_percent_long = "--percent" ascii wide
$option_recursive_long = "--recursive" ascii wide
$option_log_long = "--log" ascii wide
$option_percent_short = "-p" ascii wide fullword
$option_recursive_short = "-r" ascii wide fullword
$option_log_short = "-l" ascii wide fullword
/* Anti-analysis and process/service manipulation APIs */
$api_debug_local = "IsDebuggerPresent" ascii
$api_debug_remote = "CheckRemoteDebuggerPresent" ascii
$api_hostname = "GetComputerNameA" ascii
$api_terminate = "TerminateProcess" ascii
$api_control_service = "ControlService" ascii
$api_current_pid = "GetCurrentProcessId" ascii
condition:
uint16(0) == 0x5A4D and
filesize >= 20KB and filesize <= 100MB and
(
/* Strongest known combination */
(
$journal_magic and
$sample_extension and
2 of ($api_*)
)
or
/* Variant tolerant: multiple unique lists plus behavior APIs */
(
3 of ($folder_exclusions_*, $file_exclusions, $process_targets_*, $service_targets_*) and
3 of ($api_*) and
1 of ($option_*)
)
or
/* Journal marker, target lists, and CLI together */
(
$journal_magic and
2 of ($process_targets_*, $service_targets_*) and
2 of ($option_*) and
2 of ($api_*)
)
)
}
/* -------------------------------------------------------------------------
LINUX / VMWARE ESXi ELF HEURISTIC
The condition requires an ELF file and combinations of ESXi-specific paths,
GenieLocker command-line help text, journal data, and libsodium symbols.
------------------------------------------------------------------------- */
rule GenieLocker_Linux_ESXi_ELF_Heuristic : ransomware genielocker linux esxi heuristic
{
meta:
author = "Calogero Scarnà"
created = "2026-08-01"
modified = "2026-08-01"
description = "Detects probable GenieLocker Linux or VMware ESXi ELF variants"
family = "GenieLocker"
threat_actor = "Toy Ghouls"
platform = "Linux/VMware ESXi"
severity = "high"
confidence = "medium-high"
response_mode = "user_managed"
recommendation = "Inspect execution context, target path, hash, and matched strings before quarantine"
source = "Kaspersky Securelist technical analysis, 2026-07-30"
reference = "securelist.com/genielocker-ransomware-for-windows-linux-and-esxi/120843/"
strings:
/* ESXi-specific paths */
$esxi_volume_path = "/vmfs/volumes" ascii
$esxi_welcome_path = "/etc/vmware/welcome" ascii
/* File-encryption artifact */
$journal_magic = "VCJOURN" ascii
/* Help/argument strings reported for the ELF build */
$help_percent = "-p <perc>" ascii
$help_workers = "-j <workers>" ascii
$help_recursive = "-r <dir>" ascii
$help_delay = "-w <sec>" ascii
$help_daemon = "-d" ascii fullword
$help_log = "-l <logfile>" ascii
/* Common argument descriptions; useful when symbols are stripped */
$text_percentage = "Percentage of file content to encrypt" ascii nocase
$text_workers = "Number of encryption threads" ascii nocase
$text_delay = "Delay before start" ascii nocase
$text_logfile = "Path to log file" ascii nocase
/* Potentially retained static libsodium symbol names */
$sodium_xchacha_encrypt = "crypto_aead_xchacha20poly1305_ietf_encrypt" ascii
$sodium_xchacha_decrypt = "crypto_aead_xchacha20poly1305_ietf_decrypt" ascii
$sodium_box_easy = "crypto_box_easy" ascii
$sodium_box_seal = "crypto_box_seal" ascii
$sodium_init = "sodium_init" ascii
condition:
uint32(0) == 0x464C457F and
filesize >= 20KB and filesize <= 100MB and
(
/* ESXi paths and multiple specific CLI strings */
(
$esxi_volume_path and
$esxi_welcome_path and
3 of ($help_*)
)
or
/* ESXi target path plus journal and CLI */
(
$esxi_volume_path and
$journal_magic and
3 of ($help_*)
)
or
/* ESXi paths plus cryptographic implementation evidence */
(
$esxi_volume_path and
$esxi_welcome_path and
2 of ($sodium_*)
)
or
/* Tolerates modified help formatting */
(
$esxi_volume_path and
3 of ($text_*) and
2 of ($help_*, $sodium_*)
)
)
}
/* -------------------------------------------------------------------------
POST-IMPACT JOURNAL ARTIFACT
GenieLocker creates a small auxiliary journal containing the hardcoded
"VCJOURN" magic. This rule may identify an encryption artifact rather than
the executable itself, so investigate the affected directory immediately.
------------------------------------------------------------------------- */
rule GenieLocker_VCJOURN_Artifact : ransomware genielocker artifact journal
{
meta:
author = "Calogero Scarnà"
created = "2026-08-01"
modified = "2026-08-01"
description = "Detects a probable GenieLocker VCJOURN auxiliary encryption journal"
family = "GenieLocker"
platform = "Windows/Linux/VMware ESXi"
severity = "critical"
confidence = "high"
response_mode = "user_managed"
recommendation = "Treat as possible active encryption; inspect nearby files and running processes immediately"
source = "Kaspersky Securelist technical analysis, 2026-07-30"
reference = "securelist.com/genielocker-ransomware-for-windows-linux-and-esxi/120843/"
strings:
$journal_magic = "VCJOURN" ascii
condition:
filesize >= 16 and filesize <= 1MB and
$journal_magic at 0
}

View file

@ -672,8 +672,8 @@ target_include_directories(BastionGuard
)
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.3"
BASTIONGUARD_BUILD=20260803
BASTIONGUARD_VERSION="2.0.4"
BASTIONGUARD_BUILD=20261013
)
target_link_libraries(BastionGuard

4
debian/changelog vendored
View file

@ -1,5 +1,5 @@
bastionguard (2.0.3-3) stable; urgency=low
bastionguard (2.0.4-1) stable; urgency=low
* Debian package.
-- Calogero Scarnà <info@bastionguard.eu> Fri, 12 Jun 2026 12:00:00 +0100
-- Calogero Scarnà <info@bastionguard.eu> Tue, 13 Oct 2026 12:00:00 +0100

View file

@ -1,5 +1,5 @@
bastionguard (2.0.3-1ubuntu24.04.1) noble; urgency=low
bastionguard (2.0.4-1ubuntu24.04.1) noble; urgency=low
* Ubuntu 24.04 (Noble) package.
-- Calogero Scarnà <info@bastionguard.eu> Fri, 12 Jun 2026 12:00:00 +0100
-- Calogero Scarnà <info@bastionguard.eu> Tue, 13 Oct 2026 12:00:00 +0100

View file

@ -1,5 +1,5 @@
bastionguard (2.0.3-1ubuntu25.10) questing; urgency=low
bastionguard (2.0.4-1ubuntu25.10) questing; urgency=low
* Ubuntu 25.10 (Questing Quokka) package.
-- Calogero Scarnà <info@bastionguard.eu> Fri, 12 Jun 2026 12:00:00 +0100
-- Calogero Scarnà <info@bastionguard.eu> Tue, 13 Oct 2026 12:00:00 +0100

View file

@ -1,5 +1,5 @@
bastionguard (2.0.3-1ubuntu26.04) questing; urgency=low
bastionguard (2.0.4-1ubuntu26.04) questing; urgency=low
* Ubuntu 26.04 (Resolute Raccoon) package.
-- Calogero Scarnà <info@bastionguard.eu> Fri, 12 Jun 2026 12:00:00 +0100
-- Calogero Scarnà <info@bastionguard.eu> Tue, 13 Oct 2026 12:00:00 +0100

4
debian/control vendored
View file

@ -106,7 +106,9 @@ Depends:
curl,
yara,
libnss3,
libnss3-tools
libnss3-tools,
wireguard,
wireguard-tools
Description: Transparent security control plane for Linux desktops
BastionGuard is not a "trust us" security product.
It is a security control plane for Linux desktops where every protection

View file

@ -1,7 +1,7 @@
Source: bastionguard
Section: utils
Priority: optional
Maintainer: Caogero Scarnà <info@bastionguard.eu>
Maintainer: Calogero Scarnà <info@bastionguard.eu>
Build-Depends: debhelper-compat (= 13),
build-essential,
cmake,
@ -103,7 +103,9 @@ Depends: ${shlibs:Depends}, ${misc:Depends},
yara,
curl,
libnss3,
libnss3-tools
libnss3-tools,
wireguard,
wireguard-tools
Description: Transparent security control plane for Linux desktops
BastionGuard is not a "trust us" security product.
It is a security control plane for Linux desktops where every protection

View file

@ -1,7 +1,7 @@
Source: bastionguard
Section: utils
Priority: optional
Maintainer: Caogero Scarnà <info@bastionguard.eu>
Maintainer: Calogero Scarnà <info@bastionguard.eu>
Build-Depends: debhelper-compat (= 13),
build-essential,
cmake,
@ -105,7 +105,9 @@ Depends: ${shlibs:Depends}, ${misc:Depends},
yara,
curl,
libnss3,
libnss3-tools
libnss3-tools,
wireguard,
wireguard-tools
Description: Transparent security control plane for Linux desktops
BastionGuard is not a "trust us" security product.
It is a security control plane for Linux desktops where every protection

View file

@ -1,7 +1,7 @@
Source: bastionguard
Section: utils
Priority: optional
Maintainer: Caogero Scarnà <info@bastionguard.eu>
Maintainer: Calogero Scarnà <info@bastionguard.eu>
Build-Depends: debhelper-compat (= 13),
build-essential,
cmake,
@ -105,7 +105,9 @@ Depends: ${shlibs:Depends}, ${misc:Depends},
yara,
curl,
libnss3,
libnss3-tools
libnss3-tools,
wireguard,
wireguard-tools
Description: Transparent security control plane for Linux desktops
BastionGuard is not a "trust us" security product.
It is a security control plane for Linux desktops where every protection

View file

@ -19,7 +19,7 @@
#
Name: bastionguard
Version: 2.0.2
Version: 2.0.4
Release: 1%{?dist}
Summary: BastionGuard Security Platform
@ -141,6 +141,7 @@ Requires: curl
Requires: nss
Requires: nss-tools
Requires: sudo
Requires: wireguard-tools
%{?systemd_requires}
@ -178,7 +179,7 @@ cmake .. \
-DENABLE_CEF=ON \
-DENABLE_CEF_DAEMON=OFF \
-DENABLE_SYSTEM_CA_INSTALL=OFF \
-DBASTIONGUARD_INIT_SYSTEM=AUTO \
-DBASTIONGUARD_INIT_SYSTEM=SYSTEMD \
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
-DCMAKE_INSTALL_RPATH='$ORIGIN/../share/BastionGuard/lib;$ORIGIN/../share/BastionGuard/cef' \
-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=OFF
@ -241,6 +242,7 @@ fi
%{_datadir}/BastionGuard
%{_datadir}/bastionguard-backup
%{_datadir}/bastionguard-sc
%{_datadir}/bastionguard-rootguard
%{_datadir}/applications/BastionGuard.desktop
%{_datadir}/applications/bastionguard-sc.desktop
%{_datadir}/applications/bastionguard-backup-gtk.desktop
@ -261,6 +263,7 @@ fi
%{_datadir}/polkit-1/actions/org.BastionGuard.USBD.policy
%{_datadir}/polkit-1/actions/eu.bastionguard.sc.policy
%{_datadir}/polkit-1/actions/org.bastionguard.pkexec.backup.policy
%{_datadir}/polkit-1/actions/org.bastionguard.rootguard.policy
%{_unitdir}/*
%{_userunitdir}/*
@ -300,6 +303,15 @@ fi
%dir %{_sysconfdir}/bastionguard-secure-connectiond
%config(noreplace) %{_sysconfdir}/bastionguard-secure-connectiond/*
# ROOTGUARD
%config(noreplace) %{_sysconfdir}/bastionguard/rootguard.conf
%config(noreplace) %{_sysconfdir}/bastionguard/rootguard.conf.default
%{_includedir}/rootguard/
%{_libdir}/libbastionguard-rootguard-gtk.a
%{_datadir}/doc/bastionguard-rootguard/
%changelog
* Fri Jun 12 2026 Calogero Scarnà <info@bastionguard.eu> - 2.0
* Tue Oct 13 2026 Calogero Scarnà <info@bastionguard.eu> - 2.0.4
- Update package

View file

@ -736,8 +736,8 @@ target_include_directories(BastionGuard
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.3"
BASTIONGUARD_BUILD=20260803
BASTIONGUARD_VERSION="2.0.4"
BASTIONGUARD_BUILD=20261013
)
target_link_libraries(BastionGuard

View file

@ -19,7 +19,7 @@
#
Name: bastionguard
Version: 2.0.3
Version: 2.0.4
Release: %mkrel 1
%global yara_version 4.5.5
%global yara_stage %{_builddir}/%{name}-%{version}/.yara-stage
@ -168,6 +168,7 @@ Requires: curl
Requires: nss
Requires: nss-tools
Requires: sudo
Requires: wireguard-tools
%description
Transparent security control plane for Linux desktops.
@ -475,5 +476,5 @@ fi
%{_datadir}/doc/bastionguard-rootguard/
%changelog
* Thu Aug 27 2026 Calogero Scarnà <info@bastionguard.eu> - 2.0.3
* Tue Oct 13 2026 Calogero Scarnà <info@bastionguard.eu> - 2.0.4
- Update package

View file

@ -757,8 +757,8 @@ target_include_directories(BastionGuard
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.3"
BASTIONGUARD_BUILD=20260803
BASTIONGUARD_VERSION="2.0.4"
BASTIONGUARD_BUILD=20261013
)
target_link_libraries(BastionGuard

View file

@ -20,7 +20,7 @@
Name: bastionguard
Version: 2.0.3
Version: 2.0.4
Release: 1
%global yara_version 4.5.5
@ -187,6 +187,7 @@ Requires: curl
Requires: nss
Requires: nss-tools
Requires: sudo
Requires: wireguard-tools
%description
Transparent security control plane for Linux desktops.
@ -546,5 +547,5 @@ fi
%{_datadir}/doc/bastionguard-rootguard/
%changelog
* Fri Aug 27 2026 Calogero Scarnà <info@bastionguard.eu> - 2.0.3
* Tue Oct 13 2026 Calogero Scarnà <info@bastionguard.eu> - 2.0.4
- Update package

View file

@ -763,8 +763,8 @@ target_include_directories(BastionGuard
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.3"
BASTIONGUARD_BUILD=20260803
BASTIONGUARD_VERSION="2.0.4"
BASTIONGUARD_BUILD=20261013
)
target_link_libraries(BastionGuard

View file

@ -23,7 +23,7 @@
#
Name: bastionguard
Version: 2.0.3
Version: 2.0.4
Release: 1leap
Summary: BastionGuard Security Platform
License: GPLv3
@ -172,6 +172,7 @@ Requires: polkit
Requires: rsync
Requires: sudo
Requires: yara
Requires: wireguard-tools
# Provides the openSUSE web-service account used by the writable Web UI dirs.
Requires(pre): system-user-wwwrun
@ -351,5 +352,5 @@ fi
%{_datadir}/doc/bastionguard-rootguard/
%changelog
* Fri Aug 27 2026 Calogero Scarnà <info@bastionguard.eu> - 2.0.3
* Tue Oct 13 2026 Calogero Scarnà <info@bastionguard.eu> - 2.0.4
- Update package

View file

@ -719,8 +719,8 @@ target_include_directories(BastionGuard
)
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.3"
BASTIONGUARD_BUILD=20260803
BASTIONGUARD_VERSION="2.0.4"
BASTIONGUARD_BUILD=20261013
)
target_link_libraries(BastionGuard

View file

@ -23,7 +23,7 @@
#
Name: bastionguard
Version: 2.0.3
Version: 2.0.4
Release: 1
Summary: BastionGuard Security Platform
License: GPLv3
@ -166,7 +166,7 @@ Requires: curl
Requires: mozilla-nss
Requires: mozilla-nss-tools
Requires: sudo
Requires: wireguard-tools
%description
@ -313,5 +313,5 @@ fi
%{_datadir}/doc/bastionguard-rootguard/
%changelog
* Fri Aug 27 2026 Calogero Scarnà <info@bastionguard.eu> - 2.0.3
* Tue Oct 13 2026 Calogero Scarnà <info@bastionguard.eu> - 2.0.4
- Update package

View file

@ -719,8 +719,8 @@ target_include_directories(BastionGuard
)
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.3"
BASTIONGUARD_BUILD=20260803
BASTIONGUARD_VERSION="2.0.4"
BASTIONGUARD_BUILD=20261013
)
target_link_libraries(BastionGuard

View file

@ -21,7 +21,7 @@
# Maintainer: BastionGuard info@bastionguard.eu
pkgname=bastionguard
pkgver=2.0.3
pkgver=2.0.4
pkgrel=1
pkgdesc="BastionGuard - transparent security control plane for Linux desktops"
arch=('x86_64')

View file

@ -636,8 +636,8 @@ target_include_directories(BastionGuard
)
target_compile_definitions(BastionGuard PRIVATE
BASTIONGUARD_VERSION="2.0.3"
BASTIONGUARD_BUILD=20260803
BASTIONGUARD_VERSION="2.0.4"
BASTIONGUARD_BUILD=20261013
)
target_link_libraries(BastionGuard

View file

@ -20,6 +20,8 @@
#include "Resource.hpp"
#include "Backend.hpp"
#include "SafeProcess.hpp"
#include "DistroUtils.hpp"
#include "Utils.hpp"
#include "Quarantine.hpp"
#include "firewall/FirewallRuleManager.hpp"
@ -31,6 +33,7 @@
#include <cstdlib>
#include <cstdio>
#include <array>
#include <vector>
#include <memory>
#include <stdexcept>
#include <fstream>
@ -238,7 +241,7 @@ static std::string first_existing(const std::vector<std::string>& names)
bool Backend::isSambaScanRunning()
{
int ret = std::system("pgrep -f clamdscan >/dev/null 2>&1");
int ret = (bg_process::run({"pgrep", "-f", "clamdscan"}).exit_code);
return (ret == 0);
}
@ -303,11 +306,11 @@ std::pair<int,int> read_ports_from_conf()
}
static void restart_clamd() {
if (std::system("/usr/libexec/bastionguard/bastionguard-service --system is-active clamav-daemon.service") == 0) {
std::system("pkexec /usr/libexec/bastionguard/bastionguard-service --system restart clamav-daemon.service");
if ((bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--system", "is-active", "clamav-daemon.service"}).exit_code) == 0) {
(bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service", "--system", "restart", "clamav-daemon.service"}).exit_code);
std::cout << _("✔ Riavviato clamav-daemon\n");
} else if (std::system("/usr/libexec/bastionguard/bastionguard-service --system is-active clamd.service") == 0) {
std::system("pkexec /usr/libexec/bastionguard/bastionguard-service --system restart clamd.service");
} else if ((bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--system", "is-active", "clamd.service"}).exit_code) == 0) {
(bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service", "--system", "restart", "clamd.service"}).exit_code);
std::cout << _("✔ Riavviato clamd\n");
} else {
std::cerr << _("⚠ Nessun servizio clamd/clamav-daemon trovato\n");
@ -317,16 +320,13 @@ static void restart_clamd() {
static bool install_to_system_data(const std::string& src, const std::string& dest) {
try {
std::ostringstream cmd;
cmd << "pkexec /bin/sh -c \"mkdir -p '" << fs::path(dest).parent_path().string()
<< "' && cp -f '" << src << "' '" << dest << "' && chmod 0644 '" << dest << "'\"";
int ret = std::system(cmd.str().c_str());
if (ret != 0) {
std::cerr << _("⚠ install_to_system_data: pkexec failed (code ") << ret << ")\n";
// One fixed privileged executable, with src/dest as literal argv entries.
const auto installation = bg_process::run({"pkexec", "/usr/bin/install", "-D", "-m", "0644", src, dest});
if (!installation.ok()) {
std::cerr << _("⚠ install_to_system_data: pkexec failed (code ")
<< installation.exit_code << ")\n";
return false;
}
restart_clamd();
return true;
} catch (const std::exception& e) {
@ -442,8 +442,8 @@ static bool copy_to_final(const std::string& src, const std::string& dest) {
static std::string findClamonaccService() {
for (auto service : CLAMONACC_SERVICES) {
std::string cmd = "/usr/libexec/bastionguard/bastionguard-service --system is-active " + std::string(service);
if (std::system(cmd.c_str()) == 0) {
if (bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--system", "is-active", service}).ok()) {
return service;
}
}
@ -454,27 +454,22 @@ static std::string findClamonaccService() {
bool Backend::enableClamonacc() {
auto service = findClamonaccService();
std::string cmd = "/usr/libexec/bastionguard/bastionguard-service --system enable --now " + service;
return std::system(cmd.c_str()) == 0;
return bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--system", "enable", "--now", service}).ok();
}
bool Backend::disableClamonacc() {
auto service = findClamonaccService();
std::string cmd = "/usr/libexec/bastionguard/bastionguard-service --system disable --now " + service;
return std::system(cmd.c_str()) == 0;
return bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--system", "disable", "--now", service}).ok();
}
bool Backend::isClamonaccActive() {
for (auto service : CLAMONACC_SERVICES) {
FILE* pipe = popen(("/usr/libexec/bastionguard/bastionguard-service --system is-active " + std::string(service) + " 2>&1").c_str(), "r");
if (!pipe) continue;
char buffer[128];
std::string result;
if (fgets(buffer, sizeof(buffer), pipe)) {
result = buffer;
}
pclose(pipe);
const auto check = bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--system", "is-active", service});
if (!check.started) continue;
std::string result = check.output;
// Normalizza
result.erase(std::remove(result.begin(), result.end(), '\n'), result.end());
@ -510,14 +505,12 @@ std::string Backend::getQuarantinePath() {
std::vector<std::string> Backend::getClamonaccLogs() {
std::vector<std::string> lines;
for (auto service : CLAMONACC_SERVICES) {
FILE* pipe = popen(("/usr/libexec/bastionguard/bastionguard-service --system logs " + std::string(service) + " 100").c_str(), "r");
if (!pipe) continue;
char buffer[256];
while (fgets(buffer, sizeof(buffer), pipe)) {
lines.emplace_back(buffer);
}
pclose(pipe);
const auto logs = bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--system", "logs", service, "100"});
if (!logs.started) continue;
std::istringstream stream(logs.output);
std::string line;
while (std::getline(stream, line)) lines.push_back(line + "\n");
if (!lines.empty()) break;
}
@ -627,14 +620,12 @@ std::vector<std::string> Backend::getOnAccessPaths() {
std::vector<std::string> Backend::getOnAccessEvents() {
std::vector<std::string> events;
for (auto service : CLAMONACC_SERVICES) {
std::string cmd = "/usr/libexec/bastionguard/bastionguard-service --system logs " + std::string(service) + " 50";
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) continue;
char buffer[512];
while (fgets(buffer, sizeof(buffer), pipe)) {
std::string line = buffer;
const auto logs = bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--system", "logs", service, "50"});
if (!logs.started) continue;
std::istringstream stream(logs.output);
std::string line;
while (std::getline(stream, line)) {
if (line.find("Scanned file") != std::string::npos ||
line.find("FOUND") != std::string::npos ||
line.find("Access denied") != std::string::npos ||
@ -642,7 +633,6 @@ std::vector<std::string> Backend::getOnAccessEvents() {
events.push_back(line);
}
}
pclose(pipe);
if (!events.empty()) break;
}
@ -651,26 +641,22 @@ std::vector<std::string> Backend::getOnAccessEvents() {
static int runWithPexec(const std::string& cmd) {
std::string full = "pexec -c \"" + cmd + "\"";
return std::system(full.c_str());
// Install packages via an explicit allowlisted command; never pass an arbitrary
// command string to pexec or a privileged shell.
static int installPackages(const std::vector<std::string>& args) {
if (args.empty()) return 127;
std::vector<std::string> argv = {"pkexec"};
argv.insert(argv.end(), args.begin(), args.end());
return bg_process::run(argv, true).exit_code;
}
static std::string detectDistro() {
std::ifstream f("/etc/os-release");
if (!f.is_open()) return _("sconosciuta");
std::string line;
while (std::getline(f, line)) {
if (line.find("ID=") == 0) {
return line.substr(3);
}
}
return _("sconosciuta");
const std::string id = BastionGuard::Platform::distribution_id();
return id.empty() ? "unknown" : id;
}
static bool isClamdInstalled() {
return (std::system("command -v clamd >/dev/null 2>&1") == 0) ||
return ((bg_process::exists_on_path("clamd") ? 0 : 1) == 0) ||
(std::filesystem::exists("/usr/sbin/clamd") ||
std::filesystem::exists("/usr/bin/clamd"));
}
@ -686,19 +672,20 @@ bool Backend::installClamdIfMissing() {
int ret = -1;
if (distro == "debian" || distro == "ubuntu" || distro == "linuxmint") {
ret = runWithPexec("apt-get update && apt-get install -y clamav clamav-daemon");
ret = installPackages({"apt-get", "update"});
if (ret == 0) ret = installPackages({"apt-get", "install", "-y", "clamav", "clamav-daemon"});
} else if (distro == "fedora") {
ret = runWithPexec("dnf install -y clamav clamav-update");
ret = installPackages({"dnf", "install", "-y", "clamav", "clamav-update"});
} else if (distro == "centos" || distro == "rhel") {
ret = runWithPexec("yum install -y clamav clamav-update");
ret = installPackages({"yum", "install", "-y", "clamav", "clamav-update"});
} else if (distro == "opensuse" || distro == "suse") {
ret = runWithPexec("zypper install -y clamav");
} else if (distro == "arch" || distro == "manjaro") {
ret = runWithPexec("pacman -Sy --noconfirm clamav");
ret = installPackages({"zypper", "install", "-y", "clamav"});
} else if (distro == "arch" || distro == "manjaro" || distro == "endeavouros") {
ret = installPackages({"pacman", "-S", "--needed", "--noconfirm", "clamav"});
} else if (distro == "gentoo" || distro == "funtoo") {
ret = runWithPexec("emerge app-antivirus/clamav");
ret = installPackages({"emerge", "app-antivirus/clamav"});
} else if (distro == "slackware") {
ret = runWithPexec("slackpkg install clamav");
ret = installPackages({"slackpkg", "install", "clamav"});
} else {
std::cerr << _("❌ Distribuzione non riconosciuta: ") << distro << "\n";
return false;
@ -848,10 +835,9 @@ Backend& Backend::instance() {
void Backend::start_antiransom() {
std::string user = Glib::get_user_name();
std::string cmd = "runuser -l " + user +
" -c \"/usr/libexec/bastionguard/bastionguard-service --user start BastionGuard-ransomware-scanner.service\"";
int ret = std::system(cmd.c_str());
const int ret = bg_process::run({"runuser", "-u", Glib::get_user_name(), "--",
"/usr/libexec/bastionguard/bastionguard-service",
"--user", "start", "BastionGuard-ransomware-scanner.service"}).exit_code;
if (ret == 0)
std::cout << _("✔ Servizio Anti-Ransomware avviato\n");
else
@ -859,10 +845,9 @@ void Backend::start_antiransom() {
}
void Backend::stop_antiransom() {
std::string user = Glib::get_user_name();
std::string cmd = "runuser -l " + user +
" -c \"/usr/libexec/bastionguard/bastionguard-service --user stop BastionGuard-ransomware-scanner.service\"";
int ret = std::system(cmd.c_str());
const int ret = bg_process::run({"runuser", "-u", Glib::get_user_name(), "--",
"/usr/libexec/bastionguard/bastionguard-service",
"--user", "stop", "BastionGuard-ransomware-scanner.service"}).exit_code;
if (ret == 0)
std::cout << _("✔ Servizio Anti-Ransomware fermato\n");
else
@ -948,20 +933,16 @@ bool Backend::updateYaraRules(std::function<void(int, int, const std::string&)>
std::string url = baseUrl + "/" + file;
std::string dest = userYaraDir + "/" + file;
std::string cmd = "curl -sL --max-time 10 \"" + url + "\" -o \"" + dest + "\" 2>&1";
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) {
const auto download = bg_process::run({"curl", "--fail", "--silent", "--show-error",
"--location", "--max-time", "10", "-o", dest, "--", url}, true);
const std::string& output = download.output;
if (!download.ok()) {
std::error_code ec;
fs::remove(dest, ec);
failCount++;
continue;
}
std::string output;
char buffer[128];
while (fgets(buffer, sizeof(buffer), pipe)) {
output += buffer;
}
pclose(pipe);
if (fs::exists(dest)) {
auto size = fs::file_size(dest);
@ -993,17 +974,21 @@ bool Backend::updateYaraRules(std::function<void(int, int, const std::string&)>
if (successCount > 0) {
std::string cmd =
"pkexec sh -c \"mkdir -p '" + systemYaraDir +
"' && find '" + userYaraDir +
"' -maxdepth 1 -type f -name '*.yara' -exec cp -f {} '" + systemYaraDir + "/' \\;\"";
int ret = system(cmd.c_str());
if (ret == 0)
// Install each downloaded rule by argv, never interpolate file paths
// into a root shell script.
bool copied_all = true;
for (const auto& file : files) {
const fs::path src_path = fs::path(userYaraDir) / file;
if (!fs::exists(src_path)) continue;
const fs::path dst_path = fs::path(systemYaraDir) / file;
if (!bg_process::run({"pkexec", "/usr/bin/install", "-D", "-m", "0644",
src_path.string(), dst_path.string()}).ok())
copied_all = false;
}
if (copied_all)
std::cout << _("✔ Copiate anche in /usr/share/BastionGuard/data/yara") << "\n";
else
std::cerr << _("⚠ Impossibile copiare in /usr/share (permessi negati)") << "\n";
std::cerr << _("⚠ Alcune regole YARA non sono state installate nel sistema") << "\n";
}
return successCount > 0;
@ -1014,28 +999,38 @@ bool Backend::updateYaraRules(std::function<void(int, int, const std::string&)>
// ---------------------------------------------------------------------------
// Helper: esegue lo script privilegiato in modalità silenziosa (NOPASSWD)
// Helper: esegue lo script privilegiato dopo autorizzazione esplicita via pkexec
// Lo script si trova in /usr/share/BastionGuard/data/scripts/
// ---------------------------------------------------------------------------
static bool run_phishing_updater_script() {
static bool run_phishing_updater_script(const std::string& staging_dir) {
const std::string helper =
"/usr/share/BastionGuard/data/scripts/BastionGuard-phishing-updater.sh";
if (!std::filesystem::exists(helper)) {
std::cerr << _("[Phishing] Helper non trovato: ") << helper
<< _(", fallback a pkexec diretto\n");
std::cerr << _("[Phishing] Helper non trovato: ") << helper << "\n";
return false;
}
// sudo -n = non-interactive, richiede NOPASSWD in /etc/sudoers.d/bastionguard-phishing
std::string cmd = "sudo -n " + helper + " 2>/dev/null";
int ret = std::system(cmd.c_str());
if (ret == 0) return true;
// No shell and no passwordless sudo rule: pkexec performs the explicit
// administrator authorization, while the staging directory is passed as
// one opaque argv element.
const pid_t pid = ::fork();
if (pid < 0) {
std::cerr << _("[Phishing] fork() fallita per helper privilegiato.\n");
return false;
}
std::cerr << _("[Phishing] sudo -n fallito (ret=") << ret
<< _("), provo pkexec...\n");
ret = std::system(("pkexec " + helper).c_str());
return (ret == 0);
if (pid == 0) {
const char* argv[] = {
"pkexec", helper.c_str(), staging_dir.c_str(), nullptr
};
::execvp("pkexec", const_cast<char* const*>(argv));
_exit(127);
}
int status = 0;
if (::waitpid(pid, &status, 0) < 0) return false;
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
// ---------------------------------------------------------------------------
@ -1098,9 +1093,23 @@ bool Backend::updatePhishLists() {
std::string baseDir = userBase + "/data/phishing";
std::string finalPath = baseDir + "/blacklist.txt";
std::string finalReduce = baseDir + "/blacklist-reduce.txt";
std::string tmpPath = "/tmp/blacklist.txt";
std::string tmpReduce = "/tmp/blacklist-reduce.txt";
std::string tmpDnsmasq = "/tmp/BastionGuard-dnsmasq-blacklist.conf";
// Use a private per-update directory instead of predictable /tmp names.
// The privileged helper verifies owner/mode before consuming it.
std::string stage_template = "/tmp/bastionguard-phish.XXXXXX";
std::vector<char> stage_buf(stage_template.begin(), stage_template.end());
stage_buf.push_back('\0');
char* stage_ptr = ::mkdtemp(stage_buf.data());
if (!stage_ptr) {
std::cerr << _("[Phishing] Impossibile creare staging directory sicura.\n");
return false;
}
std::string stagingDir(stage_ptr);
::chmod(stagingDir.c_str(), 0700);
std::string tmpPath = stagingDir + "/blacklist.txt";
std::string tmpReduce = stagingDir + "/blacklist-reduce.txt";
std::string tmpDnsmasq = stagingDir + "/dnsmasq-blacklist.conf";
fs::create_directories(baseDir);
@ -1119,6 +1128,7 @@ bool Backend::updatePhishLists() {
cleanup.paths.push_back(tmpPath);
cleanup.paths.push_back(tmpReduce);
cleanup.paths.push_back(tmpDnsmasq);
cleanup.paths.push_back(stagingDir);
std::vector<std::string> sources = {
"https://phishing.army/download/phishing_army_blocklist.txt",
@ -1163,15 +1173,17 @@ bool Backend::updatePhishLists() {
}
for (const auto& url : sources) {
std::string cmd = "curl -sf --max-time 30 -A \"BastionGuard-PhishUpdater/1.0\" \"" + url + "\"";
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) { std::cerr << _("⚠ Impossibile aprire ") << url << "\n"; continue; }
const auto download = bg_process::run({"curl", "--fail", "--silent", "--max-time", "30",
"--user-agent", "BastionGuard-PhishUpdater/1.0",
"--", url}, false, 16U * 1024U * 1024U);
if (!download.ok()) { std::cerr << _("⚠ Impossibile aprire ") << url << "\n"; continue; }
char buffer[8192];
bool any = false;
std::string acc;
while (fgets(buffer, sizeof(buffer), pipe)) {
acc.append(buffer);
std::istringstream fetched(download.output);
std::string fragment;
while (std::getline(fetched, fragment)) {
acc.append(fragment).append("\n");
while (true) {
size_t nl = acc.find('\n');
if (nl == std::string::npos) break;
@ -1187,11 +1199,11 @@ bool Backend::updatePhishLists() {
}
}
}
pclose(pipe);
if (!any) std::cerr << _("⚠ Nessun dato da ") << url << "\n";
else out << "\n";
}
out.close();
::chmod(tmpPath.c_str(), 0600);
}
@ -1216,25 +1228,12 @@ bool Backend::updatePhishLists() {
fout << lines[i] << "\n";
fout << _("# ... lista completa salvata in blacklist.txt\n");
fout.close();
::chmod(tmpReduce.c_str(), 0600);
}
std::string systemDest = "/usr/share/BastionGuard/data/phishing/blacklist.txt";
std::string systemDestReduce = "/usr/share/BastionGuard/data/phishing/blacklist-reduce.txt";
copy_to_final(tmpPath, finalPath);
copy_to_final(tmpReduce, finalReduce);
try {
fs::copy_file(tmpPath,
"/tmp/bastionguard-blacklist.txt",
fs::copy_options::overwrite_existing);
fs::copy_file(tmpReduce,
"/tmp/bastionguard-blacklist-reduce.txt",
fs::copy_options::overwrite_existing);
} catch (const std::exception& ex) {
std::cerr << _("[Phishing] Errore copia tmp: ") << ex.what() << "\n";
}
try {
std::ifstream fin(finalPath);
if (!fin.is_open()) {
@ -1455,20 +1454,18 @@ bool Backend::updatePhishLists() {
}
fout.close();
::chmod(tmpDnsmasq.c_str(), 0600);
fin.close();
std::cout << _("✔ File dnsmasq generato: ") << written
<< _(" voci. Skipped whitelist: ") << skipped
<< _(" | Skipped IP: ") << skipped_ip << "\n";
bool script_ok = run_phishing_updater_script();
bool script_ok = run_phishing_updater_script(stagingDir);
if (!script_ok) {
install_to_system_data(tmpPath, systemDest);
install_to_system_data(tmpReduce, systemDestReduce);
install_to_system_data(tmpDnsmasq,
"/etc/dnsmasq.d/BastionGuard-blacklist.conf");
system("dnsmasq --test > /tmp/BastionGuard-dnsmasq.log 2>&1");
reload_dnsmasq();
std::cerr << _("[Phishing] Installazione privilegiata annullata o fallita; ")
<< _("nessun fallback insicuro viene eseguito.\n");
return false;
}
fs::remove(tmpDnsmasq);
}
@ -1522,7 +1519,7 @@ bool Backend::updatePhishLists() {
void Backend::start_antiphish() {
std::cout << _("▶ Avvio protezione Anti-Phishing...\n");
int ret = std::system("pkexec /usr/libexec/bastionguard/bastionguard-service --system start BastionGuard-phishing-scanner.service");
int ret = (bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service", "--system", "start", "BastionGuard-phishing-scanner.service"}).exit_code);
if (ret == 0) {
std::cout << _("✔ Servizio Anti-Phishing avviato\n");
} else {
@ -1532,7 +1529,7 @@ void Backend::start_antiphish() {
void Backend::stop_antiphish() {
std::cout << _("⏹ Arresto protezione Anti-Phishing...\n");
int ret = std::system("pkexec /usr/libexec/bastionguard/bastionguard-service --system stop BastionGuard-phishing-scanner.service");
int ret = (bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service", "--system", "stop", "BastionGuard-phishing-scanner.service"}).exit_code);
if (ret == 0) {
std::cout << _("✔ Servizio Anti-Phishing fermato\n");
} else {
@ -1578,8 +1575,9 @@ bool Backend::updateSanesecurityDB(std::function<void(int, int, const std::strin
std::cout << _("⬇ Scarico ") << db << " ...\n";
std::string cmd = "curl -sL \"" + url + "\" -o \"" + dest + "\"";
int ret = std::system(cmd.c_str());
const auto download = bg_process::run({"curl", "--fail", "--silent", "--location",
"--max-time", "30", "-o", dest, "--", url});
const int ret = download.exit_code;
if (ret != 0 || !fs::exists(dest) || fs::file_size(dest) < 100) {
std::cerr << _("⚠ Errore download o file vuoto: ") << db << "\n";
@ -1613,15 +1611,12 @@ bool Backend::updateSanesecurityDB(std::function<void(int, int, const std::strin
bool Backend::isAntiransomActive() {
std::string user = Glib::get_user_name();
std::string cmd = "runuser -l " + user +
" -c \"/usr/libexec/bastionguard/bastionguard-service --user is-active BastionGuard-ransomware-scanner.service\"";
int ret = std::system(cmd.c_str());
return (ret == 0);
return bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--user", "is-active", "BastionGuard-ransomware-scanner.service"}).ok();
}
bool Backend::isAntiphishActive() {
int ret = std::system("/usr/libexec/bastionguard/bastionguard-service --system is-active BastionGuard-phishing-scanner.service");
int ret = (bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--system", "is-active", "BastionGuard-phishing-scanner.service"}).exit_code);
return (ret == 0);
}
@ -1730,15 +1725,13 @@ bool Backend::testGoogleSafeKey() {
}
bool Backend::isPhishAutoUpdateEnabled() {
int ret = std::system("/usr/libexec/bastionguard/bastionguard-service --system is-enabled BastionGuard-phishing-updater.timer");
int ret = (bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--system", "is-enabled", "BastionGuard-phishing-updater.timer"}).exit_code);
return (ret == 0);
}
bool Backend::enablePhishAutoUpdate(bool enable) {
std::string cmd = enable
? "pkexec /usr/libexec/bastionguard/bastionguard-service --system enable --now BastionGuard-phishing-updater.timer"
: "pkexec /usr/libexec/bastionguard/bastionguard-service --system disable --now BastionGuard-phishing-updater.timer";
int ret = std::system(cmd.c_str());
int ret = bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service",
"--system", enable ? "enable" : "disable", "--now", "BastionGuard-phishing-updater.timer"}).exit_code;
if (ret == 0) {
if (enable) {
@ -2403,8 +2396,8 @@ void Backend::scanSambaDir(const std::string& dirInput)
}
if (std::system("pgrep -f clamd >/dev/null 2>&1") != 0 &&
std::system("pgrep -f clamav-daemon >/dev/null 2>&1") != 0)
if ((bg_process::run({"pgrep", "-f", "clamd"}).exit_code) != 0 &&
(bg_process::run({"pgrep", "-f", "clamav-daemon"}).exit_code) != 0)
{
push_log(_("ERRORE: il demone clamd non è attivo."));
samba_scan_state.scan_active = false;
@ -2517,30 +2510,14 @@ void Backend::scanSambaDir(const std::string& dirInput)
{
std::string chmodCmd = "chmod -R 755 \"" + tempDir.string() + "\"";
std::system(chmodCmd.c_str());
std::string cmd =
clamdscanPath + " --fdpass --no-summary --multiscan \"" +
tempDir.string() + "\" 2>&1";
push_log(_("Eseguo comando: ") + cmd);
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) {
push_log(_("ERRORE: popen() fallita."));
goto end_scan;
}
char buffer[2048];
// Paths are supplied as distinct argv entries and cannot alter command syntax.
push_log(_("Eseguo scansione ClamAV: ") + tempDir.string());
size_t scanned = 0;
auto scan = bg_process::run({clamdscanPath, "--fdpass", "--no-summary", "--multiscan",
"--", tempDir.string()}, true, 32U * 1024U * 1024U, {},
[&](const std::string& line) {
if (samba_stop_requested) return false;
while (fgets(buffer, sizeof(buffer), pipe))
{
if (samba_stop_requested) break;
std::string line(buffer);
push_log("ClamAV: " + line);
std::string filename = line.substr(0, line.find(':'));
@ -2562,9 +2539,9 @@ void Backend::scanSambaDir(const std::string& dirInput)
scanned++;
samba_last_progress = 0.30f + 0.70f * (float(scanned) / float(total));
}
pclose(pipe);
return true;
});
if (!scan.started || scan.truncated) push_log(_("ERRORE: scansione ClamAV non completata."));
std::this_thread::sleep_for(std::chrono::milliseconds(300));
}
@ -2826,9 +2803,17 @@ static size_t write_cb(void* c, size_t s, size_t n, std::string* o) {
bool Backend::updateBankListFromURL(const std::string& url) {
if (url.rfind("https://", 0) != 0) {
std::cerr << _("[Backend] ❌ La lista banche deve essere scaricata via HTTPS.\n");
return false;
}
std::string dir = Glib::get_home_dir() + "/.local/share/BastionGuard";
std::string path = dir + "/banks.json";
std::string tmp_path = path + ".download";
std::filesystem::create_directories(dir);
std::error_code cleanup_ec;
std::filesystem::remove(tmp_path, cleanup_ec);
CURL* curl = curl_easy_init();
if (!curl) {
@ -2836,9 +2821,9 @@ bool Backend::updateBankListFromURL(const std::string& url) {
return false;
}
std::ofstream outfile(path, std::ios::binary | std::ios::trunc);
std::ofstream outfile(tmp_path, std::ios::binary | std::ios::trunc);
if (!outfile.is_open()) {
std::cerr << _("[Backend] ❌ Errore apertura file ") << path << "\n";
std::cerr << _("[Backend] ❌ Errore apertura file temporaneo ") << tmp_path << "\n";
curl_easy_cleanup(curl);
return false;
}
@ -2850,8 +2835,10 @@ bool Backend::updateBankListFromURL(const std::string& url) {
curl_easy_setopt(curl, CURLOPT_USERAGENT, "BastionGuard/1.0");
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 25L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &outfile);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION,
@ -2872,11 +2859,44 @@ bool Backend::updateBankListFromURL(const std::string& url) {
if (res != CURLE_OK || response_code != 200) {
std::cerr << _("[Backend] ❌ Errore rete o HTTP (") << curl_easy_strerror(res) << ")\n";
std::error_code ec; std::filesystem::remove(tmp_path, ec);
return false;
}
if (!std::filesystem::exists(path) || std::filesystem::file_size(path) < 50) {
if (!std::filesystem::exists(tmp_path) || std::filesystem::file_size(tmp_path) < 50) {
std::cerr << _("[Backend] ⚠ banks.json scaricato ma vuoto.\n");
std::error_code ec; std::filesystem::remove(tmp_path, ec);
return false;
}
// Validate the trust-list structure before replacing the last known-good copy.
try {
std::ifstream check(tmp_path);
nlohmann::json j;
check >> j;
if (!j.is_object() || !j.contains("list") || !j["list"].is_array() || j["list"].empty()) {
std::cerr << _("[Backend] ❌ banks.json non ha una struttura valida.\n");
std::error_code ec; std::filesystem::remove(tmp_path, ec);
return false;
}
for (const auto& item : j["list"]) {
if (item.is_string()) continue;
if (item.is_object() && item.contains("value") && item["value"].is_string()) continue;
std::cerr << _("[Backend] ❌ banks.json contiene una voce non valida.\n");
std::error_code ec; std::filesystem::remove(tmp_path, ec);
return false;
}
} catch (...) {
std::cerr << _("[Backend] ❌ banks.json non è JSON valido.\n");
std::error_code ec; std::filesystem::remove(tmp_path, ec);
return false;
}
std::error_code replace_ec;
std::filesystem::rename(tmp_path, path, replace_ec);
if (replace_ec) {
std::cerr << _("[Backend] ❌ Impossibile sostituire banks.json: ") << replace_ec.message() << "\n";
std::filesystem::remove(tmp_path, cleanup_ec);
return false;
}
@ -2944,7 +2964,7 @@ bool Backend::isBankDomain(const std::string& host) const {
}
bool Backend::reload_dnsmasq() {
int ret = std::system("pkexec /usr/libexec/bastionguard/bastionguard-service --system reload dnsmasq.service");
int ret = (bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service", "--system", "reload", "dnsmasq.service"}).exit_code);
if (ret == 0) {
std::cout << _("✔ dnsmasq ricaricato correttamente.\n");
return true;

View file

@ -18,6 +18,7 @@
* The BastionGuard™ name and branding are not licensed under the GPL.
*/
#include "SafeProcess.hpp"
#include "BankPage.hpp"
#include "Backend.hpp"
#include <thread>
@ -98,7 +99,7 @@ static std::string iso_today_yyyy_mm_dd() {
}
static void restart_cef_service_user() {
int rc = std::system("/usr/libexec/bastionguard/bastionguard-service --user restart BastionGuard-cef.service >/dev/null 2>&1");
int rc = (bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user", "restart", "BastionGuard-cef.service"}).exit_code);
if (rc == 0)
std::cout << _("[BankPage] ✔ BastionGuard-cef.service riavviato\n");
else

View file

@ -20,6 +20,7 @@
#include "Resource.hpp"
#include "DashboardPage.hpp"
#include "SafeProcess.hpp"
#include "Backend.hpp"
#include <gdkmm/pixbuf.h>
#include <gdkmm/texture.h>
@ -73,8 +74,8 @@ static int compare_semver(const std::string& a, const std::string& b)
static bool check_service_active(const std::string& service_name) {
std::string cmd = "/usr/libexec/bastionguard/bastionguard-service --system is-active " + service_name;
return (std::system(cmd.c_str()) == 0);
return bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--system", "is-active", service_name}).ok();
}

View file

@ -18,6 +18,7 @@
* The BastionGuard™ name and branding are not licensed under the GPL.
*/
#include "SafeProcess.hpp"
#include "PrivacyPage.hpp"
#include "Resource.hpp"
#include <filesystem>
@ -334,13 +335,13 @@ PrivacyPage::~PrivacyPage() {
bool PrivacyPage::is_daemon_active() {
int rc = std::system("/usr/libexec/bastionguard/bastionguard-service --user is-active BastionGuard-privacyd.service");
int rc = (bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user", "is-active", "BastionGuard-privacyd.service"}).exit_code);
return (WIFEXITED(rc) && WEXITSTATUS(rc) == 0);
}
void PrivacyPage::start_privacy_daemon() {
write_log(_("Avvio automatico del servizio BastionGuard-privacyd..."));
std::system("/usr/libexec/bastionguard/bastionguard-service --user start BastionGuard-privacyd.service");
(bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user", "start", "BastionGuard-privacyd.service"}).exit_code);
}

143
src/SafeProcess.hpp Normal file
View file

@ -0,0 +1,143 @@
#pragma once
// Execute programs with an argv vector, never through a command interpreter.
// Output and exit status are bounded/normalized; callbacks can stop a scan.
#include <algorithm>
#include <cerrno>
#include <csignal>
#include <functional>
#include <map>
#include <cstdlib>
#include <sys/stat.h>
#include <string>
#include <vector>
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
extern char **environ;
namespace bg_process {
struct Result {
int exit_code = 127;
std::string output;
bool truncated = false;
bool started = false;
bool ok() const { return started && !truncated && exit_code == 0; }
};
// Resolve only executable names; do not interpret shell syntax or options.
inline bool exists_on_path(const std::string& name) {
if (name.empty() || name.find('/') != std::string::npos || name == "." || name == "..")
return false;
const char* path = std::getenv("PATH");
if (!path) return false;
const std::string dirs(path);
std::size_t start = 0;
do {
const auto end = dirs.find(':', start);
const std::string dir = dirs.substr(start, end == std::string::npos ? end : end - start);
// Empty PATH components mean current directory: not trustworthy for security checks.
if (!dir.empty()) {
const std::string file = dir + "/" + name;
struct stat st{};
if (::stat(file.c_str(), &st) == 0 && S_ISREG(st.st_mode) &&
::access(file.c_str(), X_OK) == 0) return true;
}
if (end == std::string::npos) break;
start = end + 1;
} while (true);
return false;
}
inline Result run(const std::vector<std::string>& args,
bool merge_stderr = false,
std::size_t limit = 8U * 1024U * 1024U,
const std::map<std::string, std::string>& environment = {},
const std::function<bool(const std::string&)>& on_line = {}) {
Result result;
if (args.empty() || args[0].empty()) return result;
for (const auto& arg : args) if (arg.find('\0') != std::string::npos) return result;
int fd[2];
if (::pipe(fd) != 0) return result;
posix_spawn_file_actions_t actions;
if (posix_spawn_file_actions_init(&actions) != 0) {
::close(fd[0]); ::close(fd[1]); return result;
}
posix_spawn_file_actions_addclose(&actions, fd[0]);
posix_spawn_file_actions_adddup2(&actions, fd[1], STDOUT_FILENO);
if (merge_stderr) posix_spawn_file_actions_adddup2(&actions, fd[1], STDERR_FILENO);
posix_spawn_file_actions_addclose(&actions, fd[1]);
std::vector<char*> argv;
for (const auto& arg : args) argv.push_back(const_cast<char*>(arg.c_str()));
argv.push_back(nullptr);
// Apply only explicitly supplied overrides, without invoking /usr/bin/env or shell.
std::vector<std::string> env_storage;
std::vector<char*> envp;
if (!environment.empty()) {
for (char** p = environ; p && *p; ++p) {
const std::string item(*p);
const auto pos = item.find('=');
if (pos != std::string::npos && environment.find(item.substr(0, pos)) == environment.end())
env_storage.push_back(item);
}
for (const auto& [key, value] : environment) {
if (key.empty() || key.find('=') != std::string::npos || key.find('\0') != std::string::npos || value.find('\0') != std::string::npos) {
posix_spawn_file_actions_destroy(&actions);
::close(fd[0]); ::close(fd[1]); return result;
}
env_storage.push_back(key + "=" + value);
}
for (auto& item : env_storage) envp.push_back(item.data());
envp.push_back(nullptr);
}
pid_t child = -1;
const int err = posix_spawnp(&child, args[0].c_str(), &actions, nullptr,
argv.data(), environment.empty() ? environ : envp.data());
posix_spawn_file_actions_destroy(&actions);
::close(fd[1]);
if (err != 0) { ::close(fd[0]); return result; }
result.started = true;
char buf[4096];
std::string pending;
std::size_t total = 0;
bool stopped = false;
for (;;) {
const ssize_t n = ::read(fd[0], buf, sizeof(buf));
if (n < 0 && errno == EINTR) continue;
if (n <= 0) break;
total += static_cast<std::size_t>(n);
if (total > limit) { result.truncated = true; stopped = true; break; }
if (on_line) {
pending.append(buf, static_cast<std::size_t>(n));
std::size_t p;
while ((p = pending.find('\n')) != std::string::npos) {
std::string line = pending.substr(0, p + 1);
pending.erase(0, p + 1);
if (!on_line(line)) { stopped = true; break; }
}
if (stopped) break;
// Guard against never-ending lines.
if (pending.size() > 65536) { result.truncated = true; stopped = true; break; }
} else {
result.output.append(buf, static_cast<std::size_t>(n));
}
}
if (on_line && !stopped && !pending.empty()) on_line(pending);
if (stopped) ::kill(child, SIGTERM);
::close(fd[0]);
int status = 0;
pid_t waited;
do { waited = ::waitpid(child, &status, 0); } while (waited == -1 && errno == EINTR);
if (waited == child) {
if (WIFEXITED(status)) result.exit_code = WEXITSTATUS(status);
else if (WIFSIGNALED(status)) result.exit_code = 128 + WTERMSIG(status);
}
if (stopped) result.truncated = true;
return result;
}
} // namespace bg_process

View file

@ -18,6 +18,7 @@
* The BastionGuard™ name and branding are not licensed under the GPL.
*/
#include "SafeProcess.hpp"
#include "SambaPage.hpp"
#include "Utils.hpp"
#include "Backend.hpp"
@ -35,7 +36,7 @@
static bool clamdscan_running()
{
return (std::system("pgrep -f clamdscan >/dev/null 2>&1") == 0);
return ((bg_process::run({"pgrep", "-f", "clamdscan"}).exit_code) == 0);
}
static bool samba_temp_exists()

View file

@ -22,8 +22,11 @@
#include <iostream>
#include <cstdlib>
#include <vector>
#include <filesystem>
#include <algorithm>
#include <system_error>
#include <glibmm/i18n.h>
#include <cstdlib>
#include "SafeProcess.hpp"
static bool reload_clamav_service() {
const std::vector<std::string> services = {
@ -33,13 +36,11 @@ static bool reload_clamav_service() {
};
for (const auto& svc : services) {
std::string cmd =
"/usr/libexec/bastionguard/bastionguard-service --system is-active " + svc +
" && /usr/libexec/bastionguard/bastionguard-service --system try-reload-or-restart " + svc;
if (std::system(cmd.c_str()) == 0) {
if (bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--system", "is-active", svc}).ok() &&
bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--system", "try-reload-or-restart", svc}).ok())
return true;
}
}
return false;
@ -49,47 +50,57 @@ bool SaneSecurityUpdater::runHeadless() {
const std::string tmpDir = "/var/lib/clamav/sanesecurity.tmp";
std::string prep =
"rm -rf " + tmpDir + " && mkdir -p " + tmpDir;
if (system(prep.c_str()) != 0) {
// Fixed working directory and argument-vector execution: no root shell.
// Keep deletion confined to this directory, never to /var/lib/clamav.
namespace fs = std::filesystem;
std::error_code ec;
fs::remove_all(tmpDir, ec);
if (ec || !fs::create_directories(tmpDir, ec) || ec) {
std::cerr << _("Errore: impossibile creare la cartella temporanea") << std::endl;
return false;
}
std::string cmd =
"rsync -av --delete "
"rsync://rsync.sanesecurity.net/sanesecurity "
+ tmpDir + "/";
int ret = system(cmd.c_str());
if (ret != 0) {
std::cerr << _("Errore durante rsync (codice ") << ret << ")" << std::endl;
system(("rm -rf " + tmpDir).c_str());
const auto sync = bg_process::run({"rsync", "-av", "--delete", "--",
"rsync://rsync.sanesecurity.net/sanesecurity", tmpDir + "/"}, true);
if (!sync.ok()) {
std::cerr << _("Errore durante rsync") << std::endl;
fs::remove_all(tmpDir, ec);
return false;
}
const std::vector<std::string> exts = {
"*.ndb", "*.hdb", "*.ldb", "*.yar", "*.yara",
"*.ign2", "*.txt"
// Never evaluate downloaded filenames as shell metacharacters.
// Copy only extensions that ClamAV/SaneSecurity expects.
const std::vector<std::string> extensions = {
".ndb", ".hdb", ".ldb", ".yar", ".yara", ".ign2", ".txt"
};
std::string installCmd = "sh -c '";
for (const auto& e : exts) {
installCmd +=
"cp -f " + tmpDir + "/" + e + " /var/lib/clamav/ 2>/dev/null; ";
bool copies_ok = true;
for (const auto& entry : fs::directory_iterator(tmpDir, ec)) {
if (ec) { copies_ok = false; break; }
// Never follow symlinks provided by an external rsync mirror.
if (entry.is_symlink(ec) || ec) {
copies_ok = false;
ec.clear();
continue;
}
if (!entry.is_regular_file(ec) || ec) {
ec.clear();
continue;
}
const auto extension = entry.path().extension().string();
if (std::find(extensions.begin(), extensions.end(), extension) == extensions.end()) continue;
const fs::path destination = fs::path("/var/lib/clamav") / entry.path().filename();
fs::copy_file(entry.path(), destination, fs::copy_options::overwrite_existing, ec);
if (ec) {
copies_ok = false;
ec.clear();
}
}
installCmd += "'";
if (system(installCmd.c_str()) != 0) {
std::cerr << _("Errore copiando i file SaneSecurity") << std::endl;
}
system(("rm -rf " + tmpDir).c_str());
fs::remove_all(tmpDir, ec);
if (!copies_ok) std::cerr << _("Errore copiando i file SaneSecurity") << std::endl;
if (!reload_clamav_service()) {
std::cerr << _("⚠ Impossibile ricaricare il servizio ClamAV: servizio non trovato") << "\n";
}
return true;
return copies_ok;
}

View file

@ -318,37 +318,20 @@ public:
const bool is_main_frame = frame && frame->IsMain();
static bool g_session_trusted = false;
static std::string g_entry_host;
const bool bank_ok = is_trusted_domain(host);
const bool pay_ok = is_payment_domain(host);
const bool https_url = url.rfind("https://", 0) == 0;
if (is_main_frame && (bank_ok || pay_ok)) {
g_session_trusted = true;
g_entry_host = host;
std::cout << _("[SecureBrowser] 🟢 Consentito (entry trusted): ") << host
<< (bank_ok ? " (bank)" : " (payments)") << "\n";
return false;
}
if (g_session_trusted) {
if (!is_main_frame) {
std::cout << _("[SecureBrowser] 🟢 Consentito (subframe in sessione trusted): ") << host << "\n";
// A previously authorized page never grants permission to
// navigate the main frame to a different, untrusted origin.
if (is_main_frame) {
if (https_url && (bank_ok || pay_ok)) {
std::cout << _("[SecureBrowser] 🟢 Navigazione attendibile: ") << host << "\n";
return false;
}
std::cout << _("[SecureBrowser] 🟢 Consentito (sessione trusted): ") << host << "\n";
return false;
}
if (bank_ok) {
std::cout << _("[SecureBrowser] 🟢 Consentito (bank domain): ") << host << "\n";
return false;
}
if (pay_ok) {
std::cout << _("[SecureBrowser] 🟢 Consentito (payment domain): ") << host << "\n";
} else if (https_url) {
// Third-party HTTPS subframes are needed by some bank
// sign-in/payment flows; main-frame trust is independent.
return false;
}
@ -367,16 +350,11 @@ public:
const std::string url = request_url;
const std::string host = host_from_url(url);
if (is_trusted_domain(host) || is_payment_domain(host)) {
std::cout << _("[SecureBrowser] ✅ Bypass cert error (") << cert_error
<< _(") per dominio trusted/payment: ") << host << "\n";
if (callback) callback->Continue();
return true;
}
std::cout << _("[SecureBrowser] ⛔ Cert error (") << cert_error
<< _(") per dominio NON trusted: ") << host << "\n";
<< _(") per dominio: ") << host << "\n";
// A trusted-domain allowlist must never override TLS
// certificate validation. Fail closed for every host.
if (browser && browser->GetMainFrame())
load_block_page_inline(browser, host);
if (callback) callback->Cancel();
@ -549,7 +527,17 @@ public:
CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override { return this; }
void OnBeforeCommandLineProcessing(const CefString&, CefRefPtr<CefCommandLine> cmd) override {
cmd->AppendSwitch("incognito");
// Il Secure Browser usa un profilo CEF dedicato e persistente. Non
// forzare --incognito nel processo bancario/payment: gli auth cookie
// di sessione devono poter sopravvivere alla chiusura della finestra,
// altrimenti ogni riapertura appare come un nuovo device/sessione ai
// sistemi SCA/3DS/anti-abuso. La finestra notify resta invece
// incognito perché BG_CEF_PERSIST_SESSION non viene impostato lì.
if (std::getenv("BG_CEF_PERSIST_SESSION"))
cmd->AppendSwitch("persist-session-cookies");
else
cmd->AppendSwitch("incognito");
cmd->AppendSwitch("disable-extensions");
cmd->AppendSwitch("disable-sync");
cmd->AppendSwitch("disable-component-update");
@ -690,6 +678,11 @@ bool SecureBrowser::open(const std::string& url, int argc, char** argv) {
std::cerr << _("[SecureBrowser] ⚠️ Creazione cache_dir fallita: ") << ec.message() << "\n";
}
// Profilo CEF dedicato persistente: non importiamo cookie dal browser
// esterno (Firefox/Chrome), ma manteniamo quelli creati dalla sessione
// sicura. Questo evita di mischiare token legati a fingerprint diversi.
setenv("BG_CEF_PERSIST_SESSION", "1", 1);
CefMainArgs args(argc, argv);
CefRefPtr<CefApp> app = createApp();
@ -706,6 +699,11 @@ bool SecureBrowser::open(const std::string& url, int argc, char** argv) {
CefString(&settings.locales_dir_path) = cef_dir + "/locales";
CefString(&settings.cache_path) = cache_dir;
// Il tuo SDK CEF espone persist_session_cookies (a differenza di
// persist_user_preferences, che nella build in uso non esiste).
// Con cache_path non vuoto conserva anche i cookie senza Expires/Max-Age.
settings.persist_session_cookies = true;
bool init_ok = CefInitialize(args, settings, app, nullptr);
if (!init_ok) {
std::cerr << _("[SecureBrowser] ❌ CefInitialize returned false for backend: ")

View file

@ -20,6 +20,7 @@
#include "Resource.hpp"
#include "SettingsPage.hpp"
#include "SafeProcess.hpp"
#include "DistroUtils.hpp"
#include "Utils.hpp"
#include "pac/PacManager.hpp"
@ -72,32 +73,9 @@ namespace {
std::string* out_stdout = nullptr,
bool silence_stderr = true)
{
try
{
Gio::Subprocess::Flags flags = Gio::Subprocess::Flags::NONE;
if (out_stdout)
flags |= Gio::Subprocess::Flags::STDOUT_PIPE;
if (silence_stderr)
flags |= Gio::Subprocess::Flags::STDERR_SILENCE;
else
flags |= Gio::Subprocess::Flags::STDERR_PIPE;
auto proc = Gio::Subprocess::create(argv, flags);
proc->wait();
if (out_stdout)
{
auto [stdout_str, stderr_str] = proc->communicate_utf8("");
*out_stdout = stdout_str.raw();
}
return proc->get_successful();
}
catch (...)
{
return false;
}
const auto result = bg_process::run(argv, !silence_stderr);
if (out_stdout) *out_stdout = result.output;
return result.ok();
}
static bool pkexec_servicectl(const std::vector<std::string>& args,
@ -1093,18 +1071,6 @@ static bool update_payments_enabled_only(bool enabled_flag) {
static bool run_cmd_capture(const std::string& cmd, std::string* out = nullptr, int* exit_code = nullptr) {
std::array<char, 256> buffer;
std::string result;
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) return false;
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) result += buffer.data();
int rc = pclose(pipe);
if (out) *out = result;
if (exit_code) *exit_code = rc;
return (rc == 0);
}
static std::string elev_cmd(const std::string& inner) {
if (std::filesystem::exists("/usr/bin/pkexec")) return std::string("pkexec ") + inner;
return std::string("sudo ") + inner;
@ -1128,21 +1094,11 @@ static bool write_temp_script_and_elevate(const std::string& script_contents, st
}
::chmod(tmpname.c_str(), 0755);
std::string runner = (std::filesystem::exists("/usr/bin/pkexec")) ? "pkexec " + tmpname : "sudo " + tmpname;
const char* display = std::getenv("DISPLAY");
const char* xauth = std::getenv("XAUTHORITY");
const char* dbus = std::getenv("DBUS_SESSION_BUS_ADDRESS");
std::string env_prefix;
if (display) env_prefix += std::string("DISPLAY=") + display + " ";
if (xauth) env_prefix += std::string("XAUTHORITY=") + xauth + " ";
if (dbus) env_prefix += std::string("DBUS_SESSION_BUS_ADDRESS=") + dbus + " ";
std::string fullcmd = "env " + env_prefix + runner;
std::string out;
int rc = 0;
bool ok = run_cmd_capture(fullcmd, &out, &rc);
const std::string runner = std::filesystem::exists("/usr/bin/pkexec") ? "pkexec" : "sudo";
const auto result = bg_process::run({runner, tmpname}, true);
std::string out = result.output;
const int rc = result.exit_code;
const bool ok = result.ok();
// pulizia
std::filesystem::remove(tmpname);
@ -1206,23 +1162,13 @@ bool copy_with_pkexec(const std::string &src, const std::string &dst)
return false;
}
std::ostringstream cmd;
cmd << "pkexec bash -c 'mkdir -p "
<< Glib::shell_quote(fs::path(dst).parent_path().string())
<< " && cp -f " << Glib::shell_quote(src)
<< " " << Glib::shell_quote(dst)
<< " && chmod 644 " << Glib::shell_quote(dst)
<< "'";
std::cout << "[copy_with_pkexec] " << cmd.str() << std::endl;
return (std::system(cmd.str().c_str()) == 0);
return bg_process::run({"pkexec", "/usr/bin/install", "-D", "-m", "0644", src, dst}).ok();
}
bool restart_service(const std::string &svc)
{
std::ostringstream cmd;
cmd << "pkexec /usr/libexec/bastionguard/bastionguard-service --system restart " << Glib::shell_quote(svc);
std::cout << "[restart_service] " << cmd.str() << std::endl;
return (std::system(cmd.str().c_str()) == 0);
return bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service",
"--system", "restart", svc}).ok();
}
bool update_ports_in_file(const std::string &file, int http_port, int https_port)
@ -1262,14 +1208,7 @@ bool update_ports_in_file(const std::string &file, int http_port, int https_port
fout << content;
fout.close();
std::ostringstream cmd;
cmd << "pkexec bash -c 'cp -f "
<< Glib::shell_quote(tmpfile) << " "
<< Glib::shell_quote(file)
<< " && chmod 644 " << Glib::shell_quote(file) << "'";
std::cout << _("[update_ports_in_file] ") << cmd.str() << std::endl;
return (std::system(cmd.str().c_str()) == 0);
return bg_process::run({"pkexec", "/usr/bin/install", "-m", "0644", tmpfile, file}).ok();
}
@ -1280,16 +1219,9 @@ std::string detect_distro()
std::string id;
if (std::system("command -v lsb_release >/dev/null 2>&1") == 0) {
std::array<char, 128> buffer{};
std::string result;
auto pipe_close = [](FILE* f) { if (f) pclose(f); };
std::unique_ptr<FILE, decltype(pipe_close)> pipe(popen("lsb_release -si 2>/dev/null", "r"), pipe_close);
if (pipe) {
while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr)
result += buffer.data();
}
{
const auto lsb = bg_process::run({"lsb_release", "-si"});
std::string result = lsb.ok() ? lsb.output : std::string{};
result.erase(std::remove(result.begin(), result.end(), '\n'), result.end());
result.erase(std::remove(result.begin(), result.end(), '\r'), result.end());
@ -1352,13 +1284,12 @@ static std::string detect_webserver() {
using namespace std;
auto is_active = [](const std::string& svc) -> bool {
std::string cmd = "/usr/libexec/bastionguard/bastionguard-service --system is-active " + svc;
return (std::system(cmd.c_str()) == 0);
return bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--system", "is-active", svc}).ok();
};
auto cmd_exists = [](const std::string& cmd) -> bool {
std::string full = "command -v " + cmd + " >/dev/null 2>&1";
return (std::system(full.c_str()) == 0);
return bg_process::run({"which", "--", cmd}).ok();
};
if (is_active("apache2") || is_active("httpd") || cmd_exists("apache2") || cmd_exists("httpd")) {
@ -1377,13 +1308,8 @@ static std::string detect_webserver() {
}
{
std::array<char, 256> buf;
std::string out;
FILE* p = popen("ps -eo comm | grep -E 'apache2|httpd|nginx|lshttpd|lsws' | grep -v grep", "r");
if (p) {
while (fgets(buf.data(), buf.size(), p)) out += buf.data();
pclose(p);
}
const auto processes = bg_process::run({"ps", "-eo", "comm"});
const std::string& out = processes.output;
if (out.find("apache2") != std::string::npos || out.find("httpd") != std::string::npos) return "apache";
if (out.find("nginx") != std::string::npos) return "nginx";
if (out.find("lshttpd") != std::string::npos || out.find("lsws") != std::string::npos) return "litespeed";
@ -1440,10 +1366,8 @@ bool update_webserver_ports(const std::string& websrv, int http_port, int https_
fout << http_port << " " << https_port << "\n";
fout.close();
std::string cmd =
"pkexec bash -c 'mkdir -p /etc/BastionGuard && install -m 0644 "
+ tmpfile + " " + targetfile + "'";
int ret = std::system(cmd.c_str());
const int ret = bg_process::run({"pkexec", "/usr/bin/install", "-D", "-m", "0644",
tmpfile, targetfile}).exit_code;
if (ret == 0) {
std::cout << _("[settings] ✅ Porte BastionGuard salvate in ") << targetfile
@ -1489,18 +1413,13 @@ std::string choose_clamd_conf_target() {
}
bool atomic_install_with_pkexec(const std::string& tmp_src, const std::string& target) {
std::string backup_cmd =
"if [ -f \"" + target + "\" ]; then "
" cp -a \"" + target + "\" \"" + target + ".bak.$(date +%s)\"; "
"fi; ";
std::string install_cmd =
"install -D -m 0644 \"" + tmp_src + "\" \"" + target + "\"";
std::string full = "pkexec sh -c '" + backup_cmd + install_cmd + "'";
int ret = std::system(full.c_str());
return (ret == 0);
if (fs::exists(target)) {
const std::string backup = target + ".bak." + std::to_string(std::time(nullptr));
if (!bg_process::run({"pkexec", "/usr/bin/cp", "-a", "--", target, backup}).ok())
return false;
}
return bg_process::run({"pkexec", "/usr/bin/install", "-D", "-m", "0644",
tmp_src, target}).ok();
}
} // namespace
@ -1508,13 +1427,8 @@ bool atomic_install_with_pkexec(const std::string& tmp_src, const std::string& t
namespace {
bool isServiceActive(const std::string& service, bool user = false) {
std::string cmd;
if (user) {
cmd = "/usr/libexec/bastionguard/bastionguard-service --user is-active " + service;
} else {
cmd = "/usr/libexec/bastionguard/bastionguard-service --system is-active " + service;
}
return (std::system(cmd.c_str()) == 0);
return bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
user ? "--user" : "--system", "is-active", service}).ok();
}
}
@ -1978,7 +1892,7 @@ void SettingsPage::onAntiransomToggled() {
if (antiransomSwitch.get_active()) {
if (!isServiceActive("BastionGuard-ransomware-scanner.service", true)) {
antiransomStatus.set_text(_("⏳ Avvio servizio Anti-Ransomware..."));
if (std::system("/usr/libexec/bastionguard/bastionguard-service --user start BastionGuard-ransomware-scanner.service") != 0) {
if ((bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user", "start", "BastionGuard-ransomware-scanner.service"}).exit_code) != 0) {
antiransomStatus.set_text(_("❌ Impossibile avviare il servizio Anti-Ransomware"));
antiransomSwitch.set_active(false);
return;
@ -1989,7 +1903,7 @@ void SettingsPage::onAntiransomToggled() {
} else {
if (isServiceActive("BastionGuard-ransomware-scanner.service", true)) {
antiransomStatus.set_text(_("⏳ Arresto servizio Anti-Ransomware..."));
if (std::system("/usr/libexec/bastionguard/bastionguard-service --user stop BastionGuard-ransomware-scanner.service") != 0) {
if ((bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user", "stop", "BastionGuard-ransomware-scanner.service"}).exit_code) != 0) {
antiransomStatus.set_text(_("❌ Impossibile fermare il servizio Anti-Ransomware"));
antiransomSwitch.set_active(true);
return;
@ -2166,8 +2080,8 @@ void SettingsPage::saveScannerConfig(bool use_rules, bool use_db,
f.close();
if (!use_local) {
std::string cmd = "pkexec cp \"" + tmp_path + "\" \"" + save_path + "\"";
if (std::system(cmd.c_str()) == 0)
if (bg_process::run({"pkexec", "/usr/bin/install", "-D", "-m", "0644",
tmp_path, save_path}).ok())
std::cout << _("✔ Config aggiornata in: ") << save_path << "\n";
else
std::cerr << _("❌ Errore: impossibile aggiornare config in ") << save_path << "\n";
@ -2332,7 +2246,7 @@ void SettingsPage::build_antiphish_tab() {
refreshPhishLists();
if (isServiceActive("BastionGuard-phishing-scanner.service")) {
antiphishStatus.set_text(_("♻ Riavvio servizio Anti-Phishing..."));
int r1 = std::system("pkexec /usr/libexec/bastionguard/bastionguard-service --system restart BastionGuard-phishing-scanner.service");
int r1 = (bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service", "--system", "restart", "BastionGuard-phishing-scanner.service"}).exit_code);
if (r1 == 0) {
antiphishStatus.set_text(_("✔ Servizio Anti-Phishing riavviato con nuova blacklist"));
} else {
@ -2482,7 +2396,7 @@ void SettingsPage::onAntiphishToggled() {
if (antiphishSwitch.get_active()) {
if (!isServiceActive("BastionGuard-phishing-scanner.service")) {
antiphishStatus.set_text(_("⏳ Avvio servizio Anti-Phishing..."));
if (std::system("pkexec /usr/libexec/bastionguard/bastionguard-service --system start BastionGuard-phishing-scanner.service") != 0) {
if ((bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service", "--system", "start", "BastionGuard-phishing-scanner.service"}).exit_code) != 0) {
antiphishStatus.set_text(_("❌ Impossibile avviare il servizio Anti-Phishing"));
antiphishSwitch.set_active(false);
return;
@ -2493,7 +2407,7 @@ void SettingsPage::onAntiphishToggled() {
} else {
if (isServiceActive("BastionGuard-phishing-scanner.service")) {
antiphishStatus.set_text(_("⏳ Arresto servizio Anti-Phishing..."));
if (std::system("pkexec /usr/libexec/bastionguard/bastionguard-service --system stop BastionGuard-phishing-scanner.service") != 0) {
if ((bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service", "--system", "stop", "BastionGuard-phishing-scanner.service"}).exit_code) != 0) {
antiphishStatus.set_text(_("❌ Impossibile fermare il servizio Anti-Phishing"));
antiphishSwitch.set_active(true);
return;
@ -2665,41 +2579,18 @@ void SettingsPage::build_options_tab() {
const char* ransomware_service = "BastionGuard-ransomware-scanner.service";
const char* alert_realtime_service = "BastionGuard-ransomware-realtime-alert.service";
auto run_cmd = [](const std::string& cmd) -> int {
return std::system(cmd.c_str());
const auto restart_user = [](const char* service) {
return bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--user", "restart", service}).ok();
};
int rc = 0;
rc = run_cmd(std::string("/usr/libexec/bastionguard/bastionguard-service --user restart ") + alert_service);
if (rc != 0) {
std::cerr << _("[BastionGuard] ⚠ Impossibile riavviare ") << alert_service
<< _(" tramite il gestore servizi utente") << std::endl;
}
rc = run_cmd(std::string("/usr/libexec/bastionguard/bastionguard-service --user restart ") + alert_realtime_service);
if (rc != 0) {
std::cerr << _("[BastionGuard] ⚠ Impossibile riavviare ") << alert_realtime_service
<< _(" tramite il gestore servizi utente") << std::endl;
}
rc = run_cmd(std::string("/usr/libexec/bastionguard/bastionguard-service --user restart ") + ransomware_service);
if (rc != 0) {
std::cerr << _("[BastionGuard] ⚠ Impossibile riavviare ") << ransomware_service
<< _(" tramite il gestore servizi utente") << std::endl;
}
rc = run_cmd(std::string("pkexec /usr/libexec/bastionguard/bastionguard-service --system restart ") + scanner_service);
if (rc != 0) {
std::string pattern = scanner_service;
int rc_pgrep = run_cmd("pgrep -f " + pattern + " >/dev/null 2>&1");
if (rc_pgrep == 0)
run_cmd("pkill -HUP -f " + pattern);
else
run_cmd("nohup " + pattern + " >/dev/null 2>&1 &");
for (const auto* unit : {alert_service, alert_realtime_service, ransomware_service}) {
if (!restart_user(unit))
std::cerr << _("[BastionGuard] Impossibile riavviare ") << unit << std::endl;
}
// Do not fall back to pkill -f or nohup with service-derived patterns.
if (!bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service",
"--system", "restart", scanner_service}).ok())
std::cerr << _("[BastionGuard] Riavvio servizio di sistema fallito") << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(800));
@ -2912,30 +2803,16 @@ void SettingsPage::build_options_tab() {
fclose(f);
chmod(tmpname.c_str(), 0755);
std::string env_prefix;
const char* display = std::getenv("DISPLAY");
const char* xauth = std::getenv("XAUTHORITY");
const char* dbus = std::getenv("DBUS_SESSION_BUS_ADDRESS");
if (display) env_prefix += "DISPLAY=" + std::string(display) + " ";
if (xauth) env_prefix += "XAUTHORITY=" + std::string(xauth) + " ";
if (dbus) env_prefix += "DBUS_SESSION_BUS_ADDRESS=" + std::string(dbus) + " ";
std::string elev = (std::filesystem::exists("/usr/bin/pkexec"))
? "env " + env_prefix + "pkexec " + tmpname
: "env " + env_prefix + "sudo " + tmpname;
std::cout << "[BastionGuard] → " << elev << std::endl;
std::array<char, 256> buffer;
std::string output;
FILE* pipe = popen(elev.c_str(), "r");
if (!pipe) return false;
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr)
output += buffer.data();
int rc = pclose(pipe);
const std::string runner = std::filesystem::exists("/usr/bin/pkexec") ? "pkexec" : "sudo";
std::map<std::string, std::string> env;
for (const char* key : {"DISPLAY", "XAUTHORITY", "DBUS_SESSION_BUS_ADDRESS"}) {
if (const char* val = std::getenv(key)) env.emplace(key, val);
}
const auto result = bg_process::run({runner, tmpname}, true, 8U * 1024U * 1024U, env);
const std::string& output = result.output;
std::filesystem::remove(tmpname);
if (out_log) *out_log = output;
return (rc == 0);
return result.ok();
};
auto replace_placeholders = [](const std::string& src, const std::string& dest,
@ -3385,16 +3262,18 @@ void SettingsPage::build_whitelist_tab() {
}
}
std::string cmd =
"pkexec sh -c '"
"install -D -m 0644 \"" + wlUserPath + "\" \"" + wlSysPath + "\" && "
"install -D -m 0644 \"" + blUserPath + "\" \"" + blSysPath + "\" && "
"install -D -m 0644 \"" + tmpDnsmasqConf + "\" \"" + dnsmasqConfPath + "\" && "
"/usr/libexec/bastionguard/bastionguard-service --system reload dnsmasq.service"
"'";
int rc = std::system(cmd.c_str());
(void)rc;
const bool saved =
bg_process::run({"pkexec", "/usr/bin/install", "-D", "-m", "0644", wlUserPath, wlSysPath}).ok() &&
bg_process::run({"pkexec", "/usr/bin/install", "-D", "-m", "0644", blUserPath, blSysPath}).ok() &&
bg_process::run({"pkexec", "/usr/bin/install", "-D", "-m", "0644", tmpDnsmasqConf, dnsmasqConfPath}).ok() &&
bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service",
"--system", "reload", "dnsmasq.service"}).ok();
if (!saved) {
auto dlg = Gtk::AlertDialog::create(_("Errore"));
dlg->set_detail(_("Impossibile installare i file o ricaricare dnsmasq."));
dlg->show();
return;
}
auto dlg = Gtk::AlertDialog::create(_("Salvataggio completato"));
dlg->set_detail(_("Whitelist/Blacklist salvate. Config dnsmasq aggiornata (127.0.0.2) e servizio ricaricato."));
@ -3403,7 +3282,7 @@ void SettingsPage::build_whitelist_tab() {
std::thread([=]() {
Backend::instance().updatePhishLists();
if (BastionGuard::Platform::cef_available()) {
std::system("/usr/libexec/bastionguard/bastionguard-service --user restart BastionGuard-cef");
(bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user", "restart", "BastionGuard-cef"}).exit_code);
}
}).detach();
});
@ -3794,7 +3673,7 @@ void SettingsPage::build_secure_payments_tab() {
if (BastionGuard::Platform::cef_available()) {
std::thread([]() {
std::system("/usr/libexec/bastionguard/bastionguard-service --user restart BastionGuard-cef.service");
(bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user", "restart", "BastionGuard-cef.service"}).exit_code);
}).detach();
}
});
@ -4283,7 +4162,7 @@ void SettingsPage::build_proxy_bypass_tab() {
// Riavvia il servizio CEF solo nelle build/distribuzioni che lo supportano.
if (BastionGuard::Platform::cef_available()) {
std::thread([]() {
std::system("/usr/libexec/bastionguard/bastionguard-service --user restart BastionGuard-cef.service 2>/dev/null || true");
(bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user", "restart", "BastionGuard-cef.service"}).exit_code);
}).detach();
}
});
@ -5200,35 +5079,16 @@ void SettingsPage::build_email_security_tab() {
const std::string password = std::string(eAdminPw->get_text());
std::string command =
"env BASTIONGUARD_SUDO_PASSWORD=" +
Glib::shell_quote(password) + " " +
Glib::shell_quote(script_path) + " 2>&1";
FILE* pipe = popen(command.c_str(), "r");
if (!pipe) {
// Password goes only to the child's environment, never to a shell or argv.
// The installer consumes it via sudo -S, and the parent does not log it.
const auto install = bg_process::run({script_path}, true,
8U * 1024U * 1024U, {{"BASTIONGUARD_SUDO_PASSWORD", password}});
if (!install.started) {
tbInstallBuffer->set_text(_("Impossibile avviare lo script di installazione.\n"));
return;
}
char buffer[512];
std::string output;
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
output += buffer;
}
int status_code = pclose(pipe);
int exit_code = -1;
#ifdef WIFEXITED
if (WIFEXITED(status_code))
exit_code = WEXITSTATUS(status_code);
else
exit_code = status_code;
#else
exit_code = status_code;
#endif
tbInstallBuffer->set_text(output);
const std::string& output = install.output;
const int exit_code = install.exit_code;
if (exit_code == 0) {
tbInstallBuffer->insert(
@ -5541,7 +5401,7 @@ void SettingsPage::build_email_security_tab() {
}
if (save_mail_security_config(auto_save)) {
std::system("/usr/libexec/bastionguard/bastionguard-service --user restart BastionGuard-mailproxy.service 2>/dev/null");
(bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user", "restart", "BastionGuard-mailproxy.service"}).exit_code);
tb_status->set_text(Glib::ustring::compose(
_("✔ %1 profili salvati e proxy riavviato."),
(int)auto_save.profiles.size()));
@ -5675,7 +5535,7 @@ void SettingsPage::build_email_security_tab() {
return;
}
std::system("/usr/libexec/bastionguard/bastionguard-service --user restart BastionGuard-mailproxy.service 2>/dev/null");
(bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user", "restart", "BastionGuard-mailproxy.service"}).exit_code);
status->set_text(_("✔ Configurazione salvata in ~/.config/BastionGuard/mail.json"));

View file

@ -19,14 +19,21 @@
*/
#include "UpdatePage.hpp"
#include "SafeProcess.hpp"
#include <filesystem>
#include <unistd.h>
#include <system_error>
#include <cstring>
#include "Resource.hpp"
#include <glibmm/spawn.h>
#include <glibmm/i18n.h>
#include <giomm.h>
#include <fstream>
#include <thread>
#include <utility>
#include <random>
#include <sstream>
#include <vector>
#include <thread>
extern "C" {
@ -102,25 +109,16 @@ UpdatePage::UpdatePage()
}
bool UpdatePage::checkServiceStatus() {
std::string out;
int status = -1;
try {
Glib::spawn_command_line_sync("/usr/libexec/bastionguard/bastionguard-service --system is-active clamav-freshclam.service",
&out, nullptr, &status);
return out.find("active") != std::string::npos;
} catch (...) {
return false;
}
return bg_process::run({"/usr/libexec/bastionguard/bastionguard-service",
"--system", "is-active", "clamav-freshclam.service"}).ok();
}
void UpdatePage::onServiceToggled() {
bool enable = serviceSwitch.get_active();
std::string cmd = std::string("pkexec /usr/libexec/bastionguard/bastionguard-service --system ") + (enable ? "enable --now" : "disable --now") + " clamav-freshclam.service";
buffer->insert(buffer->end(), (enable ? _("Abilito") : _("Disabilito")) + std::string(" il servizio freshclam...\n"));
std::string out, err;
Glib::spawn_command_line_sync(cmd, &out, &err);
buffer->insert(buffer->end(), out + err + "\n");
const auto result = bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service",
"--system", enable ? "enable" : "disable", "--now", "clamav-freshclam.service"}, true);
buffer->insert(buffer->end(), result.output + (result.ok() ? "\n" : _("Errore modifica servizio.\n")));
}
static const std::vector<std::string> sane_files = {
@ -150,7 +148,7 @@ void UpdatePage::onUpdateClicked() {
try {
logSafe(_("⏸️ Arresto servizio clamav-freshclam...\n"));
system("pkexec /usr/libexec/bastionguard/bastionguard-service --system stop clamav-freshclam.service");
(bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service", "--system", "stop", "clamav-freshclam.service"}).exit_code);
logSafe(_("🚀 Avvio aggiornamento con pkexec freshclam...\n"));
try {
Glib::spawn_command_line_sync("pkexec freshclam", &out, &err, &exit_status);
@ -165,7 +163,7 @@ void UpdatePage::onUpdateClicked() {
}
logSafe(Glib::ustring::compose(_("\n🧾 Codice di uscita: %1\n"), exit_status));
logSafe(_("▶️ Riavvio del servizio clamav-freshclam...\n"));
system("pkexec /usr/libexec/bastionguard/bastionguard-service --system start clamav-freshclam.service");
(bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service", "--system", "start", "clamav-freshclam.service"}).exit_code);
logSafe(_("\n🔽 Scarico firme aggiuntive dalle sorgenti configurate...\n"));
if (!alive->load()) return;
@ -176,7 +174,7 @@ void UpdatePage::onUpdateClicked() {
updateSaneSecurity();
logSafe(_("🔁 Ricarico clamd (clamav-daemon)...\n"));
int reload_status = system("pkexec /usr/libexec/bastionguard/bastionguard-service --system try-reload-or-restart clamav-daemon.service");
int reload_status = (bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service", "--system", "try-reload-or-restart", "clamav-daemon.service"}).exit_code);
if (reload_status != 0) {
logSafe(_("⚠ Reload clamav-daemon fallito (codice non zero).\n"));
}
@ -202,80 +200,108 @@ void UpdatePage::onUpdateClicked() {
void UpdatePage::updateFromExtraSources() {
auto alive = alive_;
Glib::signal_idle().connect_once([this, alive]() {
BastionGuard::if_alive(alive, [this]() {
buffer->insert(buffer->end(), _("\n🔽 Scarico firme aggiuntive dalle sorgenti configurate...\n"));
});
});
buffer->insert(buffer->end(), _("\n🔽 Scarico firme aggiuntive dalle sorgenti configurate...\n"));
// Snapshot the GTK widget values on the UI thread before starting a worker.
// A deferred idle callback here would capture a dead stack reference and
// race with the worker's copy of the source list.
std::vector<SourceItem> sources;
Glib::signal_idle().connect_once([this, alive, &sources]() {
BastionGuard::if_alive(alive, [this, &sources]() {
for (auto child : sourcesBox->get_children()) {
if (auto frame = dynamic_cast<Gtk::Frame*>(child)) {
auto inner = dynamic_cast<Gtk::Box*>(frame->get_child());
if (!inner) continue;
for (auto child : sourcesBox->get_children()) {
auto frame = dynamic_cast<Gtk::Frame*>(child);
if (!frame) continue;
auto section = dynamic_cast<Gtk::Box*>(frame->get_child());
if (!section) continue;
auto section_children = section->get_children();
if (section_children.size() < 2) continue;
auto revealer = dynamic_cast<Gtk::Revealer*>(section_children[1]);
if (!revealer) continue;
auto inner = dynamic_cast<Gtk::Box*>(revealer->get_child());
if (!inner) continue;
auto entries = inner->get_children();
if (entries.size() < 4) continue;
auto name = dynamic_cast<Gtk::Entry*>(entries[0]);
auto url = dynamic_cast<Gtk::Entry*>(entries[1]);
auto auth = dynamic_cast<Gtk::CheckButton*>(entries[2]);
auto username = dynamic_cast<Gtk::Entry*>(entries[3]);
if (!name || !url || !auth || !username) continue;
auto entries = inner->get_children();
if (entries.size() < 5) continue;
SourceItem source;
source.name = name->get_text();
source.url = url->get_text();
source.requires_auth = auth->get_active();
source.username = username->get_text();
if (!source.url.empty()) sources.push_back(std::move(source));
}
SourceItem s;
s.name = dynamic_cast<Gtk::Entry*>(entries[0])->get_text();
s.url = dynamic_cast<Gtk::Entry*>(entries[1])->get_text();
s.requires_auth = dynamic_cast<Gtk::CheckButton*>(entries[2])->get_active();
s.username = dynamic_cast<Gtk::Entry*>(entries[3])->get_text();
if (sources.empty()) {
buffer->insert(buffer->end(), _("Nessuna sorgente aggiuntiva configurata.\n"));
return;
}
sources.push_back(s);
}
}
});
});
std::thread([this, alive, sources]() {
std::thread([this, alive, sources = std::move(sources)]() {
bool all_ok = true;
for (const auto& src : sources) {
if (src.url.empty())
// Never allow an untrusted URL to select a local-file or other
// potentially unsafe curl protocol. No URL reaches a shell.
if (src.url.rfind("https://", 0) != 0) {
all_ok = false;
Glib::signal_idle().connect_once([this, alive, url = src.url]() {
BastionGuard::if_alive(alive, [this, &url]() {
buffer->insert(buffer->end(), _("⚠ Sorgente non HTTPS ignorata: ") + url + "\n");
});
});
continue;
}
std::string pw;
if (src.requires_auth)
pw = lookupPassword("BastionGuard:" + src.name);
std::string cmd;
std::vector<std::string> argv = {
"curl", "-f", "--proto", "=https", "--proto-redir", "=https"
};
if (src.requires_auth) {
cmd = "curl -f -u \"" + src.username + ":" + pw +
"\" -O --output-dir /var/lib/clamav " + src.url;
} else {
cmd = "curl -f -O --output-dir /var/lib/clamav " + src.url;
argv.push_back("-u");
argv.push_back(src.username + ":" + pw);
}
argv.insert(argv.end(), {
"-O", "--output-dir", "/var/lib/clamav",
"--url", src.url
});
std::string out, err;
int status = -1;
try {
Glib::spawn_command_line_sync(cmd, &out, &err);
} catch (...) {
Glib::spawn_sync("", argv, Glib::SpawnFlags::SEARCH_PATH,
{}, &out, &err, &status);
} catch (const Glib::Error& ex) {
err = ex.what();
}
const bool ok = status == 0;
if (!ok) all_ok = false;
if (!alive->load()) return;
Glib::signal_idle().connect_once([this, alive, src, out, err]() {
BastionGuard::if_alive(alive, [this, &src, &out, &err]() {
buffer->insert(buffer->end(), _("⬇️ Scarico da: ") + src.url + "\n");
Glib::signal_idle().connect_once([this, alive, src, out, err, ok]() {
BastionGuard::if_alive(alive, [this, &src, &out, &err, ok]() {
buffer->insert(buffer->end(),
(ok ? _("⬇️ Scarico da: ") : _("❌ Download fallito: ")) + src.url + "\n");
if (!out.empty()) buffer->insert(buffer->end(), out + "\n");
if (!err.empty()) buffer->insert(buffer->end(), err + "\n");
});
});
}
Glib::signal_idle().connect_once([this, alive]() {
BastionGuard::if_alive(alive, [this]() {
buffer->insert(buffer->end(), _("\n✅ Aggiornamento firme extra completato.\n"));
Glib::signal_idle().connect_once([this, alive, all_ok]() {
BastionGuard::if_alive(alive, [this, all_ok]() {
buffer->insert(buffer->end(), all_ok
? _("\n✅ Aggiornamento firme extra completato.\n")
: _("\n⚠ Aggiornamento firme extra incompleto: controllare gli errori.\n"));
});
});
}).detach();
}
void UpdatePage::onAddSourceClicked() {
createSourceRow();
}
@ -502,21 +528,26 @@ void UpdatePage::testConnection(const std::string& name, const std::string& url,
}
buffer->insert(buffer->end(), _("🔍 Test connessione a ") + url + "...\n");
std::string cmd;
std::string out, err;
std::vector<std::string> argv = {
"curl", "-I", "-s", "-o", "/dev/null", "-w", "%{http_code}"
};
if (auth) {
std::string pw = lookupPassword("BastionGuard:" + name);
if (pw.empty()) {
buffer->insert(buffer->end(), _("⚠ Nessuna password trovata per ") + name + "\n");
return;
}
cmd = "curl -I -u \"" + user + ":" + pw + "\" -s -o /dev/null -w \"%{http_code}\" " + url;
} else {
cmd = "curl -I -s -o /dev/null -w \"%{http_code}\" " + url;
argv.push_back("-u");
argv.push_back(user + ":" + pw);
}
argv.push_back("--url");
argv.push_back(url);
Glib::spawn_command_line_sync(cmd, &out, &err);
int status = -1;
Glib::spawn_sync("", argv, Glib::SpawnFlags::SEARCH_PATH,
{}, &out, &err, &status);
if (!err.empty())
buffer->insert(buffer->end(), _("Errore: ") + err + "\n");
@ -589,49 +620,48 @@ void UpdatePage::updateSaneSecurity() {
push(_("🌐 Aggiornamento database SaneSecurity (mirror sicuro)...\n"));
const std::string tmpDir = "/var/lib/clamav/sanesecurity.tmp";
std::string prep =
"pkexec sh -c 'rm -rf " + tmpDir + " && mkdir -p " + tmpDir + "'";
system(prep.c_str());
const std::string cmd =
"pkexec rsync -av --delete "
"rsync://rsync.sanesecurity.net/sanesecurity "
+ tmpDir + "/";
std::string out, err;
int status = -1;
try {
Glib::spawn_command_line_sync(cmd, &out, &err, &status);
namespace fs = std::filesystem;
// Keep the sync unprivileged. mkdtemp ensures an exclusive 0700 directory.
char tmpl[] = "/tmp/bastionguard-sanesecurity-XXXXXX";
char* created = ::mkdtemp(tmpl);
if (!created) {
push(_("❌ Impossibile creare la directory temporanea di staging.\n"));
return;
}
catch (const Glib::Error& ex) {
push(_("❌ Errore rsync: ") + std::string(ex.what()) + "\n");
const fs::path staging(created);
struct StagingGuard {
fs::path path;
~StagingGuard() { std::error_code ec; fs::remove_all(path, ec); }
} cleanup{staging};
const auto sync = bg_process::run({"rsync", "-av", "--",
"rsync://rsync.sanesecurity.net/sanesecurity/", staging.string() + "/"},
true, 4U * 1024U * 1024U);
if (!sync.ok()) {
push(_("❌ Download SaneSecurity fallito. Nessun database modificato.\n"));
return;
}
if (!out.empty()) push(out);
if (!err.empty()) push("⚠ STDERR: " + err + "\n");
const std::vector<std::string> exts = {
"*.ndb", "*.hdb", "*.ldb", "*.yar", "*.yara", "*.ign2", "*.txt"
const std::vector<std::string> extensions = {
".ndb", ".hdb", ".ldb", ".yar", ".yara", ".ign2", ".txt"
};
std::string installCmd = "pkexec sh -c '";
for (const auto& e : exts) {
installCmd +=
"cp -f " + tmpDir + "/" + e + " /var/lib/clamav/ 2>/dev/null; ";
std::size_t installed = 0;
std::error_code ec;
for (fs::directory_iterator it(staging, fs::directory_options::skip_permission_denied, ec),
end; !ec && it != end; it.increment(ec)) {
const fs::path path = it->path();
// Do not follow symlinks, directories, or special devices from a mirror.
if (!it->is_regular_file(ec) || it->is_symlink(ec)) { ec.clear(); continue; }
const std::string extension = path.extension().string();
if (std::find(extensions.begin(), extensions.end(), extension) == extensions.end())
continue;
const auto install = bg_process::run({"pkexec", "/usr/bin/install", "-m", "0644", "--",
path.string(), (fs::path("/var/lib/clamav") / path.filename()).string()}, true);
if (install.ok()) ++installed;
else push(_("⚠ Installazione firma fallita: ") + path.filename().string() + "\n");
}
installCmd += "'";
system(installCmd.c_str());
system(("pkexec rm -rf " + tmpDir).c_str());
push(_("📦 Firme SaneSecurity aggiornate senza toccare i database ufficiali.\n"));
if (installed == 0) push(_("⚠ Nessuna firma SaneSecurity installata.\n"));
else push(_("📦 Firme SaneSecurity installate: ") + std::to_string(installed) + "\n");
}
@ -671,13 +701,7 @@ void UpdatePage::onSaneAutoToggled() {
_("⏸️ Disattivo aggiornamento automatico SaneSecurity...\n")
);
std::string cmd =
std::string("pkexec /usr/libexec/bastionguard/bastionguard-service --system ") +
(enable ? "enable --now " : "disable --now ") +
"bastionguard-sanesecurity.timer";
std::string out, err;
Glib::spawn_command_line_sync(cmd, &out, &err);
buffer->insert(buffer->end(), out + err + "\n");
const auto result = bg_process::run({"pkexec", "/usr/libexec/bastionguard/bastionguard-service",
"--system", enable ? "enable" : "disable", "--now", "bastionguard-sanesecurity.timer"}, true);
buffer->insert(buffer->end(), result.output + (result.ok() ? "\n" : _("Errore modifica timer.\n")));
}

View file

@ -225,23 +225,10 @@ private:
void terminate_sandbox() {
if (!sandbox_proc_) return;
std::string cmd = "pgrep -f \"" + sandbox_cmd_ + "\"";
std::array<char, 128> buffer{};
std::string pid_str;
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
if (pipe && fgets(buffer.data(), buffer.size(), pipe.get()))
pid_str = buffer.data();
if (!pid_str.empty()) {
try {
pid_t pid = std::stoi(pid_str);
if (pid > 0) kill(pid, SIGTERM);
} catch (...) {
// pid_str non è un intero valido: ignora
}
}
// Only terminate the subprocess started by this window. Never search
// the entire process table with an untrusted URL in a shell command.
sandbox_proc_->force_exit();
sandbox_proc_.reset();
}
};

View file

@ -31,6 +31,7 @@
#include <glob.h>
#include "../DistroUtils.hpp"
#include "../SafeProcess.hpp"
#include <glib/gi18n.h>
@ -56,17 +57,15 @@ static inline fs::path stamp_path() {
}
static inline std::string ca_fingerprint(const fs::path& ca_path) {
const std::string cmd =
"openssl x509 -in \"" + ca_path.string() +
"\" -noout -fingerprint -sha1 2>/dev/null | cut -d= -f2";
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) return {};
char buf[256] = {};
fgets(buf, sizeof(buf), pipe);
pclose(pipe);
std::string s(buf);
if (!s.empty() && s.back() == '\n') s.pop_back();
return s;
const auto result = bg_process::run({"openssl", "x509", "-in", ca_path.string(),
"-noout", "-fingerprint", "-sha1"});
if (!result.ok()) return {};
const auto eq = result.output.find('=');
if (eq == std::string::npos) return {};
std::string fingerprint = result.output.substr(eq + 1);
while (!fingerprint.empty() && (fingerprint.back() == '\n' || fingerprint.back() == '\r'))
fingerprint.pop_back();
return fingerprint;
}
static inline bool stamp_is_valid() {
@ -86,8 +85,8 @@ static inline void write_stamp() {
} catch (...) {}
}
static inline bool run(const std::string& cmd) {
return std::system((cmd + " >/dev/null 2>&1").c_str()) == 0;
static inline bool run(const std::vector<std::string>& args) {
return bg_process::run(args).ok();
}
static inline std::string current_home() {
@ -120,7 +119,7 @@ static inline bool is_system_trusted() {
})
{
if (fs::exists(bundle) &&
run("openssl verify -CAfile \"" + bundle + "\" \"" + ca + "\""))
run({"openssl", "verify", "-CAfile", bundle, ca}))
return true;
}
return false;
@ -131,11 +130,8 @@ static inline void nss_add(const std::string& nssdb) {
const std::string ca = ca_src_path().string();
if (!fs::exists(ca)) return;
const std::string prefix = fs::exists(nssdb + "/cert9.db") ? "sql:" : "dbm:";
run("certutil -A"
" -d \"" + prefix + nssdb + "\""
" -n \"" + std::string(CA_NAME) + "\""
" -t \"CT,,\""
" -i \"" + ca + "\"");
run({"certutil", "-A", "-d", prefix + nssdb,
"-n", CA_NAME, "-t", "CT,,", "-i", ca});
}
static inline void install_nss_user(const std::string& home) {
@ -169,8 +165,7 @@ static inline bool install_system_via_pkexec() {
}
std::cerr << _("[ca-installer] 🔐 Richiesta autorizzazione installazione CA...\n");
const std::string cmd = "pkexec \"" + std::string(HELPER) + "\" \"" + ca_src_path().string() + "\"";
if (!run(cmd)) {
if (!run({"pkexec", HELPER, ca_src_path().string()})) {
std::cerr << _("[ca-installer] ❌ pkexec fallito o rifiutato dall'utente\n");
return false;
}
@ -205,7 +200,7 @@ static inline bool ensure_trusted() {
if (!install_system_via_pkexec()) {
std::cerr << _("[ca-installer] ⚠️ Fallback sudo...\n");
const bool sudo_ok =
run(std::string("sudo \"") + HELPER + "\" \"" + ca_path.string() + "\"");
run({"sudo", HELPER, ca_path.string()});
if (!sudo_ok) {
std::cerr << _("[ca-installer] ❌ Impossibile installare CA nel sistema\n");
install_nss_user(home);

View file

@ -31,6 +31,7 @@
#include <cctype>
#include <glib/gi18n.h>
#include "../SafeProcess.hpp"
namespace BrowserProxy {
namespace fs = std::filesystem;
@ -110,10 +111,6 @@ static void write_file(const fs::path& p, const std::string& content) {
if (f) f << content;
}
static void run_silent(const std::string& cmd) {
std::system(cmd.c_str());
}
static void remove_lines_containing(const fs::path& p, const std::string& needle) {
if (!fs::exists(p)) return;
std::ifstream in(p);
@ -311,31 +308,16 @@ static constexpr const char* BG_CA_CERT = "/etc/BastionGuard/certs/BastionGuard-
static constexpr const char* BG_CA_NICK = "BastionGuard-CA";
static bool nss_contains_ca(const std::string& database) {
const auto list = bg_process::run({"certutil", "-L", "-d", database});
return list.ok() && list.output.find(BG_CA_NICK) != std::string::npos;
}
static void install_ca_firefox_profile(const fs::path& profile) {
if (profile.empty() || !fs::exists(profile)) return;
if (!fs::exists(BG_CA_CERT)) return;
if (std::system("command -v certutil > /dev/null 2>&1") != 0) {
std::cerr << _("[ca-install] certutil not found, skipping Firefox CA install\n");
return;
}
const std::string check_cmd =
std::string("certutil -L -d '") + profile.string() +
"' 2>/dev/null | grep -q '" + BG_CA_NICK + "'";
if (std::system(check_cmd.c_str()) == 0) return;
const std::string cmd =
std::string("certutil -A -d '") + profile.string() +
"' -n '" + BG_CA_NICK +
"' -t 'CT,C,C' -i '" + BG_CA_CERT + "' 2>/dev/null";
run_silent(cmd);
char buf[512];
std::snprintf(buf, sizeof(buf),
_("[ca-install] CA installed in Firefox profile: %s\n"),
profile.c_str());
std::cerr << buf;
if (profile.empty() || !fs::exists(profile) || !fs::exists(BG_CA_CERT)) return;
if (nss_contains_ca(profile.string())) return;
(void)bg_process::run({"certutil", "-A", "-d", profile.string(),
"-n", BG_CA_NICK, "-t", "CT,C,C", "-i", BG_CA_CERT});
}
static void install_ca_firefox_family() {
@ -361,8 +343,6 @@ static void install_ca_firefox_family() {
static void install_ca_nssdb() {
if (!fs::exists(BG_CA_CERT)) return;
if (std::system("command -v certutil > /dev/null 2>&1") != 0) return;
const std::string home = home_dir();
if (home.empty()) return;
@ -370,30 +350,13 @@ static void install_ca_nssdb() {
if (!fs::exists(nssdb)) {
std::error_code ec;
fs::create_directories(nssdb, ec);
if (!ec)
run_silent("certutil -N --empty-password -d '" + nssdb.string() + "' 2>/dev/null || true");
if (!ec) (void)bg_process::run({"certutil", "-N", "--empty-password", "-d", nssdb.string()});
}
if (fs::exists(nssdb)) {
const std::string check =
"certutil -L -d '" + nssdb.string() + "' 2>/dev/null | grep -q '" + BG_CA_NICK + "'";
if (std::system(check.c_str()) != 0) {
run_silent("certutil -A -d '" + nssdb.string() +
"' -n '" + std::string(BG_CA_NICK) +
"' -t 'CT,C,C' -i '" + std::string(BG_CA_CERT) + "' 2>/dev/null");
std::cerr << _("[ca-install] CA installed in ~/.pki/nssdb (Chromium)\n");
}
}
const fs::path snap_nssdb = fs::path(home) / "snap/chromium/current/.pki/nssdb";
if (fs::exists(snap_nssdb)) {
const std::string check =
"certutil -L -d '" + snap_nssdb.string() + "' 2>/dev/null | grep -q '" + BG_CA_NICK + "'";
if (std::system(check.c_str()) != 0) {
run_silent("certutil -A -d '" + snap_nssdb.string() +
"' -n '" + std::string(BG_CA_NICK) +
"' -t 'CT,C,C' -i '" + std::string(BG_CA_CERT) + "' 2>/dev/null");
std::cerr << _("[ca-install] CA installed in snap Chromium nssdb\n");
for (const fs::path& database : {nssdb,
fs::path(home) / "snap/chromium/current/.pki/nssdb"}) {
if (fs::exists(database) && !nss_contains_ca(database.string())) {
(void)bg_process::run({"certutil", "-A", "-d", database.string(),
"-n", BG_CA_NICK, "-t", "CT,C,C", "-i", BG_CA_CERT});
}
}
}
@ -431,16 +394,12 @@ struct Settings {
int port;
};
static void run(const std::string& cmd) {
if (std::system(cmd.c_str()) != 0) {
char msg[512];
std::snprintf(msg, sizeof(msg), _("[proxy-config] command failed: %s\n"), cmd.c_str());
std::cerr << msg;
}
}
static void run_silent(const std::string& cmd) {
std::system(cmd.c_str());
static bool run(const std::vector<std::string>& args, bool silent = false) {
const auto result = bg_process::run(args, true, 256U * 1024U);
if (!result.ok() && !silent)
std::cerr << "[proxy-config] command failed: " << args.front() << " ("
<< result.exit_code << ")\n";
return result.ok();
}
enum class DE { GNOME, KDE, XFCE, MATE, CINNAMON, LXQT, OTHER };
@ -463,83 +422,82 @@ static DE detect() {
}
static void gnome_set(const Settings& s) {
const std::string p = std::to_string(s.port);
run("gsettings set org.gnome.system.proxy mode 'manual'");
const std::string port = std::to_string(s.port);
run({"gsettings", "set", "org.gnome.system.proxy", "mode", "manual"});
for (const char* scheme : {"http", "https"}) {
run(std::string("gsettings set org.gnome.system.proxy.") + scheme + " host '" + s.host + "'");
run(std::string("gsettings set org.gnome.system.proxy.") + scheme + " port " + p);
const std::string schema = std::string("org.gnome.system.proxy.") + scheme;
run({"gsettings", "set", schema, "host", s.host});
run({"gsettings", "set", schema, "port", port});
}
run("gsettings set org.gnome.system.proxy.socks host ''");
run("gsettings set org.gnome.system.proxy.socks port 0");
run(std::string("gsettings set org.gnome.system.proxy ignore-hosts \"")
+ BrowserProxy::LAN_BYPASS_GVARIANT + "\"");
run({"gsettings", "set", "org.gnome.system.proxy.socks", "host", ""});
run({"gsettings", "set", "org.gnome.system.proxy.socks", "port", "0"});
run({"gsettings", "set", "org.gnome.system.proxy", "ignore-hosts",
BrowserProxy::LAN_BYPASS_GVARIANT});
}
static void gnome_clear() {
run("gsettings set org.gnome.system.proxy mode 'none'");
run({"gsettings", "set", "org.gnome.system.proxy", "mode", "none"});
}
static void kde_refresh() {
// Either kioclient version may be installed; failures are harmless.
if (!run({"kioclient6", "clear"}, true))
(void)run({"kioclient5", "clear"}, true);
}
static void kde_set(const Settings& s) {
const std::string hp = "http://" + s.host + ":" + std::to_string(s.port);
for (const char* bin : {"kwriteconfig6", "kwriteconfig5"}) {
std::string base = std::string(bin) + " --file kioslaverc --group 'Proxy Settings'";
run_silent(base + " --key ProxyType 1");
run_silent(base + " --key httpProxy '" + hp + "'");
run_silent(base + " --key httpsProxy '" + hp + "'");
run_silent(base + " --key NoProxyFor '" +
std::string(BrowserProxy::LAN_BYPASS_CIDR) + "'");
// KDE ha un flag separato per saltare il proxy quando il dominio
// matcha "Reverse Proxy" / "ReversedException" — settato a false
// così la NoProxyFor list sopra è autorevole (esclude, non include).
run_silent(base + " --key ReversedException false");
const std::vector<std::string> common = {bin, "--file", "kioslaverc", "--group", "Proxy Settings"};
auto set = [&](const std::string& key, const std::string& val) {
std::vector<std::string> args = common;
args.insert(args.end(), {"--key", key, val});
(void)run(args, true);
};
set("ProxyType", "1");
set("httpProxy", hp);
set("httpsProxy", hp);
set("NoProxyFor", BrowserProxy::LAN_BYPASS_CIDR);
set("ReversedException", "false");
}
run_silent("kioclient5 clear 2>/dev/null || kioclient6 clear 2>/dev/null || true");
kde_refresh();
}
static void kde_clear() {
for (const char* bin : {"kwriteconfig6", "kwriteconfig5"}) {
run_silent(std::string(bin) +
" --file kioslaverc --group 'Proxy Settings' --key ProxyType 0 2>/dev/null || true");
}
run_silent("kioclient5 clear 2>/dev/null || kioclient6 clear 2>/dev/null || true");
for (const char* bin : {"kwriteconfig6", "kwriteconfig5"})
(void)run({bin, "--file", "kioslaverc", "--group", "Proxy Settings",
"--key", "ProxyType", "0"}, true);
kde_refresh();
}
static void xfce_set(const Settings& s) {
const std::string h = s.host;
const std::string p = std::to_string(s.port);
run_silent("xfconf-query -c xfce4-settings-editor -p /Net/ProxyMode -s manual 2>/dev/null || true");
const std::string port = std::to_string(s.port);
(void)run({"xfconf-query", "-c", "xfce4-settings-editor", "-p", "/Net/ProxyMode", "-s", "manual"}, true);
for (const char* scheme : {"http", "https"}) {
run_silent("xfconf-query -c xfce4-settings-editor -p /Net/" + std::string(scheme) +
"-proxy/host -s '" + h + "' 2>/dev/null || true");
run_silent("xfconf-query -c xfce4-settings-editor -p /Net/" + std::string(scheme) +
"-proxy/port -s " + p + " 2>/dev/null || true");
const std::string key = std::string("/Net/") + scheme + "-proxy/";
(void)run({"xfconf-query", "-c", "xfce4-settings-editor", "-p", key + "host", "-s", s.host}, true);
(void)run({"xfconf-query", "-c", "xfce4-settings-editor", "-p", key + "port", "-s", port}, true);
}
// XFCE non ha una "no proxy list" in xfconf nello schema standard, ma
// molti applicativi XFCE leggono no_proxy dall'environment: ci pensa env_set.
// Per le app GNOME-based che girano in XFCE (Geary, Evolution, ...)
// settiamo anche qui le ignore-hosts gsettings (best-effort).
run_silent(std::string("gsettings set org.gnome.system.proxy ignore-hosts \"")
+ BrowserProxy::LAN_BYPASS_GVARIANT + "\" 2>/dev/null || true");
(void)run({"gsettings", "set", "org.gnome.system.proxy", "ignore-hosts",
BrowserProxy::LAN_BYPASS_GVARIANT}, true);
}
static void xfce_clear() {
run_silent("xfconf-query -c xfce4-settings-editor -p /Net/ProxyMode -s none 2>/dev/null || true");
(void)run({"xfconf-query", "-c", "xfce4-settings-editor", "-p", "/Net/ProxyMode", "-s", "none"}, true);
}
static void mate_set(const Settings& s) {
const std::string p = std::to_string(s.port);
run_silent("gsettings set org.mate.network-preferences proxy-mode 'manual' 2>/dev/null || true");
run_silent("gsettings set org.mate.network-preferences http-host '" + s.host + "' 2>/dev/null || true");
run_silent("gsettings set org.mate.network-preferences http-port " + p + " 2>/dev/null || true");
run_silent("gsettings set org.mate.network-preferences https-host '" + s.host + "' 2>/dev/null || true");
run_silent("gsettings set org.mate.network-preferences https-port " + p + " 2>/dev/null || true");
run_silent(std::string("gsettings set org.mate.network-preferences ignore-hosts \"")
+ BrowserProxy::LAN_BYPASS_GVARIANT + "\" 2>/dev/null || true");
const std::string port = std::to_string(s.port);
for (const auto& [key, value] : std::vector<std::pair<std::string, std::string>>{
{"proxy-mode", "manual"}, {"http-host", s.host}, {"http-port", port},
{"https-host", s.host}, {"https-port", port},
{"ignore-hosts", BrowserProxy::LAN_BYPASS_GVARIANT}}) {
(void)run({"gsettings", "set", "org.mate.network-preferences", key, value}, true);
}
}
static void mate_clear() {
run_silent("gsettings set org.mate.network-preferences proxy-mode 'none' 2>/dev/null || true");
(void)run({"gsettings", "set", "org.mate.network-preferences", "proxy-mode", "none"}, true);
}
static void lxqt_set(const Settings& s) {
@ -611,6 +569,15 @@ static void env_clear() {
}
static void apply(const Settings& s) {
// Validate addresses before persisting them to configuration files.
const bool valid_host = !s.host.empty() && s.host.size() <= 253 &&
std::all_of(s.host.begin(), s.host.end(), [](unsigned char c) {
return std::isalnum(c) || c == '.' || c == '-' || c == ':' || c == '[' || c == ']';
});
if (!valid_host || s.port < 1 || s.port > 65535) {
std::cerr << "[proxy-config] refusing unsafe host or port\n";
return;
}
char msg[256];
std::snprintf(msg, sizeof(msg), _("[proxy-config] setting system proxy → %s:%d\n"),
s.host.c_str(), s.port);

View file

@ -519,6 +519,19 @@ static inline bool is_stateful_method(const std::string& method_l) {
return !(method_l == "get" || method_l == "head" || method_l == "options");
}
// Prefetch/prerender sono richieste speculative: per domini banca/payment non
// c'e' alcun vantaggio a farle arrivare all'upstream prima di un'azione reale
// dell'utente. Rispondendo localmente evitiamo rete/CPU e soprattutto evitiamo
// che un sito interpreti una pre-navigazione come un nuovo accesso da validare.
static inline bool is_speculative_request(const std::string& raw) {
const std::string purpose = lower_ascii(header_value_ci(raw, "purpose"));
const std::string sec_purpose = lower_ascii(header_value_ci(raw, "sec-purpose"));
return purpose.find("prefetch") != std::string::npos ||
sec_purpose.find("prefetch") != std::string::npos ||
purpose.find("prerender") != std::string::npos ||
sec_purpose.find("prerender") != std::string::npos;
}
static inline bool should_passthrough_transaction(const std::string& raw) {
const std::string method = request_method_from_raw(raw);
if (method.empty()) return false; // fail verso il comportamento storico
@ -543,18 +556,51 @@ static inline bool should_passthrough_transaction(const std::string& raw) {
return true;
}
const std::string purpose = lower_ascii(header_value_ci(raw, "purpose"));
const std::string sec_purpose = lower_ascii(header_value_ci(raw, "sec-purpose"));
if (purpose.find("prefetch") != std::string::npos ||
sec_purpose.find("prefetch") != std::string::npos) {
// Difesa in profondita': nel path normale queste richieste vengono
// soppresse prima di arrivare qui. Se la funzione viene riusata altrove,
// restano comunque non eleggibili per l'apertura CEF.
if (is_speculative_request(raw))
return true;
// Una vera apertura automatica del Secure Browser deve corrispondere a
// una navigazione top-level iniziata dall'utente. I browser moderni
// aggiungono `Sec-Fetch-User: ?1` solo quando la navigazione deriva da
// user activation (click, invio esplicito, barra indirizzi, bookmark).
//
// Se Fetch Metadata e' presente ma Sec-Fetch-User manca, si tratta di una
// navigazione automatica (redirect/script/meta refresh/autosubmit GET):
// non generiamo una seconda sessione CEF. La richiesta resta nel contesto
// del browser che l'ha originata e mantiene cookie/sessione/redirect chain.
//
// Compatibilita': se Fetch Metadata manca completamente, manteniamo il
// comportamento storico e lasciamo che la navigazione apra CEF. Questo
// evita di rompere client/browser che non implementano Sec-Fetch-*.
const bool has_fetch_metadata = !dest.empty() || !mode.empty();
if (has_fetch_metadata && dest == "document" && mode == "navigate") {
const std::string user = trim_ascii(header_value_ci(raw, "sec-fetch-user"));
if (user != "?1")
return true;
}
// dest=document + mode=navigate, oppure header Fetch Metadata assenti:
// conserva il comportamento top-level esistente.
// document+navigate+Sec-Fetch-User:?1, oppure header Fetch Metadata
// completamente assenti: conserva il comportamento top-level esistente.
return false;
}
static inline void send_speculative_no_content(asio::ssl::stream<tcp::socket>& tls) {
static const char kResponse[] =
"HTTP/1.1 204 No Content\r\n"
"Cache-Control: no-store\r\n"
"Content-Length: 0\r\n"
"Connection: close\r\n"
"\r\n";
boost::system::error_code ec;
asio::write(tls, asio::buffer(kResponse, sizeof(kResponse) - 1), ec);
if (ec)
std::cerr << "[tls-intercept] speculative response write failed: "
<< ec.message() << "\n";
}
static inline bool parse_content_length(const std::string& raw,
std::size_t& out_len) {
const std::string v = trim_ascii(header_value_ci(raw, "content-length"));
@ -824,11 +870,35 @@ static inline void do_intercept(tcp::socket sock,
asio::buffers_end(rbuf.data())
};
// Le richieste speculative non devono neppure raggiungere il sito:
// non aprono CEF e non consumano una challenge/controllo anti-abuso.
// La navigazione reale successiva (con user activation) verra' gestita
// normalmente.
if (is_speculative_request(raw)) {
const std::string method = request_method_from_raw(raw);
const std::string dest = lower_ascii(header_value_ci(raw, "sec-fetch-dest"));
std::cerr << "[tls-intercept] speculative request suppressed: host=" << host
<< " method=" << method << " dest=" << dest << "\n";
send_speculative_no_content(tls);
boost::system::error_code sd_ec;
tls.shutdown(sd_ec);
boost::system::error_code ignore;
tls.lowest_layer().close(ignore);
return;
}
if (should_passthrough_transaction(raw)) {
const std::string method = request_method_from_raw(raw);
const std::string dest = lower_ascii(header_value_ci(raw, "sec-fetch-dest"));
std::cerr << "[tls-intercept] iframe/stateful passthrough: host=" << host
<< " method=" << method << " dest=" << dest << "\n";
const std::string mode = lower_ascii(header_value_ci(raw, "sec-fetch-mode"));
const std::string user = trim_ascii(header_value_ci(raw, "sec-fetch-user"));
std::cerr << "[tls-intercept] passthrough (subresource/stateful/automatic): host=" << host
<< " method=" << method
<< " dest=" << dest
<< " mode=" << mode
<< " user=" << (user.empty() ? "<none>" : user)
<< "\n";
if (!forward_transaction_https(tls, host, raw))
send_passthrough_failure(tls);

View file

@ -18,16 +18,17 @@
* The BastionGuard™ name and branding are not licensed under the GPL.
*/
#include "../SafeProcess.hpp"
#include "FirewallDetector.hpp"
#include <fstream>
#include <cstdlib>
bool FirewallDetector::isFirewalldPresent() {
return (std::system("which firewall-cmd > /dev/null 2>&1") == 0);
return ((bg_process::exists_on_path("firewall-cmd") ? 0 : 1) == 0);
}
bool FirewallDetector::isUfwPresent() {
return (std::system("which ufw > /dev/null 2>&1") == 0);
return ((bg_process::exists_on_path("ufw") ? 0 : 1) == 0);
}
std::string FirewallDetector::detectDistro() {

View file

@ -19,84 +19,90 @@
*/
#include "FirewallRuleManager.hpp"
#include <cerrno>
#include <iostream>
#include <regex>
#include <unordered_set>
#include <sstream>
#include <vector>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <glib/gi18n.h>
static bool is_valid_ip(const std::string& ip) {
static const std::regex ipv4(
R"(^(25[0-5]|2[0-4]\d|1?\d{1,2})(\.(25[0-5]|2[0-4]\d|1?\d{1,2})){3}$)"
);
static const std::regex ipv6(
R"(^(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}$)"
);
return std::regex_match(ip, ipv4) || std::regex_match(ip, ipv6);
namespace {
int ip_family(const std::string& ip) {
struct in_addr ipv4 {};
if (::inet_pton(AF_INET, ip.c_str(), &ipv4) == 1) return AF_INET;
struct in6_addr ipv6 {};
if (::inet_pton(AF_INET6, ip.c_str(), &ipv6) == 1) return AF_INET6;
return AF_UNSPEC;
}
}
bool FirewallRuleManager::run(const std::string& cmd) {
int r = std::system(cmd.c_str());
if (r != 0)
std::cerr << _("[Firewall] Command failed: ") << cmd << "\n";
return r == 0;
bool FirewallRuleManager::run(const std::vector<std::string>& command) {
if (command.empty()) return false;
const pid_t pid = ::fork();
if (pid < 0) return false;
if (pid == 0) {
std::vector<char*> argv;
for (const auto& arg : command) argv.push_back(const_cast<char*>(arg.c_str()));
argv.push_back(nullptr);
::execvp(argv.front(), argv.data());
_exit(127);
}
int status = 0;
pid_t result;
do { result = ::waitpid(pid, &status, 0); } while (result < 0 && errno == EINTR);
const bool ok = result == pid && WIFEXITED(status) && WEXITSTATUS(status) == 0;
if (!ok) std::cerr << _("[Firewall] Command failed: ") << command[0] << "\n";
return ok;
}
void FirewallRuleManager::applyWithFirewalld(const std::vector<std::string>& ips) {
std::unordered_set<std::string> filtered;
std::unordered_set<std::string> ipv4;
std::unordered_set<std::string> ipv6;
for (const auto& ip : ips) {
if (is_valid_ip(ip))
filtered.insert(ip);
else
std::cerr << _("[Firewall] SKIP non-IP: ") << ip << "\n";
switch (ip_family(ip)) {
case AF_INET: ipv4.insert(ip); break;
case AF_INET6: ipv6.insert(ip); break;
default: std::cerr << _("[Firewall] SKIP non-IP: ") << ip << "\n"; break;
}
}
if (filtered.empty()) {
if (ipv4.empty() && ipv6.empty()) {
std::cerr << _("[Firewall] Nessun IP valido da applicare.\n");
return;
}
run("pkexec firewall-cmd --permanent --new-ipset=bastionguard-blocklist "
"--type=hash:ip --option=family=inet");
for (const auto& ip : filtered) {
std::string cmd =
"pkexec firewall-cmd --permanent "
"--ipset=bastionguard-blocklist --add-entry=" + ip;
run(cmd);
}
run("pkexec firewall-cmd --permanent "
"--add-rich-rule='rule source ipset=\"bastionguard-blocklist\" reject'");
run("pkexec firewall-cmd --reload");
const auto add_family = [&](const std::unordered_set<std::string>& entries,
const std::string& name, const std::string& family,
const std::string& ipset_family) {
if (entries.empty()) return;
// Already-existing sets may return nonzero; adding entries is idempotent.
run({"pkexec", "firewall-cmd", "--permanent", "--new-ipset=" + name,
"--type=hash:ip", "--option=family=" + ipset_family});
for (const auto& ip : entries) {
run({"pkexec", "firewall-cmd", "--permanent", "--ipset=" + name,
"--add-entry=" + ip});
}
run({"pkexec", "firewall-cmd", "--permanent",
"--add-rich-rule=rule family=\"" + family + "\" source ipset=\"" + name + "\" reject"});
};
add_family(ipv4, "bastionguard-blocklist", "ipv4", "inet");
add_family(ipv6, "bastionguard-blocklist-v6", "ipv6", "inet6");
run({"pkexec", "firewall-cmd", "--reload"});
}
void FirewallRuleManager::applyWithUfw(const std::vector<std::string>& ips) {
std::unordered_set<std::string> filtered;
for (const auto& ip : ips) {
if (is_valid_ip(ip))
filtered.insert(ip);
else
std::cerr << _("[Firewall] SKIP non-IP: ") << ip << "\n";
if (ip_family(ip) != AF_UNSPEC) filtered.insert(ip);
else std::cerr << _("[Firewall] SKIP non-IP: ") << ip << "\n";
}
if (filtered.empty()) {
std::cerr << _("[Firewall] Nessun IP valido da applicare.\n");
return;
}
for (const auto& ip : filtered) {
run("pkexec ufw deny from " + ip);
}
run("pkexec ufw reload");
for (const auto& ip : filtered) run({"pkexec", "ufw", "deny", "from", ip});
run({"pkexec", "ufw", "reload"});
}
void FirewallRuleManager::apply(FirewallType fw, const std::vector<std::string>& ips) {
@ -105,12 +111,10 @@ void FirewallRuleManager::apply(FirewallType fw, const std::vector<std::string>&
std::cout << _("[Firewall] Applying rules with firewalld (ipset)\n");
applyWithFirewalld(ips);
break;
case FirewallType::UFW:
std::cout << _("[Firewall] Applying rules with UFW\n");
applyWithUfw(ips);
break;
default:
std::cerr << _("[Firewall] Nessun firewall selezionato.\n");
break;

View file

@ -30,7 +30,7 @@ public:
static void apply(FirewallType fw, const std::vector<std::string>& ips);
private:
static bool run(const std::string& cmd);
static bool run(const std::vector<std::string>& argv);
static void applyWithFirewalld(const std::vector<std::string>& ips);
static void applyWithUfw(const std::vector<std::string>& ips);

View file

@ -18,6 +18,7 @@
* The BastionGuard™ name and branding are not licensed under the GPL.
*/
#include "../SafeProcess.hpp"
#include <arpa/inet.h>
#include <curl/curl.h>
#include <netinet/in.h>
@ -1527,8 +1528,8 @@ static bool send_via_remote_smtp(
curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, "AUTH=LOGIN");
}
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, profile.ssl_verify ? 1L : 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, profile.ssl_verify ? 2L : 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 90L);
@ -1554,8 +1555,8 @@ static bool send_via_remote_smtp(
else if (profile.auth_method == 2)
curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, "AUTH=LOGIN");
}
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, profile.ssl_verify ? 1L : 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, profile.ssl_verify ? 2L : 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 90L);
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
@ -1567,45 +1568,9 @@ static bool send_via_remote_smtp(
CURLcode res = curl_easy_perform(curl);
std::cerr << "[mailproxy] tentativo invio: " << curl_easy_strerror(res) << "\n";
// Reject untrusted certificates. Never retry SMTP with TLS verification disabled.
if (res == CURLE_PEER_FAILED_VERIFICATION || res == CURLE_SSL_CACERT) {
std::cerr << "[mailproxy] certificato server non verificabile ("
<< curl_easy_strerror(res) << "), ritento senza verifica TLS\n";
curl_easy_cleanup(curl);
curl = curl_easy_init();
if (curl) {
curl_slist_free_all(recipients);
recipients = nullptr;
for (const auto& rcpt : rcpts)
recipients = curl_slist_append(recipients, rcpt.c_str());
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_MAIL_FROM, mail_from.c_str());
curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recipients);
curl_easy_setopt(curl, CURLOPT_READFUNCTION, payload_source);
curl_easy_setopt(curl, CURLOPT_READDATA, &upload);
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(curl, CURLOPT_USE_SSL,
profile.implicit_tls || profile.starttls ? CURLUSESSL_ALL : CURLUSESSL_NONE);
curl_easy_setopt(curl, CURLOPT_USERNAME, profile.username.c_str());
if (profile.auth_method == 3 && !profile.oauth2_token.empty()) {
curl_easy_setopt(curl, CURLOPT_PASSWORD, profile.oauth2_token.c_str());
curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, "AUTH=XOAUTH2");
} else {
curl_easy_setopt(curl, CURLOPT_PASSWORD, profile.password.c_str());
if (profile.auth_method == 1)
curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, "AUTH=PLAIN");
else if (profile.auth_method == 2)
curl_easy_setopt(curl, CURLOPT_LOGIN_OPTIONS, "AUTH=LOGIN");
}
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 90L);
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_WHATEVER);
upload.bytes_read = 0;
res = curl_easy_perform(curl);
std::cerr << "[mailproxy] tentativo senza verifica TLS: " << curl_easy_strerror(res) << "\n";
}
std::cerr << "[mailproxy] TLS certificate verification failed; refusing insecure retry\n";
}
long response_code = 0;
@ -2143,14 +2108,10 @@ static void install_cert_in_thunderbird(const std::string& cert_path) {
const std::string profile_dir = entry.path().string();
std::string cmd = "certutil -A"
" -n 'BastionGuard Mail Proxy'"
" -t 'P,,'"
" -i " + cert_path +
" -d 'sql:" + profile_dir + "'"
" 2>/dev/null";
const int ret = bg_process::run({"certutil", "-A", "-n", "BastionGuard Mail Proxy",
"-t", "P,,", "-i", cert_path,
"-d", "sql:" + profile_dir}).exit_code;
int ret = std::system(cmd.c_str());
if (ret == 0)
std::cerr << _("[mailproxy] certificato installato nel profilo Thunderbird: ")
<< profile_dir << "\n";

View file

@ -90,14 +90,6 @@ void send_message(const json &j) {
std::cout.flush();
}
int run_cmd(const std::string &cmd) {
int rc = std::system(cmd.c_str());
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return rc;
}
static std::unordered_set<std::string> cached_bl;
static std::time_t cached_bl_mtime = 0;
static fs::path cached_bl_path;

View file

@ -20,6 +20,7 @@
#include "../DistroUtils.hpp"
#include "../SafeProcess.hpp"
#include <boost/asio.hpp>
#include <boost/asio/signal_set.hpp>
@ -303,13 +304,13 @@ static std::string http_response_405() {
static int run_trigger_cmd(const std::string& cmd) {
// A trigger is reached from a localhost TCP listener. Never pass its
// configurable command string to /bin/sh. Only allow the packaged action.
if (cmd.empty()) return 0;
int rc = std::system(cmd.c_str());
if (rc == -1) return 127;
#ifdef WIFEXITED
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
#endif
return 0;
if (cmd != "/usr/libexec/bastionguard/bastionguard-service --user start BastionGuard-cef.service")
return 64;
return bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user",
"start", "BastionGuard-cef.service"}).exit_code;
}
static bool wait_backend_up(const std::string& backend_host, int backend_port, int wait_ms) {

View file

@ -19,7 +19,9 @@
*/
#include "PacManager.hpp"
#include "../SafeProcess.hpp"
#include <cstdlib>
#include <unistd.h>
#include <cstdio>
#include <array>
#include <filesystem>
@ -32,29 +34,23 @@
namespace fs = std::filesystem;
bool PacManager::cmd_exists(const std::string& cmd) {
std::string c = "command -v " + cmd + " >/dev/null 2>&1";
return (std::system(c.c_str()) == 0);
if (cmd.empty()) return false;
if (cmd.find('/') != std::string::npos) return access(cmd.c_str(), X_OK) == 0;
const char* configured_path = std::getenv("PATH");
std::istringstream dirs(configured_path ? configured_path : "/usr/local/bin:/usr/bin:/bin");
std::string dir;
while (std::getline(dirs, dir, ':')) {
if (dir.empty()) dir = ".";
if (access((fs::path(dir) / cmd).c_str(), X_OK) == 0) return true;
}
return false;
}
int PacManager::run_cmd(const std::string& cmd, std::string* out) {
std::array<char, 256> buf{};
std::string result;
FILE* p = popen(cmd.c_str(), "r");
if (!p) return 127;
while (fgets(buf.data(), (int)buf.size(), p) != nullptr)
result += buf.data();
int rc = pclose(p);
if (out) *out = result;
return rc;
}
static bool run_user_servicectl(const std::string& args) {
std::string cmd = "/usr/libexec/bastionguard/bastionguard-service --user " + args + " >/dev/null 2>&1";
return (std::system(cmd.c_str()) == 0);
static bool run_user_servicectl(const std::vector<std::string>& args) {
std::vector<std::string> command = {
"/usr/libexec/bastionguard/bastionguard-service", "--user"};
command.insert(command.end(), args.begin(), args.end());
return bg_process::run(command).ok();
}
PacManager::Result PacManager::start_pacd_user() {
@ -65,8 +61,8 @@ PacManager::Result PacManager::start_pacd_user() {
return r;
}
if (run_user_servicectl("daemon-reload") &&
run_user_servicectl("enable --now BastionGuard-pacd.service")) {
if (run_user_servicectl({"daemon-reload"}) &&
run_user_servicectl({"enable", "--now", "BastionGuard-pacd.service"})) {
r.ok = true;
r.message = _("bastionguard-pacd avviato (servizio utente)");
return r;
@ -85,7 +81,7 @@ PacManager::Result PacManager::stop_pacd_user() {
}
// stop best-effort
run_user_servicectl("disable --now BastionGuard-pacd.service");
run_user_servicectl({"disable", "--now", "BastionGuard-pacd.service"});
r.ok = true;
r.message = _("bastionguard-pacd fermato (servizio utente)");
return r;
@ -130,9 +126,8 @@ PacManager::Result PacManager::enable_gnome(const std::string& pac_url) {
}
// Imposta PAC (AUTO)
std::string out;
int rc1 = run_cmd("gsettings set org.gnome.system.proxy mode 'auto' 2>/dev/null", &out);
int rc2 = run_cmd("gsettings set org.gnome.system.proxy autoconfig-url " + Glib::shell_quote(pac_url) + " 2>/dev/null", &out);
int rc1 = bg_process::run({"gsettings", "set", "org.gnome.system.proxy", "mode", "auto"}).exit_code;
int rc2 = bg_process::run({"gsettings", "set", "org.gnome.system.proxy", "autoconfig-url", pac_url}).exit_code;
if (rc1 == 0 && rc2 == 0) {
r.ok = true;
@ -152,8 +147,7 @@ PacManager::Result PacManager::disable_gnome() {
return r;
}
std::string out;
int rc = run_cmd("gsettings set org.gnome.system.proxy mode 'none' 2>/dev/null", &out);
int rc = bg_process::run({"gsettings", "set", "org.gnome.system.proxy", "mode", "none"}).exit_code;
if (rc == 0) {
r.ok = true;
r.message = _("Proxy GNOME ripristinato a NONE");
@ -177,14 +171,10 @@ PacManager::Result PacManager::enable_kde(const std::string& pac_url) {
} catch (...) {}
if (cmd_exists("kwriteconfig6")) {
std::ostringstream cmd;
cmd << "kwriteconfig6 --file " << Glib::shell_quote(p.string())
<< " --group 'Proxy Settings' --key ProxyType 2 && "
<< "kwriteconfig6 --file " << Glib::shell_quote(p.string())
<< " --group 'Proxy Settings' --key AutoConfigScript "
<< Glib::shell_quote(pac_url);
if (std::system(cmd.str().c_str()) == 0) {
auto first = bg_process::run({"kwriteconfig6", "--file", p.string(),
"--group", "Proxy Settings", "--key", "ProxyType", "2"});
if (first.ok() && bg_process::run({"kwriteconfig6", "--file", p.string(),
"--group", "Proxy Settings", "--key", "AutoConfigScript", pac_url}).ok()) {
r.ok = true;
r.message = _("Proxy KDE impostato su PAC (kioslaverc)");
return r;
@ -231,10 +221,8 @@ PacManager::Result PacManager::disable_kde() {
}
if (cmd_exists("kwriteconfig6")) {
std::ostringstream cmd;
cmd << "kwriteconfig6 --file " << Glib::shell_quote(p.string())
<< " --group 'Proxy Settings' --key ProxyType 0";
if (std::system(cmd.str().c_str()) == 0) {
if (bg_process::run({"kwriteconfig6", "--file", p.string(),
"--group", "Proxy Settings", "--key", "ProxyType", "0"}).ok()) {
r.ok = true;
r.message = _("Proxy KDE ripristinato a NONE");
return r;
@ -282,12 +270,10 @@ PacManager::Result PacManager::enable_xfce(const std::string& pac_url) {
return r;
}
std::string cmd1 = "xfconf-query -c xfce4-settings-manager -p /proxy/mode -s 2 2>/dev/null";
std::string cmd2 = "xfconf-query -c xfce4-settings-manager -p /proxy/autoconfig-url -s "
+ Glib::shell_quote(pac_url) + " 2>/dev/null";
int rc1 = std::system(cmd1.c_str());
int rc2 = std::system(cmd2.c_str());
const int rc1 = bg_process::run({"xfconf-query", "-c", "xfce4-settings-manager",
"-p", "/proxy/mode", "-s", "2"}).exit_code;
const int rc2 = bg_process::run({"xfconf-query", "-c", "xfce4-settings-manager",
"-p", "/proxy/autoconfig-url", "-s", pac_url}).exit_code;
if (rc1 == 0 && rc2 == 0) {
r.ok = true;
@ -307,8 +293,7 @@ PacManager::Result PacManager::disable_xfce() {
return r;
}
std::string cmd = "xfconf-query -c xfce4-settings-manager -p /proxy/mode -s 0 2>/dev/null";
int rc = std::system(cmd.c_str());
int rc = bg_process::run({"xfconf-query", "-c", "xfce4-settings-manager", "-p", "/proxy/mode", "-s", "0"}).exit_code;
if (rc == 0) {
r.ok = true;
r.message = _("Proxy XFCE ripristinato a NONE");

View file

@ -41,7 +41,6 @@ public:
private:
static bool cmd_exists(const std::string& cmd);
static int run_cmd(const std::string& cmd, std::string* out = nullptr);
static Result enable_gnome(const std::string& pac_url);
static Result disable_gnome();

View file

@ -18,6 +18,7 @@
* The BastionGuard™ name and branding are not licensed under the GPL.
*/
#include "../SafeProcess.hpp"
#include "BastionGuard-secure.hpp"
#include "SecureBrowser.hpp"
#include "dns_secure.hpp"
@ -64,17 +65,12 @@ static void configure_graphics_backend() {
static bool has_in_path(const char* exe) {
if (!exe || !*exe) return false;
std::string cmd = "command -v ";
cmd += exe;
cmd += " >/dev/null 2>&1";
return std::system(cmd.c_str()) == 0;
return exe && bg_process::exists_on_path(exe);
}
int main(int argc, char** argv) {
setlocale(LC_ALL, "");
bindtextdomain("BastionGuard", LOCALEDIR);

View file

@ -34,6 +34,8 @@
#include <regex>
#include <string>
#include <vector>
#include <sys/wait.h>
#include <unistd.h>
namespace fs = std::filesystem;
using json = nlohmann::json;
@ -72,20 +74,37 @@ void SecureList::open_system_browser(const std::string& url) {
}
const char* wayland = std::getenv("WAYLAND_DISPLAY");
const pid_t pid = ::fork();
if (pid < 0) {
std::cerr << _("[BastionGuard-secure] ❌ Impossibile avviare il browser di sistema.\n");
return;
}
// Costruiamo un prefisso che neutralizza il proxy SOLO per il processo
// figlio (env inline), senza toccare l'ambiente del proxy stesso.
std::string env_prefix =
"BG_OPENER_DEPTH=1 no_proxy='*' NO_PROXY='*' "
"http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= all_proxy= ALL_PROXY= ";
if (pid == 0) {
// Neutralizza il proxy soltanto nel processo figlio e passa l'URL come
// singolo argv: nessuna interpretazione di metacaratteri da una shell.
::setenv("BG_OPENER_DEPTH", "1", 1);
::setenv("no_proxy", "*", 1);
::setenv("NO_PROXY", "*", 1);
static const char* proxy_vars[] = {
"http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"
};
for (const char* name : proxy_vars)
::unsetenv(name);
std::string cmd;
if (wayland && *wayland && std::system("command -v gio > /dev/null 2>&1") == 0)
cmd = env_prefix + "gio open \"" + url + "\"";
else
cmd = env_prefix + "xdg-open \"" + url + "\"";
if (wayland && *wayland) {
const char* gio_argv[] = {"gio", "open", url.c_str(), nullptr};
::execvp("gio", const_cast<char* const*>(gio_argv));
}
std::system(cmd.c_str());
const char* xdg_argv[] = {"xdg-open", url.c_str(), nullptr};
::execvp("xdg-open", const_cast<char* const*>(xdg_argv));
_exit(127);
}
int status = 0;
if (::waitpid(pid, &status, 0) < 0)
std::cerr << _("[BastionGuard-secure] ⚠️ waitpid fallita per browser di sistema.\n");
}
std::string SecureList::clean_domain(const std::string& d) {

View file

@ -31,6 +31,7 @@
// forzare i due target a condividere il main() del CLI.
// =============================================================================
#include "../SafeProcess.hpp"
#include "dns_secure.hpp"
#include <unistd.h>
@ -126,7 +127,7 @@ static bool path_exists(const std::string& p) {
// =============================================================================
void reexec_inside_bwrap_with_custom_dns(int argc, char** argv) {
if (std::getenv("BASTIONGUARD_IN_BWRAP")) return;
if (std::system("command -v bwrap > /dev/null 2>&1") != 0) return;
if ((bg_process::exists_on_path("bwrap") ? 0 : 1) != 0) return;
const std::string resolv = "/tmp/bastionguard-resolv.conf";
write_resolv_conf(resolv);

View file

@ -20,6 +20,7 @@
#include "local_warning_server.hpp"
#include "../SafeProcess.hpp"
#include <iostream>
#include <csignal>
#include <thread>
@ -174,14 +175,8 @@ static void ensure_log_file(const std::string& log_dir, const std::string& log_f
static bool can_bind_privileged_ports() {
if (geteuid() == 0) return true;
FILE* fp = popen("getcap /proc/self/exe 2>/dev/null", "r");
if (!fp) return false;
char buf[256];
bool ok = false;
while (fgets(buf, sizeof(buf), fp)) {
if (strstr(buf, "cap_net_bind_service")) { ok = true; break; }
}
pclose(fp);
const auto caps = bg_process::run({"getcap", "/proc/self/exe"});
const bool ok = caps.ok() && caps.output.find("cap_net_bind_service") != std::string::npos;
return ok;
}

View file

@ -18,6 +18,7 @@
* The BastionGuard™ name and branding are not licensed under the GPL.
*/
#include "../SafeProcess.hpp"
#include "dnsmasq_backend.hpp"
#include <fstream>
#include <filesystem>
@ -210,7 +211,7 @@ bool DnsmasqBackend::write_whitelist(
bool DnsmasqBackend::reload_dnsmasq()
{
int st = system("/usr/libexec/bastionguard/bastionguard-service --system try-reload-or-restart dnsmasq.service");
int st = (bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--system", "try-reload-or-restart", "dnsmasq.service"}).exit_code);
return WIFEXITED(st) && WEXITSTATUS(st) == 0;
}

View file

@ -10,7 +10,7 @@ ae44155dc7b8caa0cf64fb1e99a91610b2dc77724eaf859b1c48866ab44e8ef0 ./RootGuardPag
bc601093800381c42e3e24bf4c0d4ea1c7736250468f4902ef04a8d929555504 ./RootGuardPage.hpp
02384c9de7e571fc67a1862fa1c5e3a0fe7041ab77c2f252d6f1a4878daee8eb ./bastionguard-rootguard.pot
b2a278fda378093612a9412871beebdc677d8b52aa27840cd2d67b115cc21c74 ./bpf/rootguard.bpf.c
374cc0180b20e1209bcb04c5068ff930a35f0b3dbd7ed89f359574ebefb09b60 ./cmake/BastionGuardRootGuard.cmake
81ae1b667eb6d3e53050d20d1edbb47486251ec27f277cbc3db6ab4365522c98 ./cmake/BastionGuardRootGuard.cmake
082bd47224d9a11a2026629a6bc9f4454054f28e13f8a001969e0ad1b42bdcc9 ./cmake/GenerateSkeleton.cmake
acb4b334c54bf339c2b78751114faae468201ad2cd80e51654d7f5b093558e7a ./cmake/GenerateVmlinux.cmake
94ea35bcda0aa6c4e1140f7ae0b7fd250849c60e7bacee401ed16a876a363f74 ./cmake/StripOuterQuotes.cmake

View file

@ -439,9 +439,9 @@ static __always_inline void copy_map_string(char *destination,
if (!destination || !source || size == 0)
return;
if (size == RG_FILENAME_LEN)
__builtin_memcpy(destination, source, RG_FILENAME_LEN);
__builtin_memcpy_inline(destination, source, RG_FILENAME_LEN);
else if (size == RG_COMM_LEN)
__builtin_memcpy(destination, source, RG_COMM_LEN);
__builtin_memcpy_inline(destination, source, RG_COMM_LEN);
}
static __always_inline void capture_exec_context(struct linux_binprm *bprm,
@ -549,7 +549,7 @@ static __always_inline bool consume_pending_path(
return false;
}
__builtin_memcpy(destination, pending->path, RG_FILENAME_LEN);
__builtin_memcpy_inline(destination, pending->path, RG_FILENAME_LEN);
*path_resolution = RG_PATH_LSM_EXACT;
bpf_map_delete_elem(&pending_paths, &ids);
return true;

View file

@ -26,18 +26,16 @@ function(bastionguard_enable_rootguard)
if(NOT RG_BINARY_DIR)
set(RG_BINARY_DIR "${CMAKE_BINARY_DIR}/bastionguard-rootguard-build")
endif()
# An explicit INIT_SYSTEM argument wins. If the parent passes the neutral
# value "auto", allow distro packaging to override it with
# -DROOTGUARD_INIT_SYSTEM=<init>. Fall back to auto only when neither was
# selected. This makes package builds deterministic inside chroots and
# containers, where PID 1 may not match the target distribution's init.
if(RG_INIT_SYSTEM AND NOT RG_INIT_SYSTEM STREQUAL "auto")
# Keep the explicit function argument.
elseif(DEFINED ROOTGUARD_INIT_SYSTEM AND
if(NOT RG_INIT_SYSTEM)
# Preserve an init system explicitly selected by the parent project or
# distribution packaging (for example -DROOTGUARD_INIT_SYSTEM=systemd).
# Fall back to auto-detection only when no explicit value was supplied.
if(DEFINED ROOTGUARD_INIT_SYSTEM AND
NOT "${ROOTGUARD_INIT_SYSTEM}" STREQUAL "")
set(RG_INIT_SYSTEM "${ROOTGUARD_INIT_SYSTEM}")
else()
set(RG_INIT_SYSTEM auto)
set(RG_INIT_SYSTEM "${ROOTGUARD_INIT_SYSTEM}")
else()
set(RG_INIT_SYSTEM auto)
endif()
endif()
if(NOT RG_BTF)
set(RG_BTF /sys/kernel/btf/vmlinux)

0
src/rootguard/packaging/openrc/bastionguard-rootguard Executable file → Normal file
View file

View file

0
src/rootguard/scripts/bastionguard-rootguard-service Executable file → Normal file
View file

0
src/rootguard/scripts/check-environment.sh Executable file → Normal file
View file

0
src/rootguard/scripts/enable-service.sh Executable file → Normal file
View file

0
src/rootguard/scripts/prepare-metadata-probe.sh Executable file → Normal file
View file

0
src/rootguard/scripts/prepare-vm-probe.sh Executable file → Normal file
View file

View file

@ -18,6 +18,7 @@
* The BastionGuard™ name and branding are not licensed under the GPL.
*/
#include "../SafeProcess.hpp"
#include <iostream>
#include <fstream>
#include <vector>
@ -256,104 +257,50 @@ static bool is_real_desktop_user(const std::string& user)
static std::string detect_desktop_user_logind()
{
if (access("/usr/bin/loginctl", X_OK) != 0 &&
access("/bin/loginctl", X_OK) != 0)
if (access("/usr/bin/loginctl", X_OK) != 0 && access("/bin/loginctl", X_OK) != 0)
return "";
FILE* p = popen("loginctl list-sessions --no-legend", "r");
if (!p)
return "";
char buf[512];
while (fgets(buf, sizeof(buf), p)) {
std::string line(buf);
const auto sessions = bg_process::run({"loginctl", "list-sessions", "--no-legend"});
if (!sessions.ok()) return "";
std::istringstream all(sessions.output);
std::string line;
while (std::getline(all, line)) {
std::istringstream iss(line);
std::string session_id;
std::string uid;
std::string user;
std::string seat;
std::string tty;
std::string session_id, uid, user, seat, tty;
iss >> session_id >> uid >> user >> seat >> tty;
if (session_id.empty() || user.empty() ||
!is_real_desktop_user(user))
if (session_id.empty() || user.empty() || !is_real_desktop_user(user))
continue;
std::string cmd =
"loginctl show-session " + session_id +
" --property=State --property=Type --property=Class --property=Remote";
FILE* pp = popen(cmd.c_str(), "r");
if (!pp)
continue;
std::string state;
std::string type;
std::string cls;
std::string remote;
while (fgets(buf, sizeof(buf), pp)) {
std::string prop(buf);
prop.erase(std::remove(prop.begin(), prop.end(), '\n'), prop.end());
if (prop.rfind("State=", 0) == 0)
state = prop.substr(6);
else if (prop.rfind("Type=", 0) == 0)
type = prop.substr(5);
else if (prop.rfind("Class=", 0) == 0)
cls = prop.substr(6);
else if (prop.rfind("Remote=", 0) == 0)
remote = prop.substr(7);
}
pclose(pp);
const bool active = (state == "active");
const bool graphical = (type == "wayland" || type == "x11");
const bool user_session = (cls == "user");
const bool local = (remote != "yes");
if (active && graphical && user_session && local) {
pclose(p);
return user;
// Session ID is a single argv entry: metacharacters cannot execute code.
const auto details = bg_process::run({"loginctl", "show-session", session_id,
"--property=State", "--property=Type", "--property=Class", "--property=Remote"});
if (!details.ok()) continue;
std::string state, type, cls, remote, prop;
std::istringstream properties(details.output);
while (std::getline(properties, prop)) {
if (prop.rfind("State=", 0) == 0) state = prop.substr(6);
else if (prop.rfind("Type=", 0) == 0) type = prop.substr(5);
else if (prop.rfind("Class=", 0) == 0) cls = prop.substr(6);
else if (prop.rfind("Remote=", 0) == 0) remote = prop.substr(7);
}
if (state == "active" && (type == "wayland" || type == "x11") &&
cls == "user" && remote != "yes") return user;
}
pclose(p);
return "";
}
static std::string detect_desktop_user_who()
{
FILE* p = popen("who", "r");
if (!p)
return "";
char buf[512];
while (fgets(buf, sizeof(buf), p)) {
std::string line(buf);
const auto who = bg_process::run({"who"});
if (!who.ok()) return "";
std::istringstream stream(who.output);
std::string line;
while (std::getline(stream, line)) {
std::istringstream iss(line);
std::string user;
std::string tty;
std::string user, tty;
iss >> user >> tty;
if (!is_real_desktop_user(user))
continue;
const bool graphical =
line.find("(:") != std::string::npos ||
tty == "tty7" || tty == "tty1";
if (graphical) {
pclose(p);
if (!is_real_desktop_user(user)) continue;
if (line.find("(:") != std::string::npos || tty == "tty7" || tty == "tty1")
return user;
}
}
pclose(p);
return "";
}

View file

@ -18,6 +18,7 @@
* The BastionGuard™ name and branding are not licensed under the GPL.
*/
#include "../SafeProcess.hpp"
#include "Resource.hpp"
#include <iostream>
#include <filesystem>
@ -894,8 +895,7 @@ int main(int argc, char* argv[]) {
setpriority(PRIO_PROCESS, 0, 19);
{
pid_t pid = getpid();
std::string cmd = "ionice -c3 -p " + std::to_string(pid) + " 2>/dev/null";
system(cmd.c_str());
(void)bg_process::run({"ionice", "-c3", "-p", std::to_string(pid)});
}
if (yr_initialize() != ERROR_SUCCESS) {

View file

@ -20,6 +20,7 @@
#include "ArchiveSandbox.hpp"
#include <algorithm>
#include <array>
#include <cerrno>
#include <chrono>
@ -46,7 +47,8 @@ namespace {
// Legge tutto quello che è disponibile subito su fd (non-blocking).
// Ritorna false solo su errore reale (non su EAGAIN/EWOULDBLOCK/EINTR).
// Ritorna true anche in caso di EOF (n == 0), segnalando chiusura via got_eof.
bool drain_fd_nonblocking(int fd, std::string& out, bool& got_eof) {
bool drain_fd_nonblocking(int fd, std::string& out, bool& got_eof, bool& truncated) {
constexpr std::size_t max_output_bytes = 1024 * 1024;
got_eof = false;
std::array<char, 4096> buf{};
while (true) {
@ -60,7 +62,14 @@ bool drain_fd_nonblocking(int fd, std::string& out, bool& got_eof) {
if (errno == EAGAIN || errno == EWOULDBLOCK) return true;
return false;
}
out.append(buf.data(), static_cast<std::size_t>(n));
const auto length = static_cast<std::size_t>(n);
if (out.size() < max_output_bytes) {
const auto copy = std::min(length, max_output_bytes - out.size());
out.append(buf.data(), copy);
if (copy != length) truncated = true;
} else {
truncated = true;
}
}
}
@ -198,6 +207,9 @@ SandboxExecResult run_archive_worker_bwrap(
if (::pipe(out_pipe) != 0 || ::pipe(err_pipe) != 0) {
result.stderr_text = "pipe failed";
for (int fd : {out_pipe[0], out_pipe[1], err_pipe[0], err_pipe[1]}) {
if (fd >= 0) ::close(fd);
}
fs::remove_all(sandbox_root, ec);
return result;
}
@ -280,26 +292,37 @@ SandboxExecResult run_archive_worker_bwrap(
bool out_eof = false;
bool err_eof = false;
const int effective_timeout = std::max(1, timeout_seconds);
while (true) {
// Se il child è già uscito E entrambe le pipe sono in EOF,
// abbiamo finito.
const pid_t w = ::waitpid(pid, &status, WNOHANG);
if (w == pid) {
child_done = true;
} else if (w < 0 && errno != EINTR) {
result.stderr_text = "waitpid failed";
break;
if (!child_done) {
const pid_t w = ::waitpid(pid, &status, WNOHANG);
if (w == pid) {
child_done = true;
} else if (w < 0 && errno != EINTR) {
result.stderr_text = "waitpid failed";
// Do not exit leaving an untracked child behind.
::kill(pid, SIGKILL);
::waitpid(pid, &status, 0);
break;
}
}
// Timeout: SIGKILL e reap sincrono.
const auto now = std::chrono::steady_clock::now();
const auto elapsed =
std::chrono::duration_cast<std::chrono::seconds>(now - start).count();
if (!child_done && elapsed >= timeout_seconds) {
if (elapsed >= effective_timeout && (!child_done || !out_eof || !err_eof)) {
// A forked grandchild may still own an output pipe after its
// direct parent exits. Bound the entire operation, not only waitpid.
result.timed_out = true;
::kill(pid, SIGKILL);
::waitpid(pid, &status, 0);
child_done = true;
if (!child_done) {
::kill(pid, SIGKILL);
::waitpid(pid, &status, 0);
child_done = true;
}
break;
}
// Drena le pipe disponibili (non-blocking).
@ -321,10 +344,10 @@ SandboxExecResult run_archive_worker_bwrap(
bool eof_here = false;
if (fds[i].fd == out_pipe[0]) {
drain_fd_nonblocking(out_pipe[0], result.stdout_text, eof_here);
drain_fd_nonblocking(out_pipe[0], result.stdout_text, eof_here, result.output_truncated);
if (eof_here) out_eof = true;
} else if (fds[i].fd == err_pipe[0]) {
drain_fd_nonblocking(err_pipe[0], result.stderr_text, eof_here);
drain_fd_nonblocking(err_pipe[0], result.stderr_text, eof_here, result.output_truncated);
if (eof_here) err_eof = true;
}
}
@ -345,8 +368,8 @@ SandboxExecResult run_archive_worker_bwrap(
}
// Drena residui (le EOF dovrebbero essere già arrivate, ma per sicurezza).
if (!out_eof) { bool e; drain_fd_nonblocking(out_pipe[0], result.stdout_text, e); }
if (!err_eof) { bool e; drain_fd_nonblocking(err_pipe[0], result.stderr_text, e); }
if (!out_eof) { bool e; drain_fd_nonblocking(out_pipe[0], result.stdout_text, e, result.output_truncated); }
if (!err_eof) { bool e; drain_fd_nonblocking(err_pipe[0], result.stderr_text, e, result.output_truncated); }
::close(out_pipe[0]);
::close(err_pipe[0]);
@ -381,6 +404,14 @@ ArchiveInspectionResult analyze_archive_in_sandbox(
return r;
}
if (exec.output_truncated) {
r.success = false;
r.sandbox_failed = true;
r.reason_code = "ARCHIVE_SANDBOX_OUTPUT_LIMIT";
r.detail = "archive worker exceeded output limit";
return r;
}
if (exec.timed_out) {
r.success = false;
r.sandbox_timeout = true;

View file

@ -27,6 +27,7 @@ struct SandboxExecResult {
bool launched = false;
bool exited = false;
bool timed_out = false;
bool output_truncated = false;
int exit_code = -1;
std::string stdout_text;

View file

@ -15,6 +15,8 @@
#include <fstream>
#include <sstream>
#include <filesystem>
#include <regex>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
@ -146,6 +148,121 @@ static std::vector<std::string> split_csv_(const std::string& s) {
return out;
}
static bool parse_uint_range_(const std::string& s, unsigned min, unsigned max) {
if (s.empty() || s.find_first_not_of("0123456789") != std::string::npos)
return false;
try {
unsigned long v = std::stoul(s);
return v >= min && v <= max;
} catch (...) {
return false;
}
}
static bool valid_wg_key_(const std::string& s) {
// WireGuard keys are 32 bytes encoded as 44-character base64 strings.
static const std::regex re(R"(^[A-Za-z0-9+/]{43}=$)");
return std::regex_match(s, re);
}
static bool valid_ip_(const std::string& s) {
struct in_addr a4 {};
struct in6_addr a6 {};
return ::inet_pton(AF_INET, s.c_str(), &a4) == 1 ||
::inet_pton(AF_INET6, s.c_str(), &a6) == 1;
}
static bool valid_cidr_(const std::string& s) {
const auto slash = s.rfind('/');
if (slash == std::string::npos || slash == 0 || slash + 1 >= s.size())
return false;
const std::string ip = s.substr(0, slash);
const std::string prefix = s.substr(slash + 1);
if (!parse_uint_range_(prefix, 0, 128)) return false;
struct in_addr a4 {};
if (::inet_pton(AF_INET, ip.c_str(), &a4) == 1)
return std::stoul(prefix) <= 32;
struct in6_addr a6 {};
if (::inet_pton(AF_INET6, ip.c_str(), &a6) == 1)
return std::stoul(prefix) <= 128;
return false;
}
static bool valid_hostname_(const std::string& host) {
if (host.empty() || host.size() > 253 || host.front() == '.' || host.back() == '.')
return false;
static const std::regex re(R"(^[A-Za-z0-9.-]+$)");
if (!std::regex_match(host, re) || host.find("..") != std::string::npos)
return false;
std::istringstream labels(host);
std::string label;
while (std::getline(labels, label, '.')) {
if (label.empty() || label.size() > 63 || label.front() == '-' || label.back() == '-')
return false;
}
return true;
}
static bool valid_endpoint_(const std::string& endpoint) {
std::string host, port;
if (!endpoint.empty() && endpoint.front() == '[') {
const auto close = endpoint.find(']');
if (close == std::string::npos || close + 1 >= endpoint.size() || endpoint[close + 1] != ':')
return false;
host = endpoint.substr(1, close - 1);
port = endpoint.substr(close + 2);
struct in6_addr a6 {};
if (::inet_pton(AF_INET6, host.c_str(), &a6) != 1) return false;
} else {
const auto colon = endpoint.rfind(':');
if (colon == std::string::npos || colon == 0 || colon + 1 >= endpoint.size())
return false;
host = endpoint.substr(0, colon);
port = endpoint.substr(colon + 1);
struct in_addr a4 {};
if (::inet_pton(AF_INET, host.c_str(), &a4) != 1 && !valid_hostname_(host))
return false;
}
return parse_uint_range_(port, 1, 65535);
}
static bool valid_iface_(const std::string& iface) {
static const std::regex re(R"(^[A-Za-z0-9_.-]{1,15}$)");
return std::regex_match(iface, re);
}
static bool validate_spec_(const WgTunnelSpec& sp, std::string* err) {
auto fail = [&](const char* msg) {
if (err) *err = msg;
return false;
};
if (!sp.valid()) return fail("Configurazione WireGuard incompleta.");
if (!valid_wg_key_(sp.private_key)) return fail("PrivateKey WireGuard non valida.");
if (!sp.listen_port.empty() && !parse_uint_range_(sp.listen_port, 1, 65535))
return fail("ListenPort non valida.");
if (sp.mtu != 0 && (sp.mtu < 576 || sp.mtu > 9000))
return fail("MTU fuori intervallo consentito.");
for (const auto& a : sp.addresses)
if (!valid_cidr_(a)) return fail("Address deve essere un IP/CIDR valido.");
for (const auto& ns : sp.dns)
if (!valid_ip_(ns)) return fail("DNS deve essere un indirizzo IP valido.");
for (const auto& peer : sp.peers) {
if (!valid_wg_key_(peer.public_key)) return fail("PublicKey peer non valida.");
if (!peer.preshared_key.empty() && !valid_wg_key_(peer.preshared_key))
return fail("PresharedKey non valida.");
if (!valid_endpoint_(peer.endpoint)) return fail("Endpoint WireGuard non valido.");
for (const auto& cidr : split_csv_(peer.allowed_ips))
if (!valid_cidr_(cidr)) return fail("AllowedIPs contiene un CIDR non valido.");
if (!peer.persistent_keepalive.empty() &&
!parse_uint_range_(peer.persistent_keepalive, 0, 65535))
return fail("PersistentKeepalive non valido.");
}
return true;
}
bool WireguardBackend::load_from_conf(const std::string& conf_path,
std::string* err) {
std::ifstream in(conf_path);
@ -202,10 +319,7 @@ bool WireguardBackend::load_from_conf(const std::string& conf_path,
}
flush_peer();
if (!sp.valid()) {
if (err) *err = "Conf incompleto: servono PrivateKey, Address e almeno un Peer con PublicKey+Endpoint.";
return false;
}
if (!validate_spec_(sp, err)) return false;
spec_ = std::move(sp);
return true;
}
@ -219,7 +333,7 @@ bool WireguardBackend::load_from_params(const WgConfigParams& p,
}
WgTunnelSpec sp;
sp.private_key = p.private_key;
sp.addresses = { p.address };
sp.addresses = split_csv_(p.address);
if (!p.dns.empty()) sp.dns = split_csv_(p.dns);
sp.listen_port = p.listen_port;
@ -231,7 +345,7 @@ bool WireguardBackend::load_from_params(const WgConfigParams& p,
peer.persistent_keepalive = p.persistent_keepalive;
sp.peers.push_back(peer);
if (!sp.valid()) { if (err) *err = "Spec non valida."; return false; }
if (!validate_spec_(sp, err)) return false;
spec_ = std::move(sp);
return true;
}
@ -289,110 +403,113 @@ static bool needs_default_route_(const WgTunnelSpec& sp) {
// ===========================================================================
OpResult WireguardBackend::connect() {
OpResult res;
if (!spec_.valid()) {
std::string validation_error;
if (!validate_spec_(spec_, &validation_error) || !valid_iface_(iface_)) {
res.ok = false;
res.output = "Nessuna configurazione caricata: importa un .conf o "
"compila il form Configurazione.";
res.exit_code = -1;
res.output = validation_error.empty() ? "Interfaccia WireGuard non valida." : validation_error;
return res;
}
// Genera l'input per `wg setconf` (solo cose comprese da `wg`).
auto fail = [](const std::string& step, const OpResult& r) {
OpResult out = r;
out.ok = false;
out.output = step + (r.output.empty() ? std::string{} : ": " + r.output);
return out;
};
// Cleanup residue from an interrupted previous connection. Errors here are harmless.
run({"ip", "link", "delete", "dev", iface_}, true);
OpResult r = run({"ip", "link", "add", "dev", iface_, "type", "wireguard"}, true);
if (!r.ok) return fail("ip link add", r);
const std::string wg_text = build_wg_setconf_text();
r = run_with_stdin({"wg", "setconf", iface_, "/dev/stdin"}, wg_text, true);
if (!r.ok) {
run({"ip", "link", "delete", "dev", iface_}, true);
return fail("wg setconf", r);
}
if (spec_.mtu > 0) {
r = run({"ip", "link", "set", "dev", iface_, "mtu", std::to_string(spec_.mtu)}, true);
if (!r.ok) return fail("ip link mtu", r);
}
// Backup di /etc/resolv.conf (preservando symlink) PRIMA di toccarlo.
// Poi scrittura dei nameserver se ne abbiamo. Tutto in uno script che
// costruiamo qui in memoria e passiamo a `sh` via stdin.
std::ostringstream script;
script << "#!/bin/sh\n"
<< "set -e\n"
<< "IFACE=" << iface_ << "\n"
// 1. Cleanup eventuale residuo
<< "ip link delete dev \"$IFACE\" 2>/dev/null || true\n"
// 2. Crea l'interfaccia wireguard
<< "ip link add dev \"$IFACE\" type wireguard\n"
// 3. Applica la config (privkey + peers) leggendola da FD 3
<< "wg setconf \"$IFACE\" /dev/fd/3 3<<'BG_WGCONF'\n"
<< wg_text
<< "BG_WGCONF\n"
// 4. MTU
<< (spec_.mtu > 0 ? ("ip link set dev \"$IFACE\" mtu "
+ std::to_string(spec_.mtu) + "\n")
: std::string{})
// 5. Indirizzi
;
for (const auto& a : spec_.addresses) {
// distinguiamo v4/v6 in modo grezzo (presenza di ':')
const char* fam = (a.find(':') != std::string::npos) ? "-6" : "-4";
script << "ip " << fam << " address add " << a << " dev \"$IFACE\"\n";
const std::string fam = (a.find(':') != std::string::npos) ? "-6" : "-4";
r = run({"ip", fam, "address", "add", a, "dev", iface_}, true);
if (!r.ok) return fail("ip address add", r);
}
// 6. Up
script << "ip link set dev \"$IFACE\" up\n";
// 7. Route. Per evitare di buttare giù la rotta di default principale
// (e creare loop col traffico verso l'endpoint stesso), usiamo lo
// stesso schema di wg-quick con table dedicata + ip rule.
r = run({"ip", "link", "set", "dev", iface_, "up"}, true);
if (!r.ok) return fail("ip link up", r);
if (needs_default_route_(spec_)) {
const char* TBL = "51820";
script << "ip -4 route add default dev \"$IFACE\" table " << TBL << " 2>/dev/null || true\n"
<< "ip -4 rule add not from all fwmark 0x" << TBL
<< " lookup " << TBL << " priority 32764 2>/dev/null || true\n"
<< "ip -4 rule add table main suppress_prefixlength 0 priority 32765 2>/dev/null || true\n"
<< "wg set \"$IFACE\" fwmark 0x" << TBL << "\n"
// attiva il routing simmetrico
<< "sysctl -q net.ipv4.conf.all.src_valid_mark=1 || true\n";
// These may already exist after a previous interrupted run; preserve the
// old `|| true` semantics by intentionally ignoring duplicate errors.
run({"ip", "-4", "route", "add", "default", "dev", iface_, "table", "51820"}, true);
run({"ip", "-4", "rule", "add", "not", "from", "all", "fwmark", "0x51820",
"lookup", "51820", "priority", "32764"}, true);
run({"ip", "-4", "rule", "add", "table", "main", "suppress_prefixlength", "0",
"priority", "32765"}, true);
run({"wg", "set", iface_, "fwmark", "0x51820"}, true);
run({"sysctl", "-q", "net.ipv4.conf.all.src_valid_mark=1"}, true);
} else {
// route specifiche dagli AllowedIPs (escludendo 0.0.0.0/0 / ::/0)
for (const auto& p : spec_.peers)
for (const auto& a : split_csv_(p.allowed_ips)) {
if (a == "0.0.0.0/0" || a == "::/0") continue;
const char* fam = (a.find(':') != std::string::npos) ? "-6" : "-4";
script << "ip " << fam << " route add " << a
<< " dev \"$IFACE\" 2>/dev/null || true\n";
for (const auto& peer : spec_.peers) {
for (const auto& cidr : split_csv_(peer.allowed_ips)) {
if (cidr == "0.0.0.0/0" || cidr == "::/0") continue;
const std::string fam = (cidr.find(':') != std::string::npos) ? "-6" : "-4";
run({"ip", fam, "route", "add", cidr, "dev", iface_}, true);
}
}
}
// 8. DNS: backup di /etc/resolv.conf preservando symlink, poi scrittura
// dei nameserver del tunnel. Atomico (mv su tmp).
if (!spec_.dns.empty()) {
script << "BAK=/etc/resolv.conf.bastionguard.bak\n"
<< "TMP=/etc/resolv.conf.bastionguard.tmp\n"
<< "[ -e \"$BAK\" ] || cp -a --no-dereference /etc/resolv.conf \"$BAK\" 2>/dev/null || true\n"
<< "{\n";
static const std::string bak = "/etc/resolv.conf.bastionguard.bak";
if (!fs::exists(bak)) {
run({"cp", "-a", "--no-dereference", "--", "/etc/resolv.conf", bak}, true);
}
std::ostringstream dns_text;
for (const auto& ns : spec_.dns)
script << " echo nameserver " << ns << "\n";
script << "} > \"$TMP\"\n"
<< "mv -f \"$TMP\" /etc/resolv.conf\n";
dns_text << "nameserver " << ns << "\n";
r = run_with_stdin({"tee", "/etc/resolv.conf"}, dns_text.str(), true);
if (!r.ok) return fail("aggiornamento DNS", r);
}
script << "exit 0\n";
return run_with_stdin({"sh"}, script.str(), /*privileged=*/true);
res.ok = true;
res.exit_code = 0;
res.output = "Tunnel WireGuard attivato.";
return res;
}
// ===========================================================================
// DISCONNECT: ripristina resolv.conf, rimuove route, butta giù interfaccia
// ===========================================================================
OpResult WireguardBackend::disconnect() {
std::ostringstream script;
script << "#!/bin/sh\n"
<< "IFACE=" << iface_ << "\n"
// 1. Ripristina /etc/resolv.conf se c'è un backup
<< "BAK=/etc/resolv.conf.bastionguard.bak\n"
<< "if [ -e \"$BAK\" ]; then\n"
<< " rm -f /etc/resolv.conf\n"
<< " cp -a --no-dereference \"$BAK\" /etc/resolv.conf 2>/dev/null || true\n"
<< " rm -f \"$BAK\"\n"
<< "fi\n"
// 2. Rimuove ip rules / tabella di routing del tunnel
<< "ip -4 rule del table main suppress_prefixlength 0 priority 32765 2>/dev/null || true\n"
<< "ip -4 rule del not from all fwmark 0x51820 lookup 51820 priority 32764 2>/dev/null || true\n"
<< "ip -4 route flush table 51820 2>/dev/null || true\n"
// 3. Butta giù l'interfaccia
<< "ip link delete dev \"$IFACE\" 2>/dev/null || true\n"
// Considera ok se l'interfaccia non c'è più
<< "! ip link show \"$IFACE\" >/dev/null 2>&1\n";
static const std::string bak = "/etc/resolv.conf.bastionguard.bak";
return run_with_stdin({"sh"}, script.str(), /*privileged=*/true);
if (fs::exists(bak)) {
run({"rm", "-f", "--", "/etc/resolv.conf"}, true);
run({"cp", "-a", "--no-dereference", "--", bak, "/etc/resolv.conf"}, true);
run({"rm", "-f", "--", bak}, true);
}
run({"ip", "-4", "rule", "del", "table", "main", "suppress_prefixlength", "0",
"priority", "32765"}, true);
run({"ip", "-4", "rule", "del", "not", "from", "all", "fwmark", "0x51820",
"lookup", "51820", "priority", "32764"}, true);
run({"ip", "-4", "route", "flush", "table", "51820"}, true);
run({"ip", "link", "delete", "dev", iface_}, true);
OpResult check = run({"ip", "link", "show", iface_}, false);
OpResult res;
res.ok = !check.ok;
res.exit_code = res.ok ? 0 : 1;
res.output = res.ok ? "Tunnel WireGuard disattivato." : "Interfaccia WireGuard ancora presente.";
return res;
}
// ===========================================================================
@ -451,39 +568,55 @@ std::optional<std::string> WireguardBackend::public_ip() const {
static const char* kKsTable = "bastionguard_ks";
OpResult WireguardBackend::killswitch_enable() {
if (!valid_iface_(iface_))
return OpResult{false, -1, "Interfaccia WireGuard non valida."};
WgStatus st = status();
std::string allow_endpoint;
std::string endpoint_rule;
if (!st.endpoint.empty()) {
auto pos = st.endpoint.rfind(':');
std::string host = (pos == std::string::npos) ? st.endpoint
: st.endpoint.substr(0, pos);
if (!host.empty())
allow_endpoint = "nft add rule inet " + std::string(kKsTable) +
" out ip daddr " + host + " accept; ";
std::string host;
if (st.endpoint.front() == '[') {
auto close = st.endpoint.find(']');
if (close != std::string::npos) host = st.endpoint.substr(1, close - 1);
} else {
auto pos = st.endpoint.rfind(':');
if (pos != std::string::npos) host = st.endpoint.substr(0, pos);
}
struct in_addr a4 {};
struct in6_addr a6 {};
if (::inet_pton(AF_INET, host.c_str(), &a4) == 1)
endpoint_rule = " ip daddr " + host + " accept\n";
else if (::inet_pton(AF_INET6, host.c_str(), &a6) == 1)
endpoint_rule = " ip6 daddr " + host + " accept\n";
}
std::ostringstream cmd;
cmd << "nft add table inet " << kKsTable << "; "
<< "nft add chain inet " << kKsTable
<< " out '{ type filter hook output priority 0; policy drop; }'; "
<< "nft add rule inet " << kKsTable << " out oifname lo accept; "
<< "nft add rule inet " << kKsTable << " out oifname \"" << iface_
<< "\" accept; "
<< allow_endpoint
<< "nft add rule inet " << kKsTable << " out meta l4proto udp accept;";
return run({"sh", "-c", cmd.str()}, true);
std::ostringstream rules;
rules << "table inet " << kKsTable << " {\n"
<< " chain out {\n"
<< " type filter hook output priority 0; policy drop;\n"
<< " oifname lo accept\n"
<< " oifname \"" << iface_ << "\" accept\n"
<< endpoint_rule
<< " meta l4proto udp accept\n"
<< " }\n"
<< "}\n";
return run_with_stdin({"nft", "-f", "-"}, rules.str(), true);
}
OpResult WireguardBackend::killswitch_disable() {
return run({"sh", "-c",
std::string("nft delete table inet ") + kKsTable +
" 2>/dev/null; true"}, true);
OpResult r = run({"nft", "delete", "table", "inet", kKsTable}, true);
if (!r.ok) {
// An already absent table is equivalent to a disabled kill-switch.
r.ok = true;
r.exit_code = 0;
}
return r;
}
bool WireguardBackend::killswitch_active() const {
OpResult r = run({"sh", "-c",
std::string("nft list table inet ") + kKsTable +
" >/dev/null 2>&1"}, true);
return r.ok;
return run({"nft", "list", "table", "inet", kKsTable}, true).ok;
}
} // namespace BastionGuard

View file

@ -0,0 +1,22 @@
#pragma once
#include <string>
namespace BastionGuard::wizard {
// Quote a single argument for POSIX-compatible shells. In particular, a
// single quote inside the argument must not terminate the quoted string.
// Prefer bg_process::run(argv) where a shell is not actually required.
inline std::string shell_quote(const std::string& argument) {
std::string quoted;
quoted.reserve(argument.size() + 2);
quoted.push_back('\'');
for (const char ch : argument) {
if (ch == '\'') quoted += "'\"'\"'";
else quoted.push_back(ch);
}
quoted.push_back('\'');
return quoted;
}
} // namespace BastionGuard::wizard

View file

@ -20,6 +20,8 @@
#include "wizard_setup.hpp"
#include "shell_quote.hpp"
#include "../SafeProcess.hpp"
#include "resource.hpp"
#include "../DistroUtils.hpp"
#include <glibmm/i18n.h>
@ -48,35 +50,14 @@ namespace {
static bool run_cmd_capture(const std::string& cmd, std::string* out = nullptr, int* exit_code = nullptr) {
std::array<char, 256> buffer;
std::string result;
std::string wrapped = "/bin/sh -c \"" + cmd + "\"";
FILE* pipe = popen(wrapped.c_str(), "r");
if (!pipe) return false;
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr)
result += buffer.data();
int status = pclose(pipe);
int rc = -1;
if (WIFEXITED(status)) {
rc = WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
rc = 128 + WTERMSIG(status);
} else {
rc = status;
}
if (out) *out = result;
if (exit_code) *exit_code = rc;
return (rc == 0);
// The legacy setup command plans contain fixed firewall shell operators.
// Do not add an extra /bin/sh layer or interpolate unquoted user data.
const auto result = bg_process::run({"/bin/sh", "-c", cmd}, true);
if (out) *out = result.output;
if (exit_code) *exit_code = result.exit_code;
return result.ok();
}
static std::string elev_cmd(const std::string& inner,
const std::string& action_id = "org.BastionGuard.maintenance") {
std::ostringstream cmd;
@ -86,13 +67,13 @@ static std::string elev_cmd(const std::string& inner,
if (fs::exists("/usr/bin/pkexec")) {
if (has_action_id) {
cmd << "pkexec --disable-internal-agent --action-id " << action_id
<< " /bin/bash -c '" << inner << "'";
<< " /bin/bash -c " << BastionGuard::wizard::shell_quote(inner);
} else {
cmd << "pkexec /bin/bash -c '" << inner << "'";
cmd << "pkexec /bin/bash -c " << BastionGuard::wizard::shell_quote(inner);
}
} else {
cmd << "sudo /bin/bash -c '" << inner << "'";
cmd << "sudo /bin/bash -c " << BastionGuard::wizard::shell_quote(inner);
}
return cmd.str();
@ -173,26 +154,13 @@ static bool file_exists(const std::string& path) {
}
static std::string run_capture(const std::string& cmd) {
std::array<char, 256> buf;
std::string out;
FILE* p = popen(cmd.c_str(), "r");
if (!p) return {};
while (fgets(buf.data(), buf.size(), p)) {
out += buf.data();
}
int status = pclose(p);
(void)status;
while (!out.empty() && (out.back() == '\n' || out.back() == '\r'))
out.pop_back();
const auto capture = bg_process::run({"/bin/sh", "-c", cmd});
if (!capture.ok()) return {};
std::string out = capture.output;
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
return out;
}
static bool detect_resolvectl() {
if (run_cmd_capture("command -v resolvectl >/dev/null 2>&1")) return true;
return false;
@ -435,15 +403,20 @@ bool setup_firewall_ports() {
if (user && *user) {
std::cout << "[wizard] " << _("Utente effettivo: ") << user << std::endl;
std::ostringstream check;
check << "id -nG " << user << " | grep -E '\\b(adm|systemd-journal)\\b' >/dev/null 2>&1";
int rc_check = std::system(check.str().c_str());
const auto groups = bg_process::run({"id", "-nG", user});
std::istringstream words(groups.output);
std::string group;
bool member = false;
while (words >> group) {
if (group == "adm" || group == "systemd-journal") { member = true; break; }
}
const int rc_check = groups.ok() && member ? 0 : 1;
if (rc_check != 0) {
std::ostringstream cmd;
cmd << "gpasswd -a " << user << " adm; "
cmd << "gpasswd -a " << BastionGuard::wizard::shell_quote(user) << " adm; "
<< "if getent group systemd-journal >/dev/null 2>&1; then "
<< "gpasswd -a " << user << " systemd-journal; fi";
<< "gpasswd -a " << BastionGuard::wizard::shell_quote(user) << " systemd-journal; fi";
std::string out;
int rc = 0;
std::cout << "[wizard] " << _("Aggiungo l'utente ai gruppi di log disponibili...") << std::endl;

View file

@ -19,10 +19,12 @@
*/
#include "../SafeProcess.hpp"
#include "wizard_window.hpp"
#include "wizard_setup.hpp"
#include "../DistroUtils.hpp"
#include "resource.hpp"
#include "shell_quote.hpp"
#include "../Utils.hpp"
#include "StyleProvider.hpp"
#include <glibmm/i18n.h>
@ -40,57 +42,18 @@
static bool g_wizard_root_session_started = false;
namespace fs = std::filesystem;
namespace BastionGuard::wizard {
static std::string shell_quote(const std::string& s) {
if (s.empty()) return "''";
std::string out = "'";
for (char c : s) {
if (c == '\'') out += "'\"'\"'";
else out.push_back(c);
}
out.push_back('\'');
return out;
static std::vector<std::string> elev_args(const std::string& inner) {
if (geteuid() == 0) return {"/bin/bash", "-c", inner};
if (fs::exists("/usr/bin/pkexec")) return {"pkexec", "/bin/bash", "-c", inner};
return {"sudo", "/bin/bash", "-c", inner};
}
static std::string elev_cmd(const std::string& inner,
const std::string& action_id = "org.BastionGuard.maintenance") {
auto shell_q = [](const std::string& s){
std::string out = "'";
for (char c : s) out += (c=='\'' ? "'\"'\"'" : std::string(1,c));
out += "'";
return out;
};
if (geteuid() == 0 || g_wizard_root_session_started)
return "/bin/bash -c " + shell_q(inner);
bool has_pkexec = fs::exists("/usr/bin/pkexec");
bool has_action = false;
if (has_pkexec)
has_action = (std::system("pkexec --help | grep -q action-id") == 0);
std::ostringstream cmd;
if (has_pkexec) {
if (has_action)
cmd << "pkexec --disable-internal-agent --action-id " << action_id
<< " /bin/bash -c " << shell_q(inner);
else
cmd << "pkexec /bin/bash -c " << shell_q(inner);
} else {
cmd << "sudo /bin/bash -c " << shell_q(inner);
}
g_wizard_root_session_started = true;
return cmd.str();
}
static std::string user_data_dir() {
return Glib::build_filename(Glib::get_user_data_dir(), "BastionGuard");
}
@ -175,9 +138,7 @@ bool WizardWindow::run_enable_user_agents() {
append_log(_("Script non trovato: ") + script);
return false;
}
std::string cmd = elev_cmd("/bin/sh \"" + script + "\"", "org.BastionGuard.maintenance");
int rc = std::system(cmd.c_str());
int rc = bg_process::run(elev_args("/bin/sh " + shell_quote(script))).exit_code;
if (rc == 0) {
append_log(_("User agents abilitati con successo."));
return true;
@ -748,8 +709,8 @@ void WizardWindow::start_execution() {
static bool g_auth_done = false;
if (!g_auth_done && geteuid() != 0) {
append_log(_("Richiesta autenticazione amministratore..."));
std::system("pkexec /bin/true");
g_auth_done = true;
g_auth_done = bg_process::run({"pkexec", "/bin/true"}).ok();
if (!g_auth_done) append_log(_("Autenticazione annullata o fallita."));
}
if (m_worker.joinable()) {
@ -939,11 +900,9 @@ bool WizardWindow::configure_dnsmasq() {
sh << " fi\n";
sh << "fi\n";
std::string cmd = elev_cmd("/bin/bash -c " + shell_quote(sh.str()),
"org.BastionGuard.maintenance");
append_log(_("Configuro dnsmasq (conf-dir + BastionGuard.conf)..."));
int rc = std::system(cmd.c_str());
int rc = bg_process::run(elev_args(sh.str()), true).exit_code;
if (rc == 0) {
append_log(_("✅ dnsmasq configurato correttamente."));
return true;
@ -1031,18 +990,18 @@ bool WizardWindow::configure_browser_doh() {
f.close();
return true;
} else {
std::string tmp = "/tmp/BastionGuard_policy_tmp.json";
{
std::ofstream f(tmp);
if (!f.is_open()) return false;
f << content;
}
std::string cmd = elev_cmd("install -m 644 " + shell_quote(tmp) + " " + shell_quote(path) + " && rm -f " + shell_quote(tmp),
"org.BastionGuard.maintenance");
int rc = std::system(cmd.c_str());
return (rc == 0);
char tmpl[] = "/tmp/BastionGuard_policy_XXXXXX";
const int fd = ::mkstemp(tmpl);
if (fd < 0) return false;
const std::string tmp(tmpl);
FILE* f = ::fdopen(fd, "w");
if (!f) { ::close(fd); ::unlink(tmp.c_str()); return false; }
const bool written = ::fwrite(content.data(), 1, content.size(), f) == content.size();
const bool closed = ::fclose(f) == 0;
if (!written || !closed) { ::unlink(tmp.c_str()); return false; }
const bool ok = bg_process::run({"pkexec", "/usr/bin/install", "-D", "-m", "0644", "--", tmp, path}).ok();
::unlink(tmp.c_str());
return ok;
}
};
@ -1398,25 +1357,33 @@ bool WizardWindow::apply_web_config() {
script << "nginx -t && /usr/libexec/bastionguard/bastionguard-service --system restart nginx.service\n";
std::string tmp_script = "/tmp/BastionGuard_apply_nginx.sh";
{
std::ofstream f(tmp_script);
if (!f.is_open()) {
append_log(_("Impossibile creare lo script temporaneo."));
return false;
}
f << script.str();
char tmpl[] = "/tmp/BastionGuard_apply_nginx_XXXXXX";
const int fd = ::mkstemp(tmpl);
if (fd < 0) {
append_log(_("Impossibile creare lo script temporaneo."));
return false;
}
std::filesystem::permissions(tmp_script,
std::filesystem::perms::owner_exec |
std::filesystem::perms::owner_read |
std::filesystem::perms::owner_write);
std::string cmd = elev_cmd("bash " + shell_quote(tmp_script) + " && rm -f " + shell_quote(tmp_script),
"org.BastionGuard.maintenance");
bool ok = (std::system(cmd.c_str()) == 0);
const std::string tmp_script(tmpl);
FILE* f = ::fdopen(fd, "w");
if (!f) {
::close(fd);
::unlink(tmp_script.c_str());
return false;
}
const std::string script_text = script.str();
const bool written = ::fwrite(script_text.data(), 1, script_text.size(), f) == script_text.size();
const bool closed = ::fclose(f) == 0;
if (!written || !closed || ::chmod(tmp_script.c_str(), 0700) != 0) {
::unlink(tmp_script.c_str());
append_log(_("Errore durante la preparazione dello script temporaneo."));
return false;
}
const auto result = (geteuid() == 0)
? bg_process::run({"/bin/bash", tmp_script}, true)
: bg_process::run({fs::exists("/usr/bin/pkexec") ? "pkexec" : "sudo",
"/bin/bash", tmp_script}, true);
::unlink(tmp_script.c_str());
const bool ok = result.ok();
if (!ok) {
append_log(_("Errore durante l’applicazione della configurazione NGINX."));
@ -1433,8 +1400,8 @@ bool WizardWindow::apply_web_config() {
}
bool WizardWindow::is_firewall_active() {
int rc1 = std::system("/usr/libexec/bastionguard/bastionguard-service --system is-active ufw.service");
int rc2 = std::system("/usr/libexec/bastionguard/bastionguard-service --system is-active firewalld.service");
int rc1 = (bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--system", "is-active", "ufw.service"}).exit_code);
int rc2 = (bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--system", "is-active", "firewalld.service"}).exit_code);
return (rc1 == 0 || rc2 == 0);
}
@ -1445,10 +1412,9 @@ bool WizardWindow::download_bank_list_user() {
std::filesystem::create_directories(userDir);
std::string outFile = userDir + "/banks.json";
std::string cmd = "curl -fsSL " + shell_quote(url) + " -o " + shell_quote(outFile);
append_log(_("Scarico la lista delle banche da MISP (solo utente)..."));
int rc = std::system(cmd.c_str());
int rc = bg_process::run({"curl", "-fsSL", url, "-o", outFile}, true).exit_code;
if (rc == 0) {
append_log(_("✅ Lista banche salvata in: ") + outFile);

View file

@ -25,6 +25,8 @@
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/wait.h>
#include <unistd.h>
#include <glibmm/i18n.h>
int main(int argc, char** argv) {
@ -81,21 +83,39 @@ int main(int argc, char** argv) {
return 0;
}
std::string env_prefix =
"BG_OPENER_DEPTH=1 no_proxy='*' NO_PROXY='*' "
"http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= all_proxy= ALL_PROXY= ";
std::cout << _("[BankOpener] 🌍 Apertura browser di sistema: ") << url << std::endl;
std::string cmd;
if (std::system("command -v gio > /dev/null 2>&1") == 0)
cmd = env_prefix + "gio open \"" + url + "\"";
else
cmd = env_prefix + "xdg-open \"" + url + "\"";
const pid_t pid = ::fork();
if (pid < 0) {
std::cerr << _("[BankOpener] ❌ Impossibile avviare il browser di sistema.\n");
return 1;
}
std::cout << _("[BankOpener] 🌍 Apertura browser di sistema: ") << cmd << std::endl;
if (pid == 0) {
// Imposta l'ambiente del solo processo figlio senza passare da una shell.
::setenv("BG_OPENER_DEPTH", "1", 1);
::setenv("no_proxy", "*", 1);
::setenv("NO_PROXY", "*", 1);
static const char* proxy_vars[] = {
"http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"
};
for (const char* name : proxy_vars)
::unsetenv(name);
int sysrc = std::system(cmd.c_str());
if (sysrc != 0)
std::cerr << _("[BankOpener] ⚠️ Errore esecuzione comando: ") << sysrc << std::endl;
const char* gio_argv[] = {"gio", "open", url.c_str(), nullptr};
::execvp("gio", const_cast<char* const*>(gio_argv));
return sysrc;
const char* xdg_argv[] = {"xdg-open", url.c_str(), nullptr};
::execvp("xdg-open", const_cast<char* const*>(xdg_argv));
_exit(127);
}
int status = 0;
if (::waitpid(pid, &status, 0) < 0) {
std::cerr << _("[BankOpener] ⚠️ waitpid fallita per browser di sistema.\n");
return 1;
}
if (WIFEXITED(status))
return WEXITSTATUS(status);
return 1;
}

View file

@ -60,16 +60,7 @@ bool BankOpener::open_if_trusted(const std::string& url) {
}
std::cout << _("[BankOpener] 🌐 Dominio non in lista sicura: ") << domain << "\n";
std::cout << _("[BankOpener] → Apertura nel browser predefinito.\n");
const char* wayland = std::getenv("WAYLAND_DISPLAY");
std::string cmd;
if (wayland && *wayland && std::system("command -v gio > /dev/null 2>&1") == 0)
cmd = "gio open \"" + url + "\"";
else
cmd = "xdg-open \"" + url + "\"";
std::system(cmd.c_str());
std::cout << _("[BankOpener] → Delego l'apertura sicura al chiamante.\n");
return false;
}

View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
find "$ROOT/webui" -type f -name '*.php' -print0 | xargs -0 -n1 php -l >/dev/null
find "$ROOT" \( -name '*.sh' -o -name '*.postinst' -o -name '*.prerm' -o -name '*.postrm' \) -type f -print0 | while IFS= read -r -d '' script; do bash -n "$script"; done
php "$ROOT/tests/test_webui_regressions.php"
BIN="$(mktemp /tmp/bastionguard-sandbox-regression-XXXXXX)"
trap 'rm -f "$BIN"' EXIT
g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror \
"$ROOT/tests/test_archive_sandbox.cpp" \
"$ROOT/src/security/archive/ArchiveSandbox.cpp" \
"$ROOT/src/security/archive/ArchiveInspector.cpp" -o "$BIN"
"$BIN"
# Lightweight gettext header stub for isolated command-argument regression.
# Real application compilation still requires the distribution GTK/GLib headers.
STUB="$(mktemp -d /tmp/bastionguard-glib-stub-XXXXXX)"
trap 'rm -f "$BIN"; rm -rf "$STUB"' EXIT
mkdir -p "$STUB/glib"
printf '#pragma once\n#define _(x) (x)\n' > "$STUB/glib/gi18n.h"
g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror -I"$STUB" \
"$ROOT/tests/test_firewall_args.cpp" \
"$ROOT/src/firewall/FirewallRuleManager.cpp" -o "$BIN"
"$BIN"
echo "firewall argument regressions: OK"
# CA installer must use the vector-argv API for pkexec and sudo, not shell strings.
g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror -I"$STUB" \
"$ROOT/tests/test_ca_installer_args.cpp" -o "$BIN"
"$BIN"
echo "CA installer argv regression: OK"
# GLib-independent shell quoting shared by wizard codepaths.
g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror \
"$ROOT/tests/test_wizard_shell_quote.cpp" -o "$BIN"
"$BIN"
# Shell-free process execution and systemd security regressions.
g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror \
"$ROOT/tests/test_safe_process.cpp" -o "$BIN"
"$BIN"
bash "$ROOT/tests/test_systemd_policy.sh"
echo "All standalone regression tests passed."
bash "$ROOT/tests/test_arch_packaging.sh"
# User-supplied Clang 23 BPF source patch regression.
bash "$ROOT/tests/test_rootguard_clang23_patch.sh"
# Responsible-disclosure source guards for BG-01 through BG-05.
python3 "$ROOT/tests/test_coordinated_disclosure_bg01_bg05.py"

26
tests/test_arch_packaging.sh Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
bash -n "$root/pkgbuild/PKGBUILD"
bash -n "$root/pkgbuild/build-local-package.sh"
grep -Fq 'pkgver=2.0.3' "$root/pkgbuild/PKGBUILD"
grep -Fq 'option(BG_PACKAGING ' "$root/CMakeLists.txt"
grep -Fq 'option(BG_DEBIAN_NO_INSTALL_CODE ' "$root/CMakeLists.txt"
! grep -Fq '"pacman", "-Sy"' "$root/src/Backend.cpp"
# All installer side-effect blocks should use the packaging-aware wrapper.
python3 - "$root/CMakeLists.txt" <<'PY'
from pathlib import Path
import sys
s=Path(sys.argv[1]).read_text()
assert s.count('install(CODE "') == 1, 'only the wrapper itself may call install(CODE)'
assert s.count('bg_install_code("') >= 8
PY
out="$(mktemp -d "${TMPDIR:-/tmp}/bastionguard-test-arch-XXXXXX")"
trap 'rm -rf -- "$out"' EXIT
bash "$root/pkgbuild/build-local-package.sh" "$out" > /dev/null
sha="$(sha256sum -- "$out/BastionGuard-ci.tar.gz")"; sha="${sha%% *}"
grep -Fq "sha256sums=('${sha}')" "$out/PKGBUILD"
tar -tf "$out/BastionGuard-ci.tar.gz" > "$out/archive-contents.txt"
grep -Fxq 'BastionGuard-ci/pkgbuild/PKGBUILD' "$out/archive-contents.txt"
grep -Fxq 'BastionGuard-ci/pkgbuild/patches/rootguard-clang23-memcpy-inline.patch' "$out/archive-contents.txt"
echo 'Arch local PKGBUILD/archive/packaging isolation: OK'

Some files were not shown because too many files have changed in this diff Show more