umer-wasim commited on
Commit
3ff38ef
·
verified ·
1 Parent(s): 9c7dfa9

Arx 0.5.3: Fix problems on Linux (fix.sh with undo), nftables detection

Browse files
README.md CHANGED
@@ -177,7 +177,7 @@ folder keeps its old name since the rename to Arx in 0.4.3). Reports: *Documents
177
  (add `DEVSEIS_SMOKE_MODEL=1` to include the model). The installer background is `app/build/dmg-background.html`,
178
  rendered to `dmg-background.png` / `@2x.png`.
179
 
180
- ## Fix problems (macOS, app 0.5.0)
181
 
182
  After the audit, the app offers **Fix** for problems it can change safely: firewall, stealth mode, Gatekeeper, malware
183
  definitions, installing security responses and updates automatically, guest account, analytics sharing and network time
@@ -187,6 +187,7 @@ fixes and the collector runs again, so the result is re-checked. **Undo** runs `
187
  ids marked `auto` in `fixes.json` ever reach the script; settings set by a configuration profile (MDM) are skipped.
188
  Changes that need the user (screen lock, FileVault, updates that restart, sharing, Time Machine, accounts) are kind
189
  `settings`: the app opens the exact System Settings page with the steps. The AI never writes or runs commands.
 
190
  Test without changing anything: `DEVSEIS_SMOKE=<folder> DEVSEIS_FIX_DRY=1 app/node_modules/.bin/electron app`.
191
 
192
  **Model per build** (`app/model-version.json`): `version` (v0.3) for the full-precision build on graphics chips with
 
177
  (add `DEVSEIS_SMOKE_MODEL=1` to include the model). The installer background is `app/build/dmg-background.html`,
178
  rendered to `dmg-background.png` / `@2x.png`.
179
 
180
+ ## Fix problems (macOS since 0.5.0, Linux since 0.5.3)
181
 
182
  After the audit, the app offers **Fix** for problems it can change safely: firewall, stealth mode, Gatekeeper, malware
183
  definitions, installing security responses and updates automatically, guest account, analytics sharing and network time
 
187
  ids marked `auto` in `fixes.json` ever reach the script; settings set by a configuration profile (MDM) are skipped.
188
  Changes that need the user (screen lock, FileVault, updates that restart, sharing, Time Machine, accounts) are kind
189
  `settings`: the app opens the exact System Settings page with the steps. The AI never writes or runs commands.
190
+ **Linux** (`collectors/linux/fix.sh`, through the desktop's pkexec prompt): firewall on with incoming blocked (ufw, or firewalld; SSH allowed first if it runs), automatic security updates (unattended-upgrades via an Arx apt drop-in, or dnf-automatic), network time, guest login off, popularity-contest/whoopsie off, automatic login off (GDM, LightDM, SDDM; edited files are backed up next to the state file), and ClamAV definition updates (offered only where ClamAV runs: `requires` in `fixes.json`). Tested in Ubuntu 24.04 containers: fix, re-audit and undo restore every file and service; the packaged app runs headless with `xvfb-run`.
191
  Test without changing anything: `DEVSEIS_SMOKE=<folder> DEVSEIS_FIX_DRY=1 app/node_modules/.bin/electron app`.
192
 
193
  **Model per build** (`app/model-version.json`): `version` (v0.3) for the full-precision build on graphics chips with
app/main.cjs CHANGED
@@ -147,7 +147,7 @@ function autoFixIds(ids) {
147
  const fixes = readJson('library', 'fixes.json').fixes;
148
  return [...new Set(ids)].filter(id => typeof id === 'string' && fixes[id] && fixes[id][OS_FAMILY] && fixes[id][OS_FAMILY].kind === 'auto');
149
  }
150
- const FIX_SCRIPT = { macos: 'fix.sh' };
151
  // Test only (smoke test): the fix script prints what it would change and changes nothing; no administrator prompt.
152
  const FIX_DRY = !!(process.env.DEVSEIS_SMOKE && process.env.DEVSEIS_FIX_DRY);
153
  const fixHistoryFile = () => path.join(app.getPath('userData'), 'fix-history.json');
 
147
  const fixes = readJson('library', 'fixes.json').fixes;
148
  return [...new Set(ids)].filter(id => typeof id === 'string' && fixes[id] && fixes[id][OS_FAMILY] && fixes[id][OS_FAMILY].kind === 'auto');
149
  }
150
+ const FIX_SCRIPT = { macos: 'fix.sh', linux: 'fix.sh' };
151
  // Test only (smoke test): the fix script prints what it would change and changes nothing; no administrator prompt.
152
  const FIX_DRY = !!(process.env.DEVSEIS_SMOKE && process.env.DEVSEIS_FIX_DRY);
153
  const fixHistoryFile = () => path.join(app.getPath('userData'), 'fix-history.json');
app/package.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "name": "devseis-endpoint-auditor",
3
  "productName": "Arx",
4
- "version": "0.5.2",
5
  "description": "Read-only endpoint audit for ISO 27001, GDPR and the EU AI Act, with a local AI auditor.",
6
  "author": {
7
  "name": "Devseis",
 
1
  {
2
  "name": "devseis-endpoint-auditor",
3
  "productName": "Arx",
4
+ "version": "0.5.3",
5
  "description": "Read-only endpoint audit for ISO 27001, GDPR and the EU AI Act, with a local AI auditor.",
6
  "author": {
7
  "name": "Devseis",
app/renderer/app.js CHANGED
@@ -184,11 +184,13 @@ async function renderFixes(evidence, fixResults) {
184
  const name = id => (evidence.results.find(r => r.id === id) || {}).name || id;
185
  if (fixResults && fixResults.length) {
186
  $('fix-banner').innerHTML = `<b>${fixResults.some(r => r.outcome === 'undone') ? 'Undo finished' : 'Fixes finished'}</b><ul class="fix-results">${fixResults.map(r =>
187
- `<li>${OUTCOME[r.outcome] || '•'} <b>${escapeHtml(name(r.id))}</b>: ${escapeHtml(r.message)}</li>`).join('')}</ul><p class="muted">The results below come from checking this Mac again after the change.</p>`;
188
  $('fix-banner').classList.remove('hidden');
189
  } else $('fix-banner').classList.add('hidden');
190
 
191
- const gaps = evidence.results.filter(r => r.status === 'Non-Compliant' && fixes[r.id] && fixes[r.id][os]);
 
 
192
  const auto = gaps.filter(r => fixes[r.id][os].kind === 'auto');
193
  // Several checks can share one settings page and the same steps (screen lock): show them once.
194
  const seen = new Set();
@@ -204,9 +206,9 @@ async function renderFixes(evidence, fixResults) {
204
  <div class="fix-auto">${auto.map(r => { const f = fixes[r.id][os]; return `<label class="fix-item"><input type="checkbox" data-fix="${r.id}" checked>
205
  <span><b>${escapeHtml(f.title)}</b><br><span class="muted">${escapeHtml(f.changes)}${f.note ? ` ${escapeHtml(f.note)}` : ''}</span></span></label>`; }).join('')}</div>
206
  <div class="actions"><button class="primary" id="fix-selected">Fix selected (${auto.length})</button></div>` : ''}
207
- ${guided.length ? `<h3>Fix in System Settings</h3><p class="muted">These need your password, a recovery key or a restart, so you make the change. Arx opens the right page.</p>
208
  <div class="fix-guided">${guided.map(r => { const f = fixes[r.id][os]; return `<div class="fix-item"><span><b>${escapeHtml(f.title)}</b>
209
- <ol>${f.steps.map(x => `<li>${escapeHtml(x)}</li>`).join('')}</ol></span><button class="secondary" data-settings="${r.id}">Open Settings</button></div>`; }).join('')}</div>
210
  <p class="muted">When you are done, choose <b>Run another audit</b> to check again.</p>` : ''}
211
  ${undoable.length ? `<div class="undo-row"><span class="muted">Last fixes (${new Date(last.at).toLocaleString()}): ${undoable.map(id => escapeHtml(fixes[id][os].title)).join(', ')}</span>
212
  <button class="secondary" id="fix-undo">Undo last fixes</button></div>` : ''}`;
@@ -225,9 +227,10 @@ function confirmFix(action, ids) {
225
  const os = plan.os;
226
  pendingFix = { action, ids };
227
  $('fix-title').textContent = action === 'undo' ? 'Undo these fixes?' : `Fix ${ids.length} problem${ids.length > 1 ? 's' : ''}?`;
228
- $('fix-intro').textContent = action === 'undo' ? 'Arx will put these settings back as they were before, then check this Mac again.'
229
- : 'Arx will change these settings, then check this Mac again. You can undo each fix afterwards.';
230
  $('fix-confirm-list').innerHTML = ids.map(id => { const f = fixes[id][os]; return `<li><b>${escapeHtml(f.title)}</b><br><span class="muted">${escapeHtml(action === 'undo' ? 'Back to the previous setting.' : f.changes)}</span></li>`; }).join('');
 
231
  $('fix-modal').classList.remove('hidden');
232
  }
233
  $('fix-cancel').addEventListener('click', () => { pendingFix = null; $('fix-modal').classList.add('hidden'); });
 
184
  const name = id => (evidence.results.find(r => r.id === id) || {}).name || id;
185
  if (fixResults && fixResults.length) {
186
  $('fix-banner').innerHTML = `<b>${fixResults.some(r => r.outcome === 'undone') ? 'Undo finished' : 'Fixes finished'}</b><ul class="fix-results">${fixResults.map(r =>
187
+ `<li>${OUTCOME[r.outcome] || '•'} <b>${escapeHtml(name(r.id))}</b>: ${escapeHtml(r.message)}</li>`).join('')}</ul><p class="muted">The results below come from checking this computer again after the change.</p>`;
188
  $('fix-banner').classList.remove('hidden');
189
  } else $('fix-banner').classList.add('hidden');
190
 
191
+ // A fix can depend on software being present (e.g. ClamAV updates only where ClamAV runs): requires.check's result must contain requires.contains.
192
+ const usable = f => !f.requires || ((evidence.results.find(x => x.id === f.requires.check) || {}).actual || '').toLowerCase().includes(f.requires.contains.toLowerCase());
193
+ const gaps = evidence.results.filter(r => r.status === 'Non-Compliant' && fixes[r.id] && fixes[r.id][os] && usable(fixes[r.id][os]));
194
  const auto = gaps.filter(r => fixes[r.id][os].kind === 'auto');
195
  // Several checks can share one settings page and the same steps (screen lock): show them once.
196
  const seen = new Set();
 
206
  <div class="fix-auto">${auto.map(r => { const f = fixes[r.id][os]; return `<label class="fix-item"><input type="checkbox" data-fix="${r.id}" checked>
207
  <span><b>${escapeHtml(f.title)}</b><br><span class="muted">${escapeHtml(f.changes)}${f.note ? ` ${escapeHtml(f.note)}` : ''}</span></span></label>`; }).join('')}</div>
208
  <div class="actions"><button class="primary" id="fix-selected">Fix selected (${auto.length})</button></div>` : ''}
209
+ ${guided.length ? `<h3>${os === 'macos' ? 'Fix in System Settings' : 'Fix in Settings'}</h3><p class="muted">These need your password, a recovery key or a restart, so you make the change. Arx opens the right page.</p>
210
  <div class="fix-guided">${guided.map(r => { const f = fixes[r.id][os]; return `<div class="fix-item"><span><b>${escapeHtml(f.title)}</b>
211
+ <ol>${f.steps.map(x => `<li>${escapeHtml(x)}</li>`).join('')}</ol></span>${f.url ? `<button class="secondary" data-settings="${r.id}">Open Settings</button>` : ''}</div>`; }).join('')}</div>
212
  <p class="muted">When you are done, choose <b>Run another audit</b> to check again.</p>` : ''}
213
  ${undoable.length ? `<div class="undo-row"><span class="muted">Last fixes (${new Date(last.at).toLocaleString()}): ${undoable.map(id => escapeHtml(fixes[id][os].title)).join(', ')}</span>
214
  <button class="secondary" id="fix-undo">Undo last fixes</button></div>` : ''}`;
 
227
  const os = plan.os;
228
  pendingFix = { action, ids };
229
  $('fix-title').textContent = action === 'undo' ? 'Undo these fixes?' : `Fix ${ids.length} problem${ids.length > 1 ? 's' : ''}?`;
230
+ $('fix-intro').textContent = action === 'undo' ? 'Arx will put these settings back as they were before, then check this computer again.'
231
+ : 'Arx will change these settings, then check this computer again. You can undo each fix afterwards.';
232
  $('fix-confirm-list').innerHTML = ids.map(id => { const f = fixes[id][os]; return `<li><b>${escapeHtml(f.title)}</b><br><span class="muted">${escapeHtml(action === 'undo' ? 'Back to the previous setting.' : f.changes)}</span></li>`; }).join('');
233
+ $('fix-password-note').textContent = { macos: 'macOS will ask for your administrator password.', linux: 'Your system will ask for your password (administrator rights).' }[os] || 'Your computer will ask for your administrator password.';
234
  $('fix-modal').classList.remove('hidden');
235
  }
236
  $('fix-cancel').addEventListener('click', () => { pendingFix = null; $('fix-modal').classList.add('hidden'); });
app/renderer/index.html CHANGED
@@ -88,9 +88,9 @@
88
  <div class="modal hidden" id="fix-modal" role="dialog" aria-modal="true" aria-labelledby="fix-title">
89
  <div class="modal-box">
90
  <h2 id="fix-title">Fix these problems?</h2>
91
- <p class="muted" id="fix-intro">Arx will change these settings, then check this Mac again. You can undo each fix afterwards.</p>
92
  <ul class="fix-list" id="fix-confirm-list"></ul>
93
- <p class="muted">macOS will ask for your administrator password.</p>
94
  <div class="actions">
95
  <button class="secondary" id="fix-cancel">Cancel</button>
96
  <button class="primary" id="fix-go">Fix now</button>
 
88
  <div class="modal hidden" id="fix-modal" role="dialog" aria-modal="true" aria-labelledby="fix-title">
89
  <div class="modal-box">
90
  <h2 id="fix-title">Fix these problems?</h2>
91
+ <p class="muted" id="fix-intro">Arx will change these settings, then check this computer again. You can undo each fix afterwards.</p>
92
  <ul class="fix-list" id="fix-confirm-list"></ul>
93
+ <p class="muted" id="fix-password-note">Your computer will ask for your administrator password.</p>
94
  <div class="actions">
95
  <button class="secondary" id="fix-cancel">Cancel</button>
96
  <button class="primary" id="fix-go">Fix now</button>
collectors/linux/audit.sh CHANGED
@@ -322,7 +322,9 @@ if [ -z "$fw_name" ] && active firewalld; then
322
  fi
323
  if [ -z "$fw_name" ] && have nft && is_root; then
324
  ruleset=$(nft list ruleset 2>/dev/null)
325
- if printf '%s' "$ruleset" | grep -q "hook input"; then
 
 
326
  fw_name=nftables
327
  printf '%s' "$ruleset" | grep -E "hook input.*policy drop" >/dev/null && fw_in=deny || fw_in=allow
328
  printf '%s' "$ruleset" | grep -E "hook output.*policy drop" >/dev/null && fw_out=deny || fw_out=allow
 
322
  fi
323
  if [ -z "$fw_name" ] && have nft && is_root; then
324
  ruleset=$(nft list ruleset 2>/dev/null)
325
+ # Counts only if something is blocked: an input chain with policy drop, or drop/reject rules (a disabled ufw leaves
326
+ # empty chains with policy accept behind, which is no firewall).
327
+ if printf '%s' "$ruleset" | grep -q "hook input" && printf '%s' "$ruleset" | grep -qE "policy drop|[[:space:]](drop|reject)([[:space:]]|$)"; then
328
  fw_name=nftables
329
  printf '%s' "$ruleset" | grep -E "hook input.*policy drop" >/dev/null && fw_in=deny || fw_in=allow
330
  printf '%s' "$ruleset" | grep -E "hook output.*policy drop" >/dev/null && fw_out=deny || fw_out=allow
collectors/linux/fix.sh ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Arx fixer for Linux: applies or undoes the one-click fixes in library/fixes.json, one tested function per fix.
3
+ # The AI never writes these commands; the app only passes fix ids, and this script accepts only the ids below.
4
+ #
5
+ # fix.sh apply --state FILE [--dry-run] ID [ID ...] save the current values to FILE, then fix
6
+ # fix.sh undo --state FILE [--dry-run] ID [ID ...] put back what was saved in FILE
7
+ #
8
+ # Needs root (the app asks with the desktop's password prompt, pkexec). Prints one line per fix:
9
+ # RESULT <tab> id <tab> applied|undone|unchanged|skipped|failed <tab> message
10
+ # State lines: id <tab> key <tab> previous value. Config files Arx edits are copied first to <state>.files/ and copied
11
+ # back on undo; where possible Arx adds its own drop-in file instead (undo = delete it).
12
+ set -u
13
+ ACTION=${1:-}; shift || true
14
+ STATE=""; DRY=0; IDS=()
15
+ while [ $# -gt 0 ]; do
16
+ case "$1" in
17
+ --state) STATE=$2; shift 2 ;;
18
+ --dry-run) DRY=1; shift ;;
19
+ *) IDS+=("$1"); shift ;;
20
+ esac
21
+ done
22
+ case "$ACTION" in apply|undo) ;; *) echo "usage: fix.sh apply|undo --state FILE [--dry-run] ID..." >&2; exit 2 ;; esac
23
+ [ -n "$STATE" ] || { echo "--state is required" >&2; exit 2; }
24
+ [ "$DRY" -eq 1 ] || [ "$(id -u)" -eq 0 ] || { echo "fix.sh must run as root" >&2; exit 3; }
25
+ FILES="$STATE.files"
26
+ export DEBIAN_FRONTEND=noninteractive
27
+
28
+ have() { command -v "$1" >/dev/null 2>&1; }
29
+ result() { printf 'RESULT\t%s\t%s\t%s\n' "$1" "$2" "$3"; }
30
+ run() { if [ "$DRY" -eq 1 ]; then echo "DRY: $*" >&2; else "$@"; fi; }
31
+ save() { [ "$DRY" -eq 1 ] || printf '%s\t%s\t%s\n' "$1" "$2" "$3" >> "$STATE"; }
32
+ saved() { # id key -> the value saved at apply time (last one wins), only if it is one of the known safe forms
33
+ local v
34
+ v=$(awk -F'\t' -v id="$1" -v key="$2" '$1 == id && $2 == key { v = $3 } END { print v }' "$STATE" 2>/dev/null)
35
+ case "$v" in on|off|yes|no|1|0|unset|created|none|allow|deny|reject) printf '%s' "$v" ;;
36
+ /*) [[ "$v" =~ ^/[A-Za-z0-9/_.-]+$ ]] && printf '%s' "$v" ;;
37
+ *) printf '' ;; esac
38
+ }
39
+ backup() { # id file: keep a copy of a config file before Arx edits it (once per apply)
40
+ [ "$DRY" -eq 1 ] && return 0
41
+ mkdir -p "$FILES"; chmod 700 "$FILES"
42
+ if [ -e "$2" ]; then cp -p "$2" "$FILES/$1$(printf '%s' "$2" | tr '/' '_')"; save "$1" "file:$2" yes
43
+ else save "$1" "file:$2" none; fi
44
+ }
45
+ restore() { # id file
46
+ local was; was=$(awk -F'\t' -v id="$1" -v key="file:$2" '$1 == id && $2 == key { v = $3 } END { print v }' "$STATE" 2>/dev/null)
47
+ case "$was" in
48
+ yes) run cp -p "$FILES/$1$(printf '%s' "$2" | tr '/' '_')" "$2" ;;
49
+ none) run rm -f "$2" ;;
50
+ *) return 1 ;;
51
+ esac
52
+ }
53
+ svc_active() { systemctl is-active --quiet "$1" 2>/dev/null; }
54
+ svc_enabled() { systemctl is-enabled --quiet "$1" 2>/dev/null; }
55
+ pkg_install() { # quietly install a package with the system's package manager
56
+ if have apt-get; then run apt-get install -y -qq "$@" >/dev/null 2>&1
57
+ elif have dnf; then run dnf install -y -q "$@" >/dev/null 2>&1
58
+ else return 1; fi
59
+ }
60
+
61
+ # --- firewall: ufw (Debian/Ubuntu) or firewalld (RHEL/Fedora) ----------------------------------------------------
62
+ ufw_state() { ufw status 2>/dev/null | grep -q "Status: active" && echo on || echo off; }
63
+ ufw_in() { ufw status verbose 2>/dev/null | sed -n 's/^Default: //p' | grep -oE '(deny|reject|allow) \(incoming\)' | cut -d' ' -f1; }
64
+ FW_ST=""; FW_IN=""; FW_A=""; FW_E="" # state before this run's change (firewall.enabled and .inbound share it)
65
+ firewall_fix() { # id
66
+ local id=$1
67
+ if have ufw; then
68
+ if [ "$ACTION" = apply ]; then
69
+ [ -n "$FW_ST" ] || { FW_ST=$(ufw_state); FW_IN=$(ufw_in); FW_IN=${FW_IN:-deny}; }
70
+ local st=$FW_ST in=$FW_IN; save "$id" ufw "$st"; save "$id" incoming "$in"
71
+ [ "$(ufw_state)" = on ] && [ "$(ufw_in)" = deny ] && [ "$st" = on ] && [ "$in" = deny ] && { result "$id" unchanged "The firewall was already on and blocking incoming connections."; return; }
72
+ # Keep remote administration working: allow SSH before blocking incoming connections.
73
+ if svc_active ssh || svc_active sshd; then run ufw allow OpenSSH >/dev/null 2>&1 || run ufw allow 22/tcp >/dev/null 2>&1; fi
74
+ [ "$(ufw_state)" = on ] && [ "$(ufw_in)" = deny ] && { result "$id" applied "Firewall (ufw) is on and blocking incoming connections."; return; }
75
+ run ufw default deny incoming >/dev/null && run ufw default allow outgoing >/dev/null && run ufw --force enable >/dev/null \
76
+ && result "$id" applied "Firewall (ufw) turned on: incoming connections are blocked, SSH stays allowed if it was running." \
77
+ || result "$id" failed "Could not turn on ufw."
78
+ else
79
+ local st in; st=$(saved "$id" ufw); in=$(saved "$id" incoming); [ -n "$st" ] || { result "$id" skipped "Nothing saved to undo."; return; }
80
+ [ -n "$in" ] && run ufw default "$in" incoming >/dev/null
81
+ [ "$st" = off ] && run ufw --force disable >/dev/null
82
+ result "$id" undone "Firewall set back to ${st}, incoming ${in:-unchanged}."
83
+ fi
84
+ elif have firewall-cmd; then
85
+ if [ "$ACTION" = apply ]; then
86
+ [ -n "$FW_A" ] || { svc_active firewalld && FW_A=on || FW_A=off; svc_enabled firewalld && FW_E=on || FW_E=off; }
87
+ local a=$FW_A e=$FW_E
88
+ save "$id" firewalld "$a"; save "$id" firewalld_boot "$e"
89
+ [ "$a" = on ] && [ "$e" = on ] && { result "$id" unchanged "firewalld was already running."; return; }
90
+ run systemctl enable --now firewalld >/dev/null 2>&1 && result "$id" applied "Firewall (firewalld) turned on and started at boot." || result "$id" failed "Could not start firewalld."
91
+ else
92
+ local a e; a=$(saved "$id" firewalld); e=$(saved "$id" firewalld_boot); [ -n "$a" ] || { result "$id" skipped "Nothing saved to undo."; return; }
93
+ [ "$a" = off ] && run systemctl stop firewalld >/dev/null 2>&1
94
+ [ "$e" = off ] && run systemctl disable firewalld >/dev/null 2>&1
95
+ result "$id" undone "firewalld set back to $a."
96
+ fi
97
+ else
98
+ result "$id" skipped "No firewall tool is installed. Install ufw (sudo apt install ufw) or firewalld, then try again."
99
+ fi
100
+ }
101
+ fix_firewall_enabled() { firewall_fix firewall.enabled; }
102
+ fix_firewall_inbound() { firewall_fix firewall.inbound; }
103
+
104
+ # --- automatic security updates -------------------------------------------------------------------------------
105
+ APT_DROPIN=/etc/apt/apt.conf.d/52arx-auto-upgrades
106
+ fix_gdpr_auto_updates() {
107
+ local id=gdpr.auto_updates
108
+ if have apt-get; then
109
+ if [ "$ACTION" = apply ]; then
110
+ local uu pkg=no; uu=$(apt-config dump 2>/dev/null | awk -F'"' '/^APT::Periodic::Unattended-Upgrade /{ print $2 }')
111
+ dpkg-query -W -f='${Status}' unattended-upgrades 2>/dev/null | grep -q "install ok installed" && pkg=yes
112
+ save $id package $pkg
113
+ [ "$uu" = 1 ] && [ $pkg = yes ] && { result $id unchanged "Automatic security updates were already on."; return; }
114
+ [ $pkg = yes ] || [ "$DRY" -eq 1 ] || pkg_install unattended-upgrades || { result $id failed "Could not install unattended-upgrades."; return; }
115
+ [ -e "$APT_DROPIN" ] && save $id dropin yes || save $id dropin created
116
+ if [ "$DRY" -eq 1 ]; then echo "DRY: write $APT_DROPIN" >&2
117
+ else printf '// Added by Arx (undo removes this file)\nAPT::Periodic::Update-Package-Lists "1";\nAPT::Periodic::Unattended-Upgrade "1";\n' > "$APT_DROPIN"; fi
118
+ result $id applied "Security updates now install automatically every day (unattended-upgrades)."
119
+ else
120
+ [ "$(saved $id dropin)" = created ] || { result $id skipped "Nothing saved to undo."; return; }
121
+ run rm -f "$APT_DROPIN"
122
+ if [ "$(saved $id package)" = no ]; then run apt-get purge -y -qq unattended-upgrades >/dev/null 2>&1; fi
123
+ result $id undone "Automatic updates set back as they were$( [ "$(saved $id package)" = no ] && echo " (unattended-upgrades removed again)")."
124
+ fi
125
+ elif have dnf; then
126
+ local conf=/etc/dnf/automatic.conf
127
+ if [ "$ACTION" = apply ]; then
128
+ svc_enabled dnf-automatic-install.timer && { save $id timer on; result $id unchanged "Automatic updates were already on."; return; }
129
+ save $id timer off
130
+ [ -e "$conf" ] || [ "$DRY" -eq 1 ] || pkg_install dnf-automatic || { result $id failed "Could not install dnf-automatic."; return; }
131
+ run systemctl enable --now dnf-automatic-install.timer >/dev/null 2>&1 \
132
+ && result $id applied "Updates now download and install automatically (dnf-automatic-install timer)." || result $id failed "Could not enable dnf-automatic."
133
+ else
134
+ [ "$(saved $id timer)" = off ] || { result $id skipped "Nothing saved to undo."; return; }
135
+ run systemctl disable --now dnf-automatic-install.timer >/dev/null 2>&1; result $id undone "Automatic updates turned off again."
136
+ fi
137
+ else
138
+ result $id skipped "Unknown package manager."
139
+ fi
140
+ }
141
+
142
+ # --- time synchronisation -------------------------------------------------------------------------------------
143
+ fix_gdpr_time_sync() {
144
+ local id=gdpr.time_sync
145
+ have timedatectl || { result $id skipped "timedatectl is not available."; return; }
146
+ if [ "$ACTION" = apply ]; then
147
+ local now; now=$(timedatectl show -p NTP --value 2>/dev/null)
148
+ [ "$now" = yes ] && { result $id unchanged "Network time was already on."; return; }
149
+ if run timedatectl set-ntp true 2>/dev/null; then save $id ntp no; result $id applied "The clock now sets itself from network time servers."
150
+ else result $id failed "Could not turn on network time (is systemd-timesyncd or chrony installed?)."; fi
151
+ else
152
+ [ "$(saved $id ntp)" = no ] || { result $id skipped "Nothing saved to undo."; return; }
153
+ run timedatectl set-ntp false 2>/dev/null; result $id undone "Network time turned off again."
154
+ fi
155
+ }
156
+
157
+ # --- guest login ----------------------------------------------------------------------------------------------
158
+ fix_gdpr_guest_account() {
159
+ local id=gdpr.guest_account changed=0
160
+ if [ "$ACTION" = apply ]; then
161
+ local f
162
+ while IFS= read -r f; do
163
+ [ -n "$f" ] || continue
164
+ backup $id "$f"; run sed -i -E 's/^([[:space:]]*allow-guest[[:space:]]*=[[:space:]]*)true/\1false/I' "$f"; changed=1
165
+ done < <(grep -rlsiE '^[[:space:]]*allow-guest[[:space:]]*=[[:space:]]*true' /etc/lightdm)
166
+ local shell; shell=$(awk -F: '$1 == "guest" { print $7 }' /etc/passwd)
167
+ if [ -n "$shell" ] && ! [[ "$shell" =~ (nologin|false)$ ]]; then
168
+ save $id guest_shell "$shell"; run usermod -s /usr/sbin/nologin guest; changed=1
169
+ fi
170
+ [ $changed -eq 1 ] && result $id applied "Guest login turned off." || result $id unchanged "No guest login was enabled."
171
+ else
172
+ local any=0
173
+ while IFS= read -r f; do restore $id "$f" && any=1; done < <(awk -F'\t' -v id="$id" '$1 == id && $2 ~ /^file:/ { print substr($2, 6) }' "$STATE" 2>/dev/null | sort -u)
174
+ local sh; sh=$(saved $id guest_shell); [ -n "$sh" ] && { run usermod -s "$sh" guest; any=1; }
175
+ [ $any -eq 1 ] && result $id undone "Guest login set back as it was." || result $id skipped "Nothing saved to undo."
176
+ fi
177
+ }
178
+
179
+ # --- usage reporting ------------------------------------------------------------------------------------------
180
+ fix_gdpr_telemetry() {
181
+ local id=gdpr.telemetry changed=0 pc=/etc/popularity-contest.conf
182
+ if [ "$ACTION" = apply ]; then
183
+ if grep -qsE '^PARTICIPATE="?yes' "$pc"; then backup $id "$pc"; run sed -i 's/^PARTICIPATE=.*/PARTICIPATE="no"/' "$pc"; changed=1; fi
184
+ if svc_active whoopsie || svc_enabled whoopsie; then save $id whoopsie on; run systemctl disable --now whoopsie >/dev/null 2>&1; changed=1; fi
185
+ [ $changed -eq 1 ] && result $id applied "Usage and error reporting turned off." || result $id unchanged "Usage reporting was already off."
186
+ else
187
+ local any=0
188
+ restore $id "$pc" && any=1
189
+ [ "$(saved $id whoopsie)" = on ] && { run systemctl enable --now whoopsie >/dev/null 2>&1; any=1; }
190
+ [ $any -eq 1 ] && result $id undone "Usage reporting set back as it was." || result $id skipped "Nothing saved to undo."
191
+ fi
192
+ }
193
+
194
+ # --- automatic login ------------------------------------------------------------------------------------------
195
+ fix_account_unique_login() { # turns off automatic login only; shared accounts need a person to decide
196
+ local id=account.unique_login changed=0 f
197
+ if [ "$ACTION" = apply ]; then
198
+ for f in /etc/gdm3/custom.conf /etc/gdm/custom.conf; do
199
+ if grep -qsiE '^[[:space:]]*AutomaticLoginEnable[[:space:]]*=[[:space:]]*true' "$f"; then
200
+ backup $id "$f"; run sed -i -E 's/^([[:space:]]*AutomaticLoginEnable[[:space:]]*=[[:space:]]*).*/\1false/I' "$f"; changed=1
201
+ fi
202
+ done
203
+ for f in /etc/lightdm/lightdm.conf /etc/lightdm/lightdm.conf.d/*.conf; do
204
+ if [ -f "$f" ] && grep -qsE '^[[:space:]]*autologin-user[[:space:]]*=[[:space:]]*[^[:space:]]' "$f"; then
205
+ backup $id "$f"; run sed -i -E 's/^([[:space:]]*autologin-user[[:space:]]*=).*/\1/' "$f"; changed=1
206
+ fi
207
+ done
208
+ for f in /etc/sddm.conf /etc/sddm.conf.d/*.conf; do
209
+ if [ -f "$f" ] && awk '/^\[Autologin\]/ { a = 1; next } /^\[/ { a = 0 } a && /^[[:space:]]*User[[:space:]]*=[[:space:]]*[^[:space:]]/ { f = 1 } END { exit !f }' "$f"; then
210
+ backup $id "$f"; run sed -i -E '/^\[Autologin\]/,/^\[/ s/^([[:space:]]*User[[:space:]]*=).*/\1/' "$f"; changed=1
211
+ fi
212
+ done
213
+ [ $changed -eq 1 ] && result $id applied "Automatic login turned off: everyone signs in with their own password. Shared accounts (if any) still need a decision by a person." \
214
+ || result $id unchanged "Automatic login was already off. Shared accounts need a decision by a person (see the report)."
215
+ else
216
+ local any=0
217
+ while IFS= read -r f; do restore $id "$f" && any=1; done < <(awk -F'\t' -v id="$id" '$1 == id && $2 ~ /^file:/ { print substr($2, 6) }' "$STATE" 2>/dev/null | sort -u)
218
+ [ $any -eq 1 ] && result $id undone "Login settings put back as they were." || result $id skipped "Nothing saved to undo."
219
+ fi
220
+ }
221
+
222
+ # --- ClamAV (only when it is the malware protection in use) ------------------------------------------------------
223
+ fix_malware_signatures() {
224
+ local id=malware.signatures
225
+ [ "$ACTION" = undo ] && { result $id unchanged "Malware definition updates are not undone."; return; }
226
+ have freshclam || { result $id skipped "ClamAV is not installed."; return; }
227
+ if svc_active clamav-freshclam; then run systemctl restart clamav-freshclam >/dev/null 2>&1 && result $id applied "Asked ClamAV to download the latest malware definitions." && return; fi
228
+ run freshclam --quiet >/dev/null 2>&1 && result $id applied "Malware definitions (ClamAV) updated." || result $id failed "freshclam could not update (check the internet connection)."
229
+ }
230
+ fix_malware_signature_updates() {
231
+ local id=malware.signature_updates
232
+ have freshclam || { result $id skipped "ClamAV is not installed."; return; }
233
+ if [ "$ACTION" = apply ]; then
234
+ svc_active clamav-freshclam && { save $id freshclam on; result $id unchanged "Definition updates were already automatic."; return; }
235
+ save $id freshclam off
236
+ run systemctl enable --now clamav-freshclam >/dev/null 2>&1 && result $id applied "ClamAV now updates its malware definitions automatically." || result $id failed "Could not start clamav-freshclam."
237
+ else
238
+ [ "$(saved $id freshclam)" = off ] || { result $id skipped "Nothing saved to undo."; return; }
239
+ run systemctl disable --now clamav-freshclam >/dev/null 2>&1; result $id undone "Automatic definition updates turned off again."
240
+ fi
241
+ }
242
+
243
+ # --- main ---------------------------------------------------------------------------------------------------
244
+ [ "$DRY" -eq 1 ] || { touch "$STATE"; chmod 600 "$STATE"; }
245
+ for id in "${IDS[@]}"; do
246
+ case "$id" in
247
+ firewall.enabled|firewall.inbound|gdpr.auto_updates|gdpr.time_sync|gdpr.guest_account|gdpr.telemetry|account.unique_login|malware.signatures|malware.signature_updates)
248
+ "fix_${id//./_}" ;;
249
+ *) result "$id" skipped "Arx has no automatic fix for this check." ;;
250
+ esac
251
+ done
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
- <meta name="description" content="Arx: a free security audit for your Mac, Windows or Linux computer. 70 checks in about a minute, offline, mapped to ISO 27001, GDPR and the EU AI Act, with one-click fixes on Mac. Nothing is uploaded.">
7
  <meta name="theme-color" content="#ed2939">
8
  <title>Arx · Security audit for your computer</title>
9
  <link rel="icon" href="app/build/icon.png">
@@ -33,7 +33,7 @@
33
  <section class="wrap hero">
34
  <p class="eyebrow"><img src="app/build/icon.png" alt="">Arx security audit</p>
35
  <h1>Is your computer secure?<br><span class="grad">Find out in a minute.</span></h1>
36
- <p class="lead">A free audit of your Mac, Windows or Linux computer: <b><span data-count>70</span> checks</b> mapped to <b>ISO 27001</b>, <b>GDPR</b> and the <b>EU AI Act</b>. A small AI on your computer writes the report, and on a Mac Arx can <b>fix problems for you</b>. It runs offline. <u>Nothing is uploaded.</u></p>
37
 
38
  <div class="device-card" id="deviceCard">
39
  <span class="device-icon" id="deviceIcon">💻</span>
@@ -56,7 +56,7 @@
56
  <label class="btn btn-lg" for="reportFile" title="Open the …-evidence.json file Arx saves next to each report">Open a report</label>
57
  <input type="file" id="reportFile" accept=".json,application/json" hidden>
58
  </div>
59
- <p class="tiny muted" id="heroMeta">Free · open source · version 0.5.2</p>
60
  </section>
61
 
62
  <section class="wrap">
@@ -78,7 +78,7 @@
78
  <ol class="steps">
79
  <li><span class="step-n">1</span><div><b>Download and open Arx</b><p class="muted">One app for your system. Admin rights let it read every setting; without them, a few checks are skipped.</p></div></li>
80
  <li><span class="step-n">2</span><div><b>Run the audit</b><p class="muted">The audit only reads settings: nothing changes unless you later click Fix. The first run downloads the AI once; after that it works offline.</p></div></li>
81
- <li><span class="step-n">3</span><div><b>Get your report and fix</b><p class="muted">A score, your biggest gaps and how to fix each one, as a PDF. On a Mac, click <b>Fix</b> and Arx changes the setting for you, or opens the right System Settings page. Every fix can be undone.</p></div></li>
82
  </ol>
83
  </section>
84
 
@@ -95,7 +95,7 @@
95
  </section>
96
 
97
  <section class="wrap section" id="download">
98
- <div class="section-head"><h2>Download Arx</h2><span class="tiny muted" id="dlVersion">Version 0.5.2</span></div>
99
  <div class="dl-grid">
100
  <article class="dl-card" data-os="mac">
101
  <span class="ribbon ribbon-you hidden">YOUR SYSTEM</span>
 
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <meta name="description" content="Arx: a free security audit for your Mac, Windows or Linux computer. 70 checks in about a minute, offline, mapped to ISO 27001, GDPR and the EU AI Act, with one-click fixes on Mac and Linux. Nothing is uploaded.">
7
  <meta name="theme-color" content="#ed2939">
8
  <title>Arx · Security audit for your computer</title>
9
  <link rel="icon" href="app/build/icon.png">
 
33
  <section class="wrap hero">
34
  <p class="eyebrow"><img src="app/build/icon.png" alt="">Arx security audit</p>
35
  <h1>Is your computer secure?<br><span class="grad">Find out in a minute.</span></h1>
36
+ <p class="lead">A free audit of your Mac, Windows or Linux computer: <b><span data-count>70</span> checks</b> mapped to <b>ISO 27001</b>, <b>GDPR</b> and the <b>EU AI Act</b>. A small AI on your computer writes the report, and on Mac and Linux Arx can <b>fix problems for you</b>. It runs offline. <u>Nothing is uploaded.</u></p>
37
 
38
  <div class="device-card" id="deviceCard">
39
  <span class="device-icon" id="deviceIcon">💻</span>
 
56
  <label class="btn btn-lg" for="reportFile" title="Open the …-evidence.json file Arx saves next to each report">Open a report</label>
57
  <input type="file" id="reportFile" accept=".json,application/json" hidden>
58
  </div>
59
+ <p class="tiny muted" id="heroMeta">Free · open source · version 0.5.3</p>
60
  </section>
61
 
62
  <section class="wrap">
 
78
  <ol class="steps">
79
  <li><span class="step-n">1</span><div><b>Download and open Arx</b><p class="muted">One app for your system. Admin rights let it read every setting; without them, a few checks are skipped.</p></div></li>
80
  <li><span class="step-n">2</span><div><b>Run the audit</b><p class="muted">The audit only reads settings: nothing changes unless you later click Fix. The first run downloads the AI once; after that it works offline.</p></div></li>
81
+ <li><span class="step-n">3</span><div><b>Get your report and fix</b><p class="muted">A score, your biggest gaps and how to fix each one, as a PDF. On Mac and Linux, click <b>Fix</b> and Arx changes the setting for you (on a Mac it also opens the right System Settings page for the rest). Every fix can be undone.</p></div></li>
82
  </ol>
83
  </section>
84
 
 
95
  </section>
96
 
97
  <section class="wrap section" id="download">
98
+ <div class="section-head"><h2>Download Arx</h2><span class="tiny muted" id="dlVersion">Version 0.5.3</span></div>
99
  <div class="dl-grid">
100
  <article class="dl-card" data-os="mac">
101
  <span class="ribbon ribbon-you hidden">YOUR SYSTEM</span>
library/fixes.json CHANGED
@@ -9,6 +9,13 @@
9
  "changes": "Turns on the macOS firewall, which blocks unwanted incoming connections.",
10
  "note": "Apps you already allowed keep working.",
11
  "undo": true
 
 
 
 
 
 
 
12
  }
13
  },
14
  "firewall.inbound": {
@@ -18,6 +25,13 @@
18
  "changes": "Turns on the firewall and stealth mode, so the Mac does not answer probes from other computers on the network.",
19
  "note": "Other computers can no longer ping this Mac.",
20
  "undo": true
 
 
 
 
 
 
 
21
  }
22
  },
23
  "malware.service": {
@@ -36,6 +50,17 @@
36
  "changes": "Installs the latest XProtect malware definitions from Apple.",
37
  "note": "An update: there is nothing to undo.",
38
  "undo": false
 
 
 
 
 
 
 
 
 
 
 
39
  }
40
  },
41
  "malware.signature_updates": {
@@ -45,6 +70,17 @@
45
  "changes": "Turns on 'Install Security Responses and system files' in Software Update, so malware definitions stay current.",
46
  "note": "",
47
  "undo": true
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
  },
50
  "gdpr.auto_updates": {
@@ -54,6 +90,13 @@
54
  "changes": "Turns on checking for, downloading and installing macOS updates and security responses automatically.",
55
  "note": "macOS may restart overnight to finish an update; it warns first.",
56
  "undo": true
 
 
 
 
 
 
 
57
  }
58
  },
59
  "gdpr.guest_account": {
@@ -63,6 +106,13 @@
63
  "changes": "Turns off guest login and guest access to shared folders.",
64
  "note": "",
65
  "undo": true
 
 
 
 
 
 
 
66
  }
67
  },
68
  "gdpr.telemetry": {
@@ -72,6 +122,13 @@
72
  "changes": "Turns off 'Share Mac Analytics' and 'Share with app developers'.",
73
  "note": "",
74
  "undo": true
 
 
 
 
 
 
 
75
  }
76
  },
77
  "gdpr.time_sync": {
@@ -81,6 +138,13 @@
81
  "changes": "Turns on 'Set time and date automatically' from Apple's time server, so logs have correct times.",
82
  "note": "",
83
  "undo": true
 
 
 
 
 
 
 
84
  }
85
  },
86
  "screen_lock.enabled": {
@@ -240,6 +304,13 @@
240
  "Set 'Automatically log in as' to Off (at the bottom of the page).",
241
  "Give each person their own account; remove or rename shared accounts such as office or admin, keeping one named IT administrator."
242
  ]
 
 
 
 
 
 
 
243
  }
244
  }
245
  }
 
9
  "changes": "Turns on the macOS firewall, which blocks unwanted incoming connections.",
10
  "note": "Apps you already allowed keep working.",
11
  "undo": true
12
+ },
13
+ "linux": {
14
+ "kind": "auto",
15
+ "title": "Turn on the firewall",
16
+ "changes": "Turns on the firewall (ufw, or firewalld on Red Hat-type systems) and blocks incoming connections. If remote login (SSH) is running, it stays allowed.",
17
+ "note": "Outgoing connections are not affected.",
18
+ "undo": true
19
  }
20
  },
21
  "firewall.inbound": {
 
25
  "changes": "Turns on the firewall and stealth mode, so the Mac does not answer probes from other computers on the network.",
26
  "note": "Other computers can no longer ping this Mac.",
27
  "undo": true
28
+ },
29
+ "linux": {
30
+ "kind": "auto",
31
+ "title": "Block incoming connections",
32
+ "changes": "Turns on the firewall (ufw or firewalld) with incoming connections blocked by default. If remote login (SSH) is running, it stays allowed.",
33
+ "note": "Programs that serve other computers (for example a local web server) may need a firewall rule.",
34
+ "undo": true
35
  }
36
  },
37
  "malware.service": {
 
50
  "changes": "Installs the latest XProtect malware definitions from Apple.",
51
  "note": "An update: there is nothing to undo.",
52
  "undo": false
53
+ },
54
+ "linux": {
55
+ "kind": "auto",
56
+ "title": "Update malware definitions",
57
+ "changes": "Downloads the latest ClamAV malware definitions (freshclam).",
58
+ "note": "An update: there is nothing to undo.",
59
+ "undo": false,
60
+ "requires": {
61
+ "check": "malware.antivirus",
62
+ "contains": "ClamAV"
63
+ }
64
  }
65
  },
66
  "malware.signature_updates": {
 
70
  "changes": "Turns on 'Install Security Responses and system files' in Software Update, so malware definitions stay current.",
71
  "note": "",
72
  "undo": true
73
+ },
74
+ "linux": {
75
+ "kind": "auto",
76
+ "title": "Update malware definitions automatically",
77
+ "changes": "Starts the ClamAV definition updater (clamav-freshclam) and runs it at every start-up.",
78
+ "note": "",
79
+ "undo": true,
80
+ "requires": {
81
+ "check": "malware.antivirus",
82
+ "contains": "ClamAV"
83
+ }
84
  }
85
  },
86
  "gdpr.auto_updates": {
 
90
  "changes": "Turns on checking for, downloading and installing macOS updates and security responses automatically.",
91
  "note": "macOS may restart overnight to finish an update; it warns first.",
92
  "undo": true
93
+ },
94
+ "linux": {
95
+ "kind": "auto",
96
+ "title": "Turn on automatic security updates",
97
+ "changes": "Installs and turns on unattended-upgrades (Ubuntu, Debian) or dnf-automatic (Red Hat-type systems), so security updates install every day.",
98
+ "note": "Undo turns it off again and removes the tool if Arx installed it.",
99
+ "undo": true
100
  }
101
  },
102
  "gdpr.guest_account": {
 
106
  "changes": "Turns off guest login and guest access to shared folders.",
107
  "note": "",
108
  "undo": true
109
+ },
110
+ "linux": {
111
+ "kind": "auto",
112
+ "title": "Turn off guest login",
113
+ "changes": "Turns off the guest session on the login screen and the login shell of a 'guest' account.",
114
+ "note": "",
115
+ "undo": true
116
  }
117
  },
118
  "gdpr.telemetry": {
 
122
  "changes": "Turns off 'Share Mac Analytics' and 'Share with app developers'.",
123
  "note": "",
124
  "undo": true
125
+ },
126
+ "linux": {
127
+ "kind": "auto",
128
+ "title": "Stop usage reporting",
129
+ "changes": "Turns off popularity-contest and the whoopsie error reporter.",
130
+ "note": "",
131
+ "undo": true
132
  }
133
  },
134
  "gdpr.time_sync": {
 
138
  "changes": "Turns on 'Set time and date automatically' from Apple's time server, so logs have correct times.",
139
  "note": "",
140
  "undo": true
141
+ },
142
+ "linux": {
143
+ "kind": "auto",
144
+ "title": "Set the time automatically",
145
+ "changes": "Turns on network time (timedatectl set-ntp true), so logs have correct times.",
146
+ "note": "",
147
+ "undo": true
148
  }
149
  },
150
  "screen_lock.enabled": {
 
304
  "Set 'Automatically log in as' to Off (at the bottom of the page).",
305
  "Give each person their own account; remove or rename shared accounts such as office or admin, keeping one named IT administrator."
306
  ]
307
+ },
308
+ "linux": {
309
+ "kind": "auto",
310
+ "title": "Turn off automatic login",
311
+ "changes": "Turns off automatic login (GDM, LightDM or SDDM), so everyone signs in with their own password.",
312
+ "note": "Shared accounts are not changed: a person decides which to remove.",
313
+ "undo": true
314
  }
315
  }
316
  }